• linux kernel下输入输出console怎样实现


    近期工作在调试usb虚拟串口,让其作为kernel启动的调试串口,以及user空间的输入输出控制台。

    利用这个机会,学习下printk怎样选择往哪个console输出以及user空间下控制台怎样选择。记录与此。与大家共享,也方便自己以后翻阅。

    Kernel版本号号:3.4.55

    按照我的思路(还是时间顺序)分了4部分,指定kernel调试console ,  kernel下printk console的选择 ,kernel下console的注冊。user空间console的选择。


    一 指定kernel调试console

    首先看kernel启动时怎样获取和处理指定的console參数。

    kernel的启动參数cmdline能够指定调试console。如指定‘console=ttyS0,115200’,

    kernel怎样解析cmdline。我之前写了一篇博文例如以下:

    http://blog.csdn.net/skyflying2012/article/details/41142801

    依据之前的分析,cmdline中有console=xxx。start_kernel中parse_args遍历.init.setup段全部obs_kernel_param。

    kernel/printk.c中注冊了‘console=’的解析函数console_setup(注冊了obs_kernel_param),所以匹配成功,会调用console_setup来解析。例如以下:

    static int __init console_setup(char *str)
    {
        char buf[sizeof(console_cmdline[0].name) + 4]; /* 4 for index */
        char *s, *options, *brl_options = NULL;
        int idx; 
    
    #ifdef CONFIG_A11Y_BRAILLE_CONSOLE
        if (!memcmp(str, "brl,", 4)) {
            brl_options = "";
            str += 4;
        } else if (!memcmp(str, "brl=", 4)) {
            brl_options = str + 4; 
            str = strchr(brl_options, ',');
            if (!str) {
                printk(KERN_ERR "need port name after brl=
    ");
                return 1;
            }    
            *(str++) = 0; 
        }    
    #endif
    
        /*
         * Decode str into name, index, options.
         */
        if (str[0] >= '0' && str[0] <= '9') {
            strcpy(buf, "ttyS");
            strncpy(buf + 4, str, sizeof(buf) - 5);
        } else {
            strncpy(buf, str, sizeof(buf) - 1);
        }
        buf[sizeof(buf) - 1] = 0;
        if ((options = strchr(str, ',')) != NULL)
            *(options++) = 0;
    #ifdef __sparc__
        if (!strcmp(str, "ttya"))
            strcpy(buf, "ttyS0");
        if (!strcmp(str, "ttyb"))
            strcpy(buf, "ttyS1");
    #endif
        for (s = buf; *s; s++)
            if ((*s >= '0' && *s <= '9') || *s == ',')
                break;
        idx = simple_strtoul(s, NULL, 10);
        *s = 0;
    
        __add_preferred_console(buf, idx, options, brl_options);
        console_set_on_cmdline = 1;
        return 1;
    }
    __setup("console=", console_setup);

    參数是console=的值字符串,如“ttyS0,115200”,console_setup对console=參数值做解析,以ttyS0,115200为例,最后buf=“ttyS”,idx=0,options="115200",brl_options=NULL。调用__add_preferred_console例如以下:

    /*
     * If exclusive_console is non-NULL then only this console is to be printed to.
     */
    static struct console *exclusive_console;
    
    /*
     *  Array of consoles built from command line options (console=)
     */
    struct console_cmdline
    {                  
        char    name[8];            /* Name of the driver       */
        int index;              /* Minor dev. to use        */
        char    *options;           /* Options for the driver   */
    #ifdef CONFIG_A11Y_BRAILLE_CONSOLE
        char    *brl_options;           /* Options for braille driver */
    #endif
    };
    
    #define MAX_CMDLINECONSOLES 8
            
    static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];
    static int selected_console = -1;
    static int preferred_console = -1;
    int console_set_on_cmdline;
    EXPORT_SYMBOL(console_set_on_cmdline);
    static int __add_preferred_console(char *name, int idx, char *options,
                       char *brl_options)
    {
        struct console_cmdline *c;
        int i;
    
        /*
         *  See if this tty is not yet registered, and
         *  if we have a slot free.
         */
        for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++)
            if (strcmp(console_cmdline[i].name, name) == 0 &&
                  console_cmdline[i].index == idx) {
                    if (!brl_options)
                        selected_console = i;
                    return 0;
            }
        if (i == MAX_CMDLINECONSOLES)
            return -E2BIG;
        if (!brl_options)
            selected_console = i;
        c = &console_cmdline[i];
        strlcpy(c->name, name, sizeof(c->name));
        c->options = options;
    #ifdef CONFIG_A11Y_BRAILLE_CONSOLE
        c->brl_options = brl_options;
    #endif
        c->index = idx;
        return 0;
    }

    kernel利用结构体数组console_cmdline[8],最多可支持8个cmdline传入的console參数。

    __add_preferred_console将name idx options保存到数组下一个成员console_cmdline结构体中,假设数组中已有重名。则不加入。并置selected_console为最新加入的console_cmdline的下标号。

    比方cmdline中有“console=ttyS0,115200 console=ttyS1,9600”

    则在console_cmdline[8]数组中console_cmdline[0]代表ttyS0,console_cmdline[1]代表ttyS1,而selected_console=1.

    二 kernel下printk console的选择

    kernel下调试信息是通过printk输出,假设要kernel正常打印,则须要搞明确printk怎么选择输出的设备。

    关于printk的实现原理,我在刚工作的时候写过一篇博文,kernel版本号是2.6.21的,可是原理还是一致的。可供參考:

    http://blog.csdn.net/skyflying2012/article/details/7970341

    printk首先将输出内容加入到一个kernel缓冲区中。叫log_buf,log_buf相关代码例如以下:

    #define MAX_CMDLINECONSOLES 8
    
    static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];
    static int selected_console = -1;
    static int preferred_console = -1;
    int console_set_on_cmdline;
    EXPORT_SYMBOL(console_set_on_cmdline);
    
    /* Flag: console code may call schedule() */
    static int console_may_schedule;
    
    #ifdef CONFIG_PRINTK
            
    static char __log_buf[__LOG_BUF_LEN];
    static char *log_buf = __log_buf;
    static int log_buf_len = __LOG_BUF_LEN;
    static unsigned logged_chars; /* Number of chars produced since last read+clear operation */
    static int saved_console_loglevel = -1;
    log_buf的大小由kernel menuconfig配置,我配置的CONFIG_LOG_BUF_SHIFT为17。则log_buf为128k。

    printk内容会一直存在log_buf中。log_buf满了之后则会从头在開始存,覆盖掉原来的数据。

    依据printk的实现原理,printk最后调用console_unlock实现log_buf数据刷出到指定设备。

    这里先不关心printk怎样处理log buf数据(比方加入内容级别)。仅仅关心printk怎样一步步找到指定的输出设备,依据printk.c代码,能够找到例如以下线索。

    printk->vprintk->console_unlock->call_console_drivers->_call_console_drivers->_call_console_drivers->__call_console_drivers

    看线索最底层__call_console_drivers代码。例如以下:

    /*
     * Call the console drivers on a range of log_buf
     */
    static void __call_console_drivers(unsigned start, unsigned end)
    {
        struct console *con;
    
        for_each_console(con) {
            if (exclusive_console && con != exclusive_console)
                continue;
            if ((con->flags & CON_ENABLED) && con->write &&
                    (cpu_online(smp_processor_id()) ||
                    (con->flags & CON_ANYTIME)))
                con->write(con, &LOG_BUF(start), end - start);
        }
    }
    
    for_each_console定义例如以下:

    /*          
     * for_each_console() allows you to iterate on each console
     */             
    #define for_each_console(con) 
        for (con = console_drivers; con != NULL; con = con->next)
    

    遍历console_drivers链表全部console struct,假设有exclusive_console。则调用与exclusive_console一致console的write,

    假设exclusive_console为NULL。则调用全部ENABLE的console的write方法将log buf中start到end的内容发出。

    能够看出。execlusive_console来指定printk输出唯一console。假设未指定。则向全部enable的console写。

    默认情况下execlusive_console=NULL,所以printk默认是向全部enable的console写!

    仅仅有一种情况是指定execlusive_console。就是在console注冊时。以下会讲到。

    到这里就非常明了了,kernel下每次printk打印,首先存log_buf,然后遍历console_drivers。找到合适console(execlusive_console或全部enable的)。刷出log。

    console_drivers链表的成员是哪里来的,谁会指定execulsive_console?接着来看下一部分。kernel下console的注冊


    三 kernel下console的注冊

    上面分析能够看出,作为kernel移植最主要的一步,kernel下printk正常输出,最重要的一点是在console_drivers链表中加入console struct。那谁来完毕这个工作?

    答案是register_console函数,在printk.c中。以下来分析下该函数。

    void register_console(struct console *newcon)
    {
        int i;
        unsigned long flags;
        struct console *bcon = NULL;
    
        //假设注冊的是bootconsole(kernel早期启动打印),须要检查console_drivers中
        //没有“real console”也就是说bootconsole必须是第一个注冊的console。

    if (console_drivers && newcon->flags & CON_BOOT) { /* find the last or real console */ for_each_console(bcon) { if (!(bcon->flags & CON_BOOT)) { printk(KERN_INFO "Too late to register bootconsole %s%d ", newcon->name, newcon->index); return; } } } if (console_drivers && console_drivers->flags & CON_BOOT) bcon = console_drivers; //preferred console为console_cmdline中最后一个console if (preferred_console < 0 || bcon || !console_drivers) preferred_console = selected_console; if (newcon->early_setup) newcon->early_setup(); if (preferred_console < 0) { if (newcon->index < 0) newcon->index = 0; if (newcon->setup == NULL || newcon->setup(newcon, NULL) == 0) { newcon->flags |= CON_ENABLED; if (newcon->device) { newcon->flags |= CON_CONSDEV; preferred_console = 0; } } } //检查newcon是否是cmdline指定的console,假设是,则使能(CON_ENABLE)并初始化该console for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++) { if (strcmp(console_cmdline[i].name, newcon->name) != 0) continue; if (newcon->index >= 0 && newcon->index != console_cmdline[i].index) continue; if (newcon->index < 0) newcon->index = console_cmdline[i].index; #ifdef CONFIG_A11Y_BRAILLE_CONSOLE if (console_cmdline[i].brl_options) { newcon->flags |= CON_BRL; braille_register_console(newcon, console_cmdline[i].index, console_cmdline[i].options, console_cmdline[i].brl_options); return; } #endif if (newcon->setup && newcon->setup(newcon, console_cmdline[i].options) != 0) break; newcon->flags |= CON_ENABLED; newcon->index = console_cmdline[i].index; if (i == selected_console) { //假设newcon是cmdline指定的最新的console。则置位CONSDEV newcon->flags |= CON_CONSDEV; preferred_console = selected_console; } break; } //该console没有使能,退出 if (!(newcon->flags & CON_ENABLED)) return; //假设有bootconsole,则newcon不须要输出register之前的log,由于假设bootconsole和newcon是同一个设备 //则之前的log就输出2次 if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV)) newcon->flags &= ~CON_PRINTBUFFER; //把newcon加入console_drivers链表,对于置位CON_CONSDEV的con,放在链表首 console_lock(); if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) { newcon->next = console_drivers; console_drivers = newcon; if (newcon->next) newcon->next->flags &= ~CON_CONSDEV; } else { newcon->next = console_drivers->next; console_drivers->next = newcon; } if (newcon->flags & CON_PRINTBUFFER) { //假设newcon置位PRINTBUFFER,则将log全部刷出 raw_spin_lock_irqsave(&logbuf_lock, flags); con_start = log_start; raw_spin_unlock_irqrestore(&logbuf_lock, flags); //改动printk输出的指定唯一exclusive_console为newcon //保证将之前的log仅仅输出到newcon exclusive_console = newcon; } //解锁console,刷出log到newcon console_unlock(); console_sysfs_notify(); //假设有bootconsole,则unregister bootconsole(从console_drivers中删掉) //并告诉使用者如今console切换 if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV) && !keep_bootcon) { /* we need to iterate through twice, to make sure we print * everything out, before we unregister the console(s) */ printk(KERN_INFO "console [%s%d] enabled, bootconsole disabled ", newcon->name, newcon->index); for_each_console(bcon) if (bcon->flags & CON_BOOT) unregister_console(bcon); } else { printk(KERN_INFO "%sconsole [%s%d] enabled ", (newcon->flags & CON_BOOT) ?

    "boot" : "" , newcon->name, newcon->index); } }

    假设之前注冊了bootconsole,则不会将该次register之前的log刷出。防止bootconsole和该次注冊的newcon是同一个物理设备时,log打印2次。

    假设没有bootconsole。则会指定exclusive_console=newcon,console_unlock时。刷新全部log到该指定exclusive console。

    console_unlock结束时会将exclusive_console置NULL,所以exclusive console默认情况下就是NULL。

    最后会unregister bootconsole,是将bootconsole从console_drivers中删除,这样之后的printk就不会想bootconsole输出了。

    有意思的一个地方是,在unregister bootconsole之前的printk:

    printk(KERN_INFO "console [%s%d] enabled, bootconsole disabled
    ",
                newcon->name, newcon->index);
    由于此时bootconsole还没删掉,而newconsole已经加入console_drivers,假设bootconsole和newconsole是同一个物理设备,我们会看到这句printk会出现2次哦!

    假设在cmdline指定2个I/O设备,如“console==ttyS0,115200 console=ttyS1,115200”,因ttyS设备都是serial driver中注冊的real console,所以会看到kernel的打印分别出如今2个串口上!

    boot console和real console区别在于bootconsole注冊于kernel启动早期,方便对于kernel早期启动进行调试打印。

    那这些console是在哪里调用register_console进行注冊的?

    bootconsole的注冊,如arch/arm/kernel/early_printk.c,是在parse_args參数解析阶段注冊bootconsole。

    在start_kernel中console_init函数也会遍历.con_initcall.init段中全部注冊函数,而这些注冊函数也能够来注冊bootconsole。

    .con_initcall.init段中函数的注冊能够使用宏定义console_initcall。这些函数中调用register_console,方便在kernel初期实现printk打印。

    realconsole的注冊,是在各个driver,如serial载入时完毕。

    经过上面分析,对于一个新实现的输入输出设备,假设要将其作为kernel下的printk调试输出设备,须要2步:

    (1)register console。console struct例如以下:

    struct console {
        char    name[16];
        void    (*write)(struct console *, const char *, unsigned);
        int (*read)(struct console *, char *, unsigned);
        struct tty_driver *(*device)(struct console *, int *); 
        void    (*unblank)(void);
        int (*setup)(struct console *, char *); 
        int (*early_setup)(void);
        short   flags;
        short   index;
        int cflag;
        void    *data;
        struct   console *next;
    };
    
    
    定义一个console,由于kernel调试信息是单向的,没有交互。所以仅仅须要实现write就可以,还须要实现setup函数,进行设备初始化(如设置波特率等),以及标志位flags(将全部log刷出),举个样例,例如以下:

    static struct console u_console =
    {
        .name       = "ttyS",
        .write      = u_console_write,
        .setup      = u_console_setup,
        .flags      = CON_PRINTBUFFER,
        .index      = 0,
        .data       = &u_reg,
    };static int __init
    u_console_init(void)
    {
        register_console(&u_console);
        return 0;
    }

    为了调试方便。能够在console_init调用该函数进行注冊,则须要

    console_initcall(u_console_init);
    也能够在kernel载入driver时调用,则须要在driver的probe时调用u_console_init。可是这样仅仅能等driver调register_console之后,console_unlock才将全部log刷出。之前的log都会存在log buf中。

    (2)cmdline指定调试console。在kernel的cmdline加入參数console=ttyS0,115200



    四 user空间console的选择

    用户空间的输入输出依赖于其控制台使用的哪个。这里有非常多名词,如控制台,tty,console等,这些名字我也非常晕。不用管他们的真正含义。搞嵌入式。直接找到它的实现,搞明确从最上层软件,到最底层硬件,怎样操作,还有什么会不清楚呢。

    在start_kernel中最后起内核init进程时,例如以下:

    /* Open the /dev/console on the rootfs, this should never fail */
        if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
            printk(KERN_WARNING "Warning: unable to open an initial console.
    ");
    
        (void) sys_dup(0);
        (void) sys_dup(0);
    
    
    去打开console设备,console设备做了控制台。

    console设备文件的创建在driver/tty/tty_io.c中。例如以下:

    static const struct file_operations console_fops = {
        .llseek     = no_llseek,
        .read       = tty_read,
        .write      = redirected_tty_write,
        .poll       = tty_poll,
        .unlocked_ioctl = tty_ioctl,
        .compat_ioctl   = tty_compat_ioctl,
        .open       = tty_open,
        .release    = tty_release,
        .fasync     = tty_fasync,
    };
    int __init tty_init(void)
    {
        cdev_init(&tty_cdev, &tty_fops);
        if (cdev_add(&tty_cdev, MKDEV(TTYAUX_MAJOR, 0), 1) ||
            register_chrdev_region(MKDEV(TTYAUX_MAJOR, 0), 1, "/dev/tty") < 0)
            panic("Couldn't register /dev/tty driver
    ");
        device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 0), NULL, "tty");
    
        cdev_init(&console_cdev, &console_fops);
        if (cdev_add(&console_cdev, MKDEV(TTYAUX_MAJOR, 1), 1) ||
            register_chrdev_region(MKDEV(TTYAUX_MAJOR, 1), 1, "/dev/console") < 0)
            panic("Couldn't register /dev/console driver
    ");
        consdev = device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 1), NULL,
                      "console");
        if (IS_ERR(consdev))
            consdev = NULL;
        else
            WARN_ON(device_create_file(consdev, &dev_attr_active) < 0);
        
    #ifdef CONFIG_VT
        vty_init(&console_fops);
    #endif
        return 0;
    }

    console的操作函数都是使用的tty的操作函数。看open的实现。怎样找到详细的操作设备:

    static int tty_open(struct inode *inode, struct file *filp)
    {
        struct tty_struct *tty;
        int noctty, retval;
        struct tty_driver *driver = NULL;
        int index;
        dev_t device = inode->i_rdev;
        unsigned saved_flags = filp->f_flags;
        
        nonseekable_open(inode, filp);
    
    retry_open:
        retval = tty_alloc_file(filp);
        if (retval) 
            return -ENOMEM;
        
        noctty = filp->f_flags & O_NOCTTY;
        index  = -1;
        retval = 0;
    
        mutex_lock(&tty_mutex);
        tty_lock();
    
        tty = tty_open_current_tty(device, filp);
        if (IS_ERR(tty)) {
            retval = PTR_ERR(tty);
            goto err_unlock;
        } else if (!tty) {
            driver = tty_lookup_driver(device, filp, &noctty, &index);
            if (IS_ERR(driver)) {
                retval = PTR_ERR(driver);
                goto err_unlock;
            }  /* check whether we're reopening an existing tty */
            tty = tty_driver_lookup_tty(driver, inode, index);
            if (IS_ERR(tty)) {
                retval = PTR_ERR(tty);
                goto err_unlock;
            }
        }
    }

    首先tty_open_current_tty找该进程所相应的tty,由于init进程我们并没有制定tty,所以该函数返回NULL。

    接下来调用tty_lookup_driver,例如以下:

    static struct tty_driver *tty_lookup_driver(dev_t device, struct file *filp,
            int *noctty, int *index)
    {
        struct tty_driver *driver;
    
        switch (device) {
    #ifdef CONFIG_VT
        case MKDEV(TTY_MAJOR, 0): {
            extern struct tty_driver *console_driver;
            driver = tty_driver_kref_get(console_driver);
            *index = fg_console;
            *noctty = 1;
            break;
        }
    #endif
        case MKDEV(TTYAUX_MAJOR, 1): {
            struct tty_driver *console_driver = console_device(index);
            if (console_driver) {
                driver = tty_driver_kref_get(console_driver);
                if (driver) {
                    /* Don't let /dev/console block */
                    filp->f_flags |= O_NONBLOCK;
                    *noctty = 1;
                    break;
                }
            }
            return ERR_PTR(-ENODEV);
        }
        default:
            driver = get_tty_driver(device, index);
            if (!driver)
                return ERR_PTR(-ENODEV);
            break;
        }
        return driver;
    }
    
    
    console设备文件。次设备号是1,依据代码,会调用console_device来获取相应的tty_driver。例如以下:

    struct tty_driver *console_device(int *index)
    {       
        struct console *c;
        struct tty_driver *driver = NULL;
    
        console_lock(); 
        for_each_console(c) {
            if (!c->device)
                continue; 
            driver = c->device(c, index);
            if (driver)
                break;
        }   
        console_unlock();
        return driver;
    }
    
    又遇到了熟悉的for_each_console。遍历console_drivers链表。对于存在device成员的console,调用device方法。获取tty_driver,退出遍历。

    之后对于该console设备的读写操作都是基于该tty_driver。

    全部的输入输出设备都会注冊tty_driver。

    所以,对于一个新实现的输入输出设备,假设想让其即作为kernel的printk输出设备。也作为user空间的控制台。则须要在上面u_console基础上再实现device方法成员,来返回该设备的tty_driver。


    那么另一个问题:

    假设cmdline指定2个I/O设备。“console=ttyS0,115200 console=ttyS1,115200”,user空间选择哪个作为console?

    用户空间console open时,console_device遍历console_drivers,找到有device成员的console。获取tty_driver,就会退出遍历。

    所以哪个console放在console_drivers前面。就会被选择为user空间的console。

    在分析register_console时,假设要注冊的newcon是cmdline指定的最新的console(i = selected_console),则置位CON_CONSDEV。

    而在后面newcon加入console_drivers时,推断该置位。置位CON_CONSDEV,则将newcon加入到console_drivers的链表头,否则插入到后面。

    所以这里user空间会选择ttyS1作为用户控件的console!


    总结下,kernel和user空间下都有一个console,关系到kernel下printk的方向和user下printf的方向,实现区别还是非常大的。

    kernel下的console是输入输出设备driver中实现的简单的输出console,仅仅实现write函数,而且是直接输出到设备。


    user空间下的console,实际就是tty的一个样例。全部操作函数都继承与tty。全功能,能够打开 读写 关闭。所以对于console的读写。都是由kernel的tty层来终于发送到设备。

    kernel的tty层之下还有ldisc线路规程层,线路规程层之下才是详细设备的driver。

    ldisc层处理一些对于控制台来说有意义的输入输出字符,比方输入的crtl+C。输出的‘ ‘进过线路规程会变为’ ‘。


    所以对于kernel下console的write方法。不要忘记,对于log buf中' '的处理。实现一个简单的线路规程!









  • 相关阅读:
    BI之SSAS完整实战教程1 -- 开篇, BI简介 & SSAS简介
    MVC 5 + EF6 入门完整教程14 -- 动态生成面包屑导航
    MVC5+EF6 入门完整教程12--灵活控制Action权限
    JVM垃圾收集
    JVM类加载
    前端UI框架
    前端学习
    JVM字节码
    网络编程
    Java NIO
  • 原文地址:https://www.cnblogs.com/brucemengbm/p/6707111.html
Copyright © 2020-2023  润新知