Linux设备模型之tty驱动架构分析

2019-07-13 03:14发布

Linux设备模型之tty驱动架构分析 

一:前言 Tty这个名称源于电传打字节的简称。在linux表示各种终端。终端通常都跟硬件相对应。比如对应于输入设备键盘鼠标。输出设备显示器的控制终端和串口终端.也有对应于不存在设备的pty驱动。在如此众多的终端模型之中,linux是怎么将它们统一建模的呢?这就是我们今天要讨论的问题. 二:tty驱动概貌 Tty架构如下所示: 如上图所示,用户空间主要是通过设备文件同tty_core交互.tty_core根据用空间操作的类型再选择跟line discipline和tty_driver交互.例如设置硬件的ioctl指令就直接交给tty_driver处理。Read和write操作就会交给 line discipline处理. Line discipline是线路规程的意思。正如它的名字一样,它表示的是这条终端 线程 的输入与输出规范设置.主要用来进行输入/输出数据的预处理。处理之后。就会将数据交给tty_driver Tty_driver就是终端对应的驱动了。它将字符转换成终端可以理解的字串.将其传给终端设备。 值得注意的是,这个架构没有为tty_drivero提供read操作。也就是说tty_core  和line discipline都没有办法从tty_driver里直接读终端信息。这是因为tty_driver对就的hardware并不一定是输入数据和输出数据的共同负载者。例如控制终端,输出设备是显示器。输入设备是键盘。基于这样的原理。在line discipline中有一个输入缓存区。并提供了一个名叫receive_buf()的接口函数。对应的终端设备只要调用line discipine的receiver_buf函数,将数据写入到输入缓存区就可以了。 如果一个设备同时是输入设备又是输出设备。那在设备的中断处理中调用receive_buf()将数据写入即可.   三:tty驱动接口分析 具体的tty驱动设计可以参考LDD3。这里只对它的接口实现做一个分析.tty driver的所有操作都包含在tty_driver中。内核即供了一个名叫alloc_tty_driver()来分配这个tty_driver。当然我们也可以在自己的驱动中将它定义成一个静态的结构。对tty_driver进行一些必要的初始化之后,调用tty_register_driver() 将其注册. alloc_tty_driver()接口代码如下所示: struct tty_driver *alloc_tty_driver(int lines) {          struct tty_driver *driver;            driver = kzalloc(sizeof(struct tty_driver), GFP_KERNEL);          if (driver) {                    driver->magic = TTY_DRIVER_MAGIC;                    driver->num = lines;                    /* later we'll move allocation of tables here */          }          return driver; } 这个函数只有一个参数。这个参数的含义为line的个数。也即次设备号的个数。注意每个设备文件都会对应一个line. 在这个接口里为tty_driver分配内存,然后将driver->mage.driver->num初始化之后就返回了.   tty_register_driver()用来注册一个tty_driver。代码如下: int tty_register_driver(struct tty_driver *driver) {          int error;          int i;          dev_t dev;          void **p = NULL;            //TTY_DRIVER_INSTALLED:已安装的          if (driver->flags & TTY_DRIVER_INSTALLED)                    return 0;            //TTY_DRIVER_DEVPTS_MEM:使用devpts进行动态内存映射          if (!(driver->flags & TTY_DRIVER_DEVPTS_MEM) && driver->num) {                    p = kzalloc(driver->num * 3 * sizeof(void *), GFP_KERNEL);                    if (!p)                             return -ENOMEM;          }            //注册字符设备号          //如果没有指定driver->major          if (!driver->major) {                    error = alloc_chrdev_region(&dev, driver->minor_start,                                                         driver->num, driver->name);                    if (!error) {                             driver->major = MAJOR(dev);                             driver->minor_start = MINOR(dev);                    }          } else {                    dev = MKDEV(driver->major, driver->minor_start);                    error = register_chrdev_region(dev, driver->num, driver->name);          }          if (error < 0) {                    kfree(p);                   return error;          }            if (p) {                    driver->ttys = (struct tty_struct **)p;                    driver->termios = (struct ktermios **)(p + driver->num);                    driver->termios_locked = (struct ktermios **)                                                                  (p + driver->num * 2);          } else {                    driver->ttys = NULL;                    driver->termios = NULL;                    driver->termios_locked = NULL;          }            //注册字符设备          cdev_init(&driver->cdev, &tty_fops);          driver->cdev.owner = driver->owner;          error = cdev_add(&driver->cdev, dev, driver->num);          if (error) {                    unregister_chrdev_region(dev, driver->num);                    driver->ttys = NULL;                    driver->termios = driver->termios_locked = NULL;                    kfree(p);                    return error;          }            //指定默认的put_char          if (!driver->put_char)                    driver->put_char = tty_default_put_char;            mutex_lock(&tty_mutex);          list_add(&driver->tty_drivers, &tty_drivers);          mutex_unlock(&tty_mutex);            //如果没有指定TTY_DRIVER_DYNAMIC_DEV.即动态设备管理          if (!(driver->flags & TTY_DRIVER_DYNAMIC_DEV)) {                    for (i = 0; i < driver->num; i++)                        tty_register_device(driver, i, NULL);          }          proc_tty_register_driver(driver);          return 0; } 这个函数操作比较简单。就是为tty_driver创建字符设备。然后将字符设备的操作集指定为tty_fops.并且将tty_driver 挂载到tty_drivers链表中.其实这个链表的作用跟我们之前分析的input子系统中的input_dev[ ]数组类似。都是以设备号为关键字找到对应的driver. 特别的。如果没有定义TTY_DRIVER_DYNAMIC_DEV.还会在sysfs中创建一个类设备.这样主要是为了udev管理设备. 以流程图的方式将上述操作表示如下:     四:设备文件的操作 设备文件的操作是本节分析的重点。它的主要操作是将各项操作对应到ldsic或者是tty_driver.   4.1:打开tty设备的操作 从注册的过程可以看到,所有的操作都会对应到tty_fops中。Open操作对应的操作接口是tty_open()。代码如下: 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 = get_current_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;                    driver = 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);                    if (driver) {                             /* Don't let /dev/console block */                             filp->f_flags |= O_NONBLOCK;                             noctty = 1;                             goto got_driver;                    }                    mutex_unlock(&tty_mutex);                    return -ENODEV;          }            //以文件的设备号为关键字,到tty_drivers中搜索所注册的driver          driver = get_tty_driver(device, &index);          if (!driver) {                    mutex_unlock(&tty_mutex);                    return -ENODEV;          } got_driver:          //index表示它的次设备号          retval = init_dev(driver, index, &tty);          mutex_unlock(&tty_mutex);          if (retval)                    return retval;            filp->private_data = tty;          file_move(filp, &tty->tty_files);          check_tty_count(tty, "tty_open");          if (tty->driver->type == TTY_DRIVER_TYPE_PTY &&              tty->driver->subtype == PTY_TYPE_MASTER)                    noctty = 1; #ifdef TTY_DEBUG_HANGUP          printk(KERN_DEBUG "opening %s...", tty->name); #endif          if (!retval) {                    if (tty->driver->open)                             retval = tty->driver->open(tty, filp);                    else                             retval = -ENODEV;          }          filp->f_flags = saved_flags;            if (!retval && test_bit(TTY_EXCLUSIVE, &tty->flags) &&                                                         !capable(CAP_SYS_ADMIN))                    retval = -EBUSY;            if (retval) { #ifdef TTY_DEBUG_HANGUP                    printk(KERN_DEBUG "error %d in opening %s...", retval,                           tty->name); #endif                    release_dev(filp);                    if (retval != -ERESTARTSYS)                             return retval;                    if (signal_pending(current))                             return retval;                    schedule();                    /*                     * Need to reset f_op in case a hangup happened.                     */                    if (filp->f_op == &hung_up_tty_fops)                             filp->f_op = &tty_fops;                    goto retry_open;          }            mutex_lock(&tty_mutex);          spin_lock_irq(¤t->sighand->siglock);            //设置当前进程的终端          if (!noctty &&              current->signal->leader &&              !current->signal->tty &&              tty->session == NULL)                    __proc_set_tty(current, tty);          spin_unlock_irq(¤t->sighand->siglock);          mutex_unlock(&tty_mutex);          tty_audit_opening();          return 0; } 注意在这里有个容易忽略的操作:init_dev()。 Init_dev() -à initialize_tty_struct() à tty_ldisc_assign(tty, tty_ldisc_get(N_TTY)); 看一下tty_ldisc_assign(tty, tty_ldisc_get(N_TTY))的操作: Tty_ldisc_get(): struct tty_ldisc *tty_ldisc_get(int disc) {          unsigned long flags;          struct tty_ldisc *ld;            if (disc < N_TTY || disc >= NR_LDISCS)                    return NULL;            spin_lock_irqsave(&tty_ldisc_lock, flags);            ld = &tty_ldiscs[disc];          /* Check the entry is defined */          if (ld->flags & LDISC_FLAG_DEFINED) {                    /* If the module is being unloaded we can't use it */                    if (!try_module_get(ld->owner))                             ld = NULL;                    else /* lock it */                             ld->refcount++;          } else                    ld = NULL;          spin_unlock_irqrestore(&tty_ldisc_lock, flags);          return ld; } 这个函数的操作为到tty_ldiscs[ ]找到对应项.这个数组中的成员是调用tty_register_ldisc()将其设置进去的.   tty_ldisc_assign操作如下: static void tty_ldisc_assign(struct tty_struct *tty, struct tty_ldisc *ld) {          tty->ldisc = *ld;          tty->ldisc.refcount = 0; } 即将取出来的idisc作为tty->ldisc字段. 在这段代码中涉及到了tty_driver,tty_struct, struct tty_ldisc.这三者之间的关系用下图表示如下:   在这里,为tty_struct的ldisc是默认指定为tty_ldiscs[N_TTY].该ldisc对应的是控制终端的线路规范。可以在用空间用带TIOCSETD的ioctl调用进行更改. 将上述open用流程图的方式表示如下:   4.2:设备文件的write操作 设备文件的write操作对应tty_fops->write即tty_write().代码如下: static ssize_t tty_write(struct file *file, const char __user *buf,                                                         size_t count, loff_t *ppos) {          struct tty_struct *tty;          struct inode *inode = file->f_path.dentry->d_inode;          ssize_t ret;          struct tty_ldisc *ld;            tty = (struct tty_struct *)file->private_data;          if (tty_paranoia_check(tty, inode, "tty_write"))                    return -EIO;          if (!tty || !tty->driver->write ||                    (test_bit(TTY_IO_ERROR, &tty->flags)))                             return -EIO;            ld = tty_ldisc_ref_wait(tty);          if (!ld->write)                    ret = -EIO;          else                    ret = do_tty_write(ld->write, tty, file, buf, count);          tty_ldisc_deref(ld);          return ret; } 在open的过程中,将tty_struct存放在file的私有区。在write中,从file的私有区中就可以取到要操作的tty_struct. 如果tty_driver中没有write.如果tty有错误都会有效性判断失败返回。如果一切正常,递增ldsic的引用计数。将用do_tty_wirte()再行写操作。写完之后,再递减ldsic的引用计数. Do_tty_write代码分段分析如下: static inline ssize_t do_tty_write(          ssize_t (*write)(struct tty_struct *, struct file *, const unsigned char *, size_t),          struct tty_struct *tty,          struct file *file,          const char __user *buf,          size_t count) {          ssize_t ret, written = 0;          unsigned int chunk;            ret = tty_write_lock(tty, file->f_flags & O_NDELAY);          if (ret < 0)                    return ret;            /*           * We chunk up writes into a temporary buffer. This           * simplifies low-level drivers immensely, since they           * don't have locking issues and user mode accesses.           *           * But if TTY_NO_WRITE_SPLIT is set, we should use a           * big chunk-size..           *           * The default chunk-size is 2kB, because the NTTY           * layer has problems with bigger chunks. It will           * claim to be able to handle more characters than           * it actually does.           *           * FIXME: This can probably go away now except that 64K chunks           * are too likely to fail unless switched to vmalloc...           */          chunk = 2048;          if (test_bit(TTY_NO_WRITE_SPLIT, &tty->flags))                    chunk = 65536;          if (count < chunk)                    chunk = count;            /* write_buf/write_cnt is protected by the atomic_write_lock mutex */          if (tty->write_cnt < chunk) {                    unsigned char *buf;                      if (chunk < 1024)                             chunk = 1024;                      buf = kmalloc(chunk, GFP_KERNEL);                    if (!buf) {                             ret = -ENOMEM;                             goto out;                    }                    kfree(tty->write_buf);                    tty->write_cnt = chunk;                    tty->write_buf = buf;          } 默认一次写数据的大小为2K.如果设置了TTY_NO_WRITE_SPLIT.则将一次写的数据量扩大为65536. Tty->write_buf是写操作的临时缓存区。即将用户空的数据暂时存放到这里 Tty->write_cnt是临时缓存区的大小。 在这里,必须要根据一次写的数据量对这个临时缓存区做调整            /* Do the write .. */          for (;;) {                    size_t size = count;                    if (size > chunk)                             size = chunk;                    ret = -EFAULT;                    if (copy_from_user(tty->write_buf, buf, size))                             break;                    lock_kernel();                    ret = write(tty, file, tty->write_buf, size);                    unlock_kernel();                    if (ret <= 0)                             break;                    written += ret;                    buf += ret;                    count -= ret;                    if (!count)                             break;                    ret = -ERESTARTSYS;                    if (signal_pending(current))                             break;                    cond_resched();          }          if (written) {                    struct inode *inode = file->f_path.dentry->d_inode;                    inode->i_mtime = current_fs_time(inode->i_sb);                    ret = written;          } out:          tty_write_unlock(tty);          return ret; } 后面的操作就比较简单了。先将用户空间的数据copy到临时缓存区,然后再调用ldisc->write()完成这次写操作.最后再更新设备结点的时间戳. Write操作的流程图如下示:   在这里,我们只看到将数据写放到了ldisc->write().没有看到与tty_driver相关的部份。实际上在ldisc中对写入的数据做预处理过后,还是会调用tty_driver->write()将其写入硬件.   4.3:设备文件的read操作 static ssize_t tty_read(struct file *file, char __user *buf, size_t count,                             loff_t *ppos) {          int i;          struct tty_struct *tty;          struct inode *inode;          struct tty_ldisc *ld;            tty = (struct tty_struct *)file->private_data;          inode = file->f_path.dentry->d_inode;          if (tty_paranoia_check(tty, inode, "tty_read"))                    return -EIO;          if (!tty || (test_bit(TTY_IO_ERROR, &tty->flags)))                    return -EIO;            /* We want to wait for the line discipline to sort out in this             situation */          ld = tty_ldisc_ref_wait(tty);          lock_kernel();          if (ld->read)                    i = (ld->read)(tty, file, buf, count);          else                    i = -EIO;          tty_ldisc_deref(ld);          unlock_kernel();          if (i > 0)                    inode->i_atime = current_fs_time(inode->i_sb);          return i; } 这个read操作就更简单。直接调用ldsic->read()完成工作 流程图如下:   五:小结 在tty设备文件的操作中。Open操作会进行一系统初始化。然后调用ldsic->open tty_driver->open。在write和read调用中只tty_core只会用到 ldisc->wirte/ldisc->read.除了上面分析的几个操作之外,还有一个ioctl操作,以及它封装的几个 termios。这些ioctl类的操作会直接和tty_driver相关联. 在这一节里,只对tty的构造做一个分析,具体ldisc的操作我们之后以控制终端为例进行分析.

1、LINUX下TTY、CONSOLE、串口之间是怎样的层次关系?具体的函数接口是怎样的?串口是如何被调用的? 

2、printk函数是把信息发送到控制台上吧?如何让PRINTK把信息通过串口送出?或者说系统在什么地方来决定是将信息送到显示器还是串口? 


3、start_kernel中一开始就用到了printk函数(好象是printk(linux_banner什么的),在 这个时候整个内核还没跑起来呢那这时候的printk是如何被调用的?在我们的系统中,系统启动是用的现代公司的BOOTLOADER程序,后来好象跳到了LINUX下的head- armv.s, 然后跳到start_kernel,在bootloader 里串口已经是可用的了,那么在进入内核后是不是要重新设置? 


以上问题可能问的比较乱,因为我自己脑子里也比较乱,主要还是对tty,console,serial之间的关系,特别是串口是如何被调用的没搞清这方面的资料又比较少(就情景分析中讲了一点),希望高手能指点一二,非常谢! 我最近也在搞这方面的东西,也是写一个串口设备的驱动 
搞了将近一个月了,其中上网找资料,看源代码,什么都做了 
但还是一蹋糊涂的,有些问题还是不明白,希望一起讨论讨论 

在/proc/device(没记错应该是这个文件) 
里面有一个叫serial的驱动,其主设备号是4,次设备号是64-12X(没记错应该是这个范围) 
大家都知道,串口的次设备号是从64开始的,串口1 /dev/ttyS0就对应次设备号64,串口2就对应65 
问题是现在我机上只有两个串口,它注册这么多次设备号来干什么? 

对于一个接在串口1的设备,在我注册驱动的时候 
我是需要自己找一个主设备号呢? 
还是就用主设备号4,次设备号从上面12X的后面选? 
还是就用主设备号4,次设备号64? 

在linux的内核中有一个tty层,我看好像有些串口驱动是从这里开始的 
例如调用tty_register_driver()来注册驱动 
就像在pci子系统里调用pci_register_driver()那样的 
那么,用这种机制来注册的驱动, 
它是直接对串口的端口操作呢(例如用inb(),outb()....之类的) 
还是某些更底层的驱动接口呢? 

这些问题缠了我很久都没解决,搞得最后不得不放弃 
现在转向用户空间的应用程序,看能不能有些更高效的方法来实现 
(在用户空间只能用open("/dev/ttyS0", O_RDWR)来实现了) 另外还有,系统里已经为我们实现了串口的驱动 
所以我们在用户空间的程序里直接open("/dev/ttyS0")就可用了 
但是现在要写的是接在串口上的设备的驱动 
在内核模块中可不可以包含某个头文件,然后就可以直接用串口驱动中的接口呢? 看到你们的问题后,感觉很有典型性,因此花了点工夫看了一下,做了一些心得贴在这里,欢迎讨论并指正: 
1、LINUX下TTY、CONSOLE、串口之间是怎样的层次关系?具体的函数接口是怎样的?串口是如何被调用的? 
tty 和console这些概念主要是一些虚设备的概念,而串口更多的是指一个真正的设备驱动Tty实际是一类终端I/O设备的抽象,它实际上更多的是一个管理的概念,它和tty_ldisc(行规程)和tty_driver(真实设备驱动)组合在一起,目的是向上层的VFS提供一个统一的接口通过file_operations结构中的tty_ioctl可以对其进行配置查 tty_driver,你将得到n个结果,实际都是相关芯片的驱动因此,可以得到的结论是(实际情况比这复杂得多):每个描述tty设备的 tty_struct在初始化时必然挂如了某个具体芯片的字符设备驱动(不一定是字符设备驱动),可以是很多,包括显卡或串口chip不知道你的ARM Soc是那一款,不过看情况你们应该用的是常见的chip,这些驱动实际上都有而console是一个缓冲的概念,它的目的有一点类似于tty实际上 console不仅和tty连在一起,还和framebuffer连在一起,具体的原因看下面的键盘的中断处理过程Tty的一个子集需要使用 console(典型的如主设备号4,次设备号1―64),但是要注意的是没有console的tty是存在的
而串口则指的是tty_driver举个典型的例子: 
分析一下键盘的中断处理过程: 
keyboard_interrupt―>handle_kbd_event―>handle_keyboard_event―>handle_scancode 
void handle_scancode(unsigned char scancode, int down) 

…….. 
tty = ttytab? ttytab[fg_console]: NULL; 
if (tty && (!tty->driver_data)) { 
…………… 
tty = NULL; 

…………. 
schedule_console_callback(); 

这段代码中的两个地方很值得注意,也就是除了获得tty外(通过全局量tty记录),还进行了console 回显schedule_console_callbackTty和console的关系在此已经很明了!!! 

2、printk函数是把信息发送到控制台上吧?如何让PRINTK把信息通过串口送出?或者说系统在什么地方来决定是将信息送到显示器还是串口? 
具体看一下printk函数的实现就知道了,printk不一定是将信息往控制台上输出,设置kernel的启动参数可能可以打到将信息送到显示器的效果。函数前有一段英文,很有意思: 
/*This is printk. It can be called from any context. We want it to work. 

* We try to grab the console_sem. If we succeed, it's easy - we log the output and 
* call the console drivers. If we fail to get the semaphore we place the output 
* into the log buffer and return. The current holder of the console_sem will 
* notice the new output in release_console_sem() and will send it to the 
* consoles before releasing the semaphore. 

* One effect of this deferred printing is that code which calls printk() and 
* then changes console_loglevel may break. This is because console_loglevel 
* is inspected when the actual printing occurs. 
*/ 
这段英文的要点:要想对console进行操作,必须先要获得console_sem信号量如果获得console_sem信号量,则可以“log the output and call the console drivers”,反之,则“place the output into the log buffer and return”,实际上,在代码: 
asmlinkage int printk(const char *fmt, ...) 

va_list args; 
unsigned long flags; 
int printed_len; 
char *p; 
static char printk_buf[1024]; 
static int log_level_unknown = 1; 
if (oops_in_progress) { /*如果为1情况下,必然是系统发生crush*/ 
/* If a crash is occurring, make sure we can't deadlock */ 
spin_lock_init(&logbuf_lock); 
/* And make sure that we print immediately */ 
init_MUTEX(&console_sem); 

/* This stops the holder of console_sem just where we want him */ 
spin_lock_irqsave(&logbuf_lock, flags); 
/* Emit the output into the temporary buffer */ 
va_start(args, fmt); 
printed_len = vsnprintf(printk_buf, sizeof(printk_buf), fmt, args);/*对传入的buffer进行处理,注意还不是 
真正的对终端写,只是对传入的string进行格式解析*/ 
va_end(args); 
/*Copy the output into log_buf. If the caller didn't provide appropriate log level tags, we insert them here*/ 
/*注释很清楚*/ 
for (p = printk_buf; *p; p++) { 
if (log_level_unknown) { 
if (p[0] != '<' || p[1] < '0' || p[1] > '7' || p[2] != '>') { 
emit_log_char('<'); 
emit_log_char(default_message_loglevel + '0'); 
emit_log_char('>'); 

log_level_unknown = 0; 

emit_log_char(*p); 
if (*p == ' ') 
log_level_unknown = 1; 

if (!arch_consoles_callable()) { 
/*On some architectures, the consoles are not usable on secondary CPUs early in the boot process.*/ 
spin_unlock_irqrestore(&logbuf_lock, flags); 
goto out; 

if (!down_trylock(&console_sem)) { 
/*We own the drivers. We can drop the spinlock and let release_console_sem() print the text*/ 
spin_unlock_irqrestore(&logbuf_lock, flags); 
console_may_schedule = 0; 
release_console_sem(); 
} else { 
/*Someone else owns the drivers. We drop the spinlock, which allows the semaphore holder to 
proceed and to call the console drivers with the output which we just produced.*/ 
spin_unlock_irqrestore(&logbuf_lock, flags); 

out: 
return printed_len; 

实际上printk是将format后的string放到了一个buffer中,在适当的时候再加以show,这也回答了在start_kernel中一开始就用到了printk函数的原因 

3、start_kernel中一开始就用到了printk函数(好象是printk(linux_banner什么的),在这个时候整个内核还没跑起来呢那这时候的printk是如何被调用的?在我们的系统中,系统启动是用的现代公司的BOOTLOADER程序,后来好象跳到了LINUX下的head- armv.s, 然后跳到start_kernel,在bootloader 里串口已经是可用的了,那么在进入内核后是不是要重新设置? 
Bootloader一般会做一些基本的初始化,将kernel拷贝物理空间,然后再跳到kernel去执行可以肯定的是kernel肯定要对串口进行重新设置,原因是Bootloader有很多种,有些不一定对串口进行设置,内核不能依赖于bootloader而存在。
  多谢楼上大侠,分析的很精辟我正在看printk函数

我们用的CPU是hynix的hms7202在评估板上是用串口0作 
控制台,所有启动过程中的信息都是通过该串口送出的。 
在bootloader中定义了函数ser_printf通过串口进行交互

但我还是没想明白在跳转到linux内核而console和串口尚未 
初始化时printk是如何能够工作的?我看了start_kernel 
的过程(并通过超级终端作了一些跟踪),console的初始化 
是在console_init函数里,而串口的初始化实际上是在1号 
进程里(init->do_basic_setup->do_initcalls->rs_init), 
那么在串口没有初始化以前prink是如何工作的?特别的,在 
start_kernel一开始就有printk(linux_banner),而这时候 
串口和console都尚未初始化呢   1.在start_kernel一开始就有printk(linux_banner),而这时候串口和console都尚未初始化? 
仔细分析printk可以对该问题进行解答代码中的: 
/* Emit the output into the temporary buffer */ 
va_start(args, fmt); 
printed_len = vsnprintf(printk_buf, sizeof(printk_buf), fmt, args); 
va_end(args); 
将输入放到了printk_buf中,接下来的 
for (p = printk_buf; *p; p++) { 
if (log_level_unknown) { 
if (p[0] != '<' || p[1] < '0' || p[1] > '7' || p[2] != '>') { 
emit_log_char('<'); 
emit_log_char(default_message_loglevel + '0'); 
emit_log_char('>'); 

log_level_unknown = 0; 

emit_log_char(*p); 
if (*p == ' ') 
log_level_unknown = 1; 

则将printk_buf中的内容进行解析并放到全局的log_buf(在emit_log_char函数)中if (!down_trylock(&console_sem)) { 
/* 
* We own the drivers. We can drop the spinlock and let 
* release_console_sem() print the text 
*/ 
spin_unlock_irqrestore(&logbuf_lock, flags); 
console_may_schedule = 0; 
release_console_sem(); 
} else { 
/* 
* Someone else owns the drivers. We drop the spinlock, which 
* allows the semaphore holder to proceed and to call the 
* console drivers with the output which we just produced. 
*/ 
spin_unlock_irqrestore(&logbuf_lock, flags); 

则是根据down_trylock(&console_sem)的结果调用release_console_sem(),在release_console_sem()中才真正的对全局的log_buf中的内容相应的console设备驱动进行处理至此,可以得到如下的一些结论: 
(1)printk的主操作实际上还是针对一个buffer(log_buf),该buffer中的内容是否显示(或者说向终端输出),则要看是否可以获得console_sem(2)printk所在的文件为printk.c,是和体系结构无关的,因此对任何平台都一样。 可以推测的结论是: 
(1)kernel 在初始化时将console_sem标为了locked,因此在start_kernel一开始的printk(linux_banner)中实际只将输入写入了缓冲,等在串口和console初始化后,对printk的调用才一次将缓冲中的内容向串口和console输出。 (2)在串口和console的初始化过程中,必然有对console_sem的up操作。 
(3)因此,在embedded的调试中,如果在console的初始化之前系统出了问题,不会有任何的输出。 唯一可以使用的只能是led或jtag了(4)因此,你的问题可以看出解答2.console的初始化. 
不知道你用的是那一个内核版本,在我看的2.4.18和2.4.19中,都是在start_kernel中就对console进行的初始化从前面的分析来看,console的初始化不应该太晚,否则log_buf有可能溢出 多谢楼上,分析的很精彩! 

我们用的内核版本是2.4.18,console的初始化确实是在 
start_kernel->console->init关于tty和串口,我这里还想再问一下tty设备的操作的总入口 
是 

static struct file_operations tty_fops = { 
llseek: no_llseek, 
read: tty_read, 
write: tty_write, 
poll: tty_poll, 
ioctl: tty_ioctl, 
open: tty_open, 
release: tty_release, 
fasync: tty_fasync, 
}; 

而对串口的操作定义在: 

static struct tty_driver serial_driver 这个结构中
serial.c中的多数函数都是填充serial_driver中的函数指针
那么在对串口操作时,应该是先调用tty_fops中的操作(比如 
tty_open等),然后再分流到具体的串口操作(rs_open等)吧? 
但tty_driver(对串口就是serial_driver)中有很多函数指针 
并不跟file_operations中的函数指针对应,不知道这些对应 
不上的操作是如何被执行的?比如put_char,flush_char,read_proc, 
write_proc,start,stop等 以下是我对这个问题的一些理解: 
这实际上还是回到原先的老问题,即tty和tty_driver之间的关系从实现上看,tty_driver实际上是tty机制的实现组件之一,借用面向对象设计中的常用例子,这时的tty_driver就象是tty这部汽车的轮胎,tty这部汽车要正常运行,还要tty_ldisc(行规程),termios,甚至struct tq_struct tq_hangup(看tty_struct)等基础设施它们之间的关系并非继承至于tty_driver中的函数指针,再打个C++中的比喻,它们实际上很象虚函数,也就是说,可以定义它们,但并不一定实现它们实际上还不用说tty_driver,只要查一下serial_driver都会发现n多个具体的实现,但对各个具体的设备,其tty_driver中的函数不一定全部实现所以put_char,flush_char,read_proc, write_proc,start,stop这些函数的情况是有可能实现,也有可能不实现 即使被实现,也不一定为上层(VFS层)所用.