分类: LINUX
2009-02-27 22:14:08
------------------------------------
本文系本站原创,欢迎转载!
当我们打开机器或一个嵌入式系统时,我们可能都适应了它会显示信息,我们也理所当然的认为这是应该,但是它从计算机中通过多少关口才到达显示器或串口终端显示的呢,估计大家都没在意过,其实这个过程在linux操作系统中可为千辛万库啊,当然在其他操作系统也一样,今天我就来分析从printf到显示设备的过程。
我们先来看下面这些程序。
1 If (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
2 (void) sys_dup(0);
3 (void) sys_dup(0);
第1语句执行后生成第一个文件,这个文件的文件号为0,第2语句过后产生第二个文件,这个文件的文件号为1,第3语句过后产生第三个文件,这个文件的文件号为2,这时这三个文件都指向同一个文件,那就是设备文件/dev/console,这个三个文件就是大名鼎鼎的标准输入,标准输出,错误输出文件,当然他们有不同属性,后来才改,以后对这些文件号的操作就是对/dev/console的操作,由于所有进程是由这个进程fork继承而来,故所有进程都拥有这些文件号,都可以执行写读操作,即向显示设备输出内容,输入设备读入内容。
在上面语句没有执行前都是通过printk来输出字符,printk是不通过文件系统输出字符的
,而直接通过相关驱动输出到显示设备,执行这些语句后,大家就可以通过向0号文件写入或读入内容(write(0,buf,len))来达到输出字符的目的,也许你会说我们没有用这个write函数啊,其实是操作系统帮你调用这个函数的,也许大家经常在应用程序用printf,这个函数就会调用系统调用write(0,buf,len)等等。
应用层分析就到此位置,但是设备层又来了。
当我们向/dev/console设备文件写或读入数据,怎么就在显示设备上(没有在串口中)显示了,并且在Pc上它是在显示器上显示,在arm等嵌入式设备上,往往是在串口中显示(当然要超级终端或minicom),难道/dev/console很智能,对它确实很智能,要分析这个过程,就不得不谈到终端的概念。
1. 一般终端
2. 控制台终端
3 pts
控制台终端是终端的一种,如/dev/tty0,/dev/tty1,…./dev/tty8等虚拟控制台终端,其中/dev/tty0叫当前控制台终端,它通常指向/dev/tty1,…../dev/tty8中的一个,/dev/tty是进程的终端,它可以是控制台终端也可以是其他终端,反正它就是进程拥有的终端。
现在开始非常重要的终端到了,那就是/dev/console有很多人将它说成是外接控制台终端,我不理解,我认为它就是用来输出信息(应用程序printf使用)的终端。我们知道
在pc中我们的控制台就是/tty1,……/tty..,我们应该记得ctrl+alt+fn来切换控制台吧,这个时候这些控制台就是对应/dev/tty1……/dev/tty4,这个时候这些控制台就是用来输出信息的,照这样说来,/dev/console应该是指向/dev/tty*中的一个,事实上是对的,我们可以在Linux文本模式下实验,在第一个控制台,我们输入
echo “kfdl”>/dev/tty0,echo “kfds”>/dev/tty1,echo”kfdl”>/dev/console这三调命令都可以在屏幕下看到信息,这说明这三个设备文件都引导到了同一个tty_driver,tty_struct,
然后按ctrl+alt+f2,进入另一个控制,
echo “kfdl”>/dev/tty0,echo “kfds”>/dev/tty2,echo”kfdl”>/dev/console,仍然可以看到信息,但输入/dev/tty1输出不了信息,为什么,因为这个是控制台2了啊,而console有机制可以指向活动的tty,所以可以显示,当然/dev/tty0本身就是指向活动的控制台的。
在arm嵌入式系统中,
我们也可以实验,但是结果很不一样,
echo “kfdl”>/dev/tty0,echo “kfds”>/dev/tty1,echo”kfdl”>/dev/console
中只有echo”kfdl”>/dev/console能输出信息,这是为什么因为在arm的嵌入式系统中,显示信息的不是显示器了,是串口了,所以由于console是引导向显示信息的设备,而
Tty1-tty4只是控制台(只不过在Pc中恰好就是显示器作为显示它才能显示),故只有console,当然如果有显示器等显示设别,echo “kfdl”>/dev/tty0,echo “kfds”>/dev/tty1可以在显示器等设备显示,但在串口是如何都不能显示的。
这时如何实现的,我们知道当我们打开终端设备时,内核的入口就是
Tty设备文件打开关键是找到tty_driver,因为它里面有具体tty的操作函数啊。
static int tty_open(struct inode *inode, struct file *filp)
{
struct tty_struct *tty;
int noctty, retval;
struct tty_driver *driver;
int index;
dev_t device = inode->i_rdev;
unsigned short saved_flags = filp->f_flags;
nonseekable_open(inode, filp);
retry_open:
//O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端
//noctty:需不需要更改当前进程的控制终端
noctty = filp->f_flags & O_NOCTTY;
index = -1;
retval = 0;
mutex_lock(&tty_mutex);
//设备号(5,0) 即/dev/tty.表示当前进程的控制终端
if (device == MKDEV(TTYAUX_MAJOR, 0)) {//tty设备文件的driver的获取方式
tty = get_current_tty();/这就是为什么说tty代表当前进程终端,代码中就是这样实现的啊
//如果当前进程的控制终端不存在,退出
if (!tty) {
mutex_unlock(&tty_mutex);
return -ENXIO;
}
//取得当前进程的tty_driver
driver = tty->driver;
index = tty->index;
filp->f_flags |= O_NONBLOCK; /* Don't let /dev/tty block */
/* noctty = 1; */
goto got_driver;
}
#ifdef CONFIG_VT
//设备号(4,0).即/dev/tty0:表示当前的控制台
if (device == MKDEV(TTY_MAJOR, 0)) {
extern struct tty_driver *console_driver;//tty0设备文件的driver的获取方式
driver = console_driver;
//fg_console: 表示当前的控制台全局变量(console_driver ,fg-console)设置了它为活动控制台
index = fg_console;
noctty = 1;
goto got_driver;
}
#endif
//设备号(5,1).即/dev/console.表示外接的控制台. 通过regesit_console()
if (device == MKDEV(TTYAUX_MAJOR, 1)) {
driver = console_device(&index);//关键在这里/console设备文件的driver的获取方式
if (driver) {
/* Don't let /dev/console block */
filp->f_flags |= O_NONBLOCK;
noctty = 1;
goto got_driver;
}
mutex_unlock(&tty_mutex);
return -ENODEV;
}
driver = get_tty_driver(device, &index);//tty1~ttyn设备文件driver获取的方式
……….
………….
return 0;
}
console_device致关重要啊:
struct tty_driver *console_device(int *index)
{
struct console *c;
struct tty_driver *driver = NULL;
acquire_console_sem();
for (c = console_drivers; c != NULL; c = c->next) {
if (!c->device)
continue;
driver = c->device(c, index);//console的tty找到了啊
if (driver)
break;
}
release_console_sem();
return driver;
}
由上面的红色可知,console设备的driver由console_drivers获取。
那console_drivers是如何赋值的,
它是通过register_console 注册的。
void register_console(struct console * console)
{
………………………
………………………..
if ((console->flags & CON_CONSDEV) || console_drivers == NULL) {
console->next = console_drivers;
console_drivers = console;
if (console->next)
console->next->flags &= ~CON_CONSDEV;
} else {
console->next = console_drivers->next;
console_drivers->next = console;//这些就将console挂到console_drivers链上了啊
}
if (console->flags & CON_PRINTBUFFER) {
/*
* release_console_sem() will print out the buffered messages
* for us.
*/
spin_lock_irqsave(&logbuf_lock, flags);
con_start = log_start;
spin_unlock_irqrestore(&logbuf_lock, flags);
}
………
………
}
由上可知在Pc中console和tty0~tty1显示是一样的,那它的驱动应该是一样的,也就是说console_driver->device返回的tty_driver应该是console_driver,是如何实现,这个就是con_init实现的哦,
static int __init con_init(void)
{
const char *display_desc = NULL;
struct vc_data *vc;
unsigned int currcons = 0;
acquire_console_sem();
if (conswitchp)
display_desc = conswitchp->con_startup();
if (!display_desc) {
fg_console = 0;
release_console_sem();
return 0;
}
init_timer(&console_timer);
console_timer.function = blank_screen_t;
if (blankinterval) {
blank_state = blank_normal_wait;
mod_timer(&console_timer, jiffies + blankinterval);
}
/*
* kmalloc is not running yet - we use the bootmem allocator.
*/
for (currcons = 0; currcons < MIN_NR_CONSOLES; currcons++) {
vc_cons[currcons].d = vc = alloc_bootmem(sizeof(struct vc_data));
visual_init(vc, currcons, 1);
vc->vc_screenbuf = (unsigned short *)alloc_bootmem(vc->vc_screenbuf_size);
vc->vc_kmalloced = 0;
vc_init(vc, vc->vc_rows, vc->vc_cols,
currcons || !vc->vc_sw->con_save_screen);
}
currcons = fg_console = 0;
master_display_fg = vc = vc_cons[currcons].d;
set_origin(vc);
save_screen(vc);
gotoxy(vc, vc->vc_x, vc->vc_y);
csi_J(vc, 0);
update_screen(vc);
printk("Console: %s %s %dx%d",
vc->vc_can_do_color ? "colour" : "mono",
display_desc, vc->vc_cols, vc->vc_rows);
printable = 1;
printk("\n");
release_console_sem();
#ifdef CONFIG_VT_CONSOLE
register_console(&vt_console_driver);//这一句致关重要,它注册了console,挂到了console_driver
#endif
return 0;
}
再看下vt_console_driver的console_device是不是返回console_driver,
static struct console vt_console_driver = {
.name = "tty",
.write = vt_console_print,
.device = vt_console_device,
.unblank = unblank_screen,
.flags = CON_PRINTBUFFER,
.index = -1,
};
static struct tty_driver *vt_console_device(struct console *c, int *index)
{
*index = c->index ? c->index-1 : fg_console;
return console_driver;
}
console_initcall(con_init);
正是,大功告成。
下面来看在arm等嵌入式系统中,console是和串口相关练的,那么应该也有一个类似的注册console的函数,经发现,正是啊
在s3c2410.c是
tatic int s3c24xx_serial_initconsole(void)
{
struct s3c24xx_uart_info *info;
struct platform_device *dev = s3c24xx_uart_devs[0];
dbg("s3c24xx_serial_initconsole\n");
/* select driver based on the cpu */
if (dev == NULL) {
printk(KERN_ERR "s3c24xx: no devices for console init\n");
return 0;
}
if (strcmp(dev->name, "s3c2400-uart") == 0) {
info = s3c2400_uart_inf_at;
} else if (strcmp(dev->name, "s3c2410-uart") == 0) {
info = s3c2410_uart_inf_at;
} else if (strcmp(dev->name, "s3c2440-uart") == 0) {
info = s3c2440_uart_inf_at;
} else {
printk(KERN_ERR "s3c24xx: no driver for %s\n", dev->name);
return 0;
}
if (info == NULL) {
printk(KERN_ERR "s3c24xx: no driver for console\n");
return 0;
}
s3c24xx_serial_console.data = &s3c24xx_uart_drv;
s3c24xx_serial_init_ports(info);
register_console(&s3c24xx_serial_console);
return 0;
}
static struct console s3c24xx_serial_console =
{
.name = S3C24XX_SERIAL_NAME,
.device = uart_console_device,
.flags = CON_PRINTBUFFER,
.index = -1,
.write = s3c24xx_serial_console_write,
.setup = s3c24xx_serial_console_setup
};
struct tty_driver *uart_console_device(struct console *co, int *index)
{
struct uart_driver *p = co->data;
*index = co->index;
return p->tty_driver;
}
我们可以知道,返回的是s3c24xx_uart_drv中的tty_dirver,这个driver是何时注册的呢,
其实这就关系到uart结构
static int __init s3c24xx_serial_modinit(void)
{
int ret;
ret = uart_register_driver(&s3c24xx_uart_drv);
if (ret < 0) {
printk(KERN_ERR "failed to register UART driver\n");
return -1;
}
s3c2400_serial_init();
s3c2410_serial_init();
s3c2440_serial_init();
return 0;
}
int uart_register_driver(struct uart_driver *drv)
{
struct tty_driver *normal = NULL;
int i, retval;
BUG_ON(drv->state);
/*
* Maybe we should be using a slab cache for this, especially if
* we have a large number of ports to handle.
*/
drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL);
retval = -ENOMEM;
if (!drv->state)
goto out;
normal = alloc_tty_driver(drv->nr);
if (!normal)
goto out;
drv->tty_driver = normal;
normal->owner = drv->owner;
normal->driver_name = drv->driver_name;
normal->name = drv->dev_name;
normal->major = drv->major;
normal->minor_start = drv->minor;
normal->type = TTY_DRIVER_TYPE_SERIAL;
normal->subtype = SERIAL_TYPE_NORMAL;
normal->init_termios = tty_std_termios;
normal->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
normal->init_termios.c_ispeed = normal->init_termios.c_ospeed = 9600;
normal->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;
normal->driver_state = drv;
tty_set_operations(normal, &uart_ops);
/*
* Initialise the UART state(s).
*/
for (i = 0; i < drv->nr; i++) {
struct uart_state *state = drv->state + i;
state->close_delay = 500; /* .5 seconds */
state->closing_wait = 30000; /* 30 seconds */
mutex_init(&state->mutex);
}
retval = tty_register_driver(normal);
out:
if (retval < 0) {
put_tty_driver(normal);
kfree(drv->state);
}
return retval;
}
从上面可以看到,当串口注册时它作为一般的tty_driver注册,但是由于uart_driver还是console.data,且在console设备通过console_device然后uart_driver->device函数会把uart_driver的tty_driver(上面的normal)赋给console故它还是console的tty_driver,这个可以明白,串口我们当然也希望当作一般的串口接口,而不仅仅是作为控制终端在超级终端中显示的。
无论是一般tty_driver,还是console的tty_driver,它的统一接口是uart_opd,这就区别了其他tty_driver,
Tty_driver->driver_data=uart_driver
Uart_open后
Tty_struct->driver_data=uart_state;
那s3c24xx_serial_initconsole,和con_init又是在那调用的呢在start_kernel->console_init调用的
all = __con_initcall_start;
while (call < __con_initcall_end) {
(*call)();
call++;
}
__con_initcall_start是一个初始段,通过console_initcall注册,
#define console_initcall(fn) \
static initcall_t __initcall_##fn \
__attribute_used__ __attribute__((__section__(".con_initcall.init")))=fn
Console_initcall(con_init)
console_initcall(s3c24xx_serial_initconsole);
下面讲/dev/tty,/dev/tty0,/dev/console,串口终端/tts/0~n(在内核叫做ttySAC0~n)的产生过程,虚拟控制台终端/dev/tty1~ttyn
/dev/tty,/dev/tty0,/dev/console都是在tty_init中通过cdev_add,register_chrdev()等添加
的,而/dev/tty~ttyn是在vty_init中通过tty_driver_register创建的,并且创建console_driver->num即MAX_NR_CONSOLES,在alloc_ttydriver是赋值的,而/tts/0~n是在s3c24xx_serial_probe中,当static inline int s3c2440_serial_init(void)
{
return s3c24xx_serial_init(&s3c2440_serial_drv, &s3c2440_uart_inf);
}
当s3c2440_serial_drv检测到串口设备时就会调用s3c24xx_serial_probe,然后它就会通过uart_add_one_port->tty_register_device(由于这个device的dev_t由它的tty_driver major指定,当打开这个设备肯定可以找到这个驱动)创建终端的设备,如果这个些串口终端设备的uart_driver->con存在且con.flags=con_enable,它也同时console_register,所以这时console可以指向它们中的任意一个。