linux kernel下輸入輸出console怎樣實現

近期工做在調試usb虛擬串口,讓其做爲kernel啓動的調試串口,以及user空間的輸入輸出控制檯。node

利用這個機會,學習下printk怎樣選擇往哪一個console輸出以及user空間下控制檯怎樣選擇。記錄與此。與你們共享,也方便本身之後翻閱。數組

Kernel版本號號:3.4.55async

按照個人思路(仍是時間順序)分了4部分,指定kernel調試console ,  kernel下printk console的選擇 ,kernel下console的註冊。user空間console的選擇。函數


一 指定kernel調試console學習

首先看kernel啓動時怎樣獲取和處理指定的console參數。ui

kernel的啓動參數cmdline可以指定調試console。如指定‘console=ttyS0,115200’,this

kernel怎樣解析cmdline。我以前寫了一篇博文例如如下:spa

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

依據以前的分析,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=\n");
            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\n", 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\n", newcon->name, newcon->index); for_each_console(bcon) if (bcon->flags & CON_BOOT) unregister_console(bcon); } else { printk(KERN_INFO "%sconsole [%s%d] enabled\n", (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\n",
            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.\n");

    (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\n");
    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\n");
    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。輸出的‘\n‘進過線路規程會變爲’\n\r‘。


因此對於kernel下console的write方法。不要忘記,對於log buf中'\n'的處理。實現一個簡單的線路規程!

相關文章
相關標籤/搜索