http://blog.csdn.net/skyflying2012/article/details/41078349
2012
最近工作在調(diào)試usb虛擬串口,讓其作為kernel啟動的調(diào)試串口,以及user空間的輸入輸出控制臺。
利用這個機會,學(xué)習(xí)下printk如何選擇往哪個console輸出以及user空間下控制臺如何選擇,記錄與此,與大家共享,也方便自己以后翻閱。
Kernel版本號:3.4.55
依照我的思路(還是時間順序)分了4部分,指定kernel調(diào)試console , kernel下printk console的選擇 ,kernel下console的注冊,user空間console的選擇。
一 指定kernel調(diào)試console
首先看kernel啟動時如何獲取和處理指定的console參數(shù)。
kernel的啟動參數(shù)cmdline可以指定調(diào)試console,如指定‘console=ttyS0,115200’,
kernel如何解析cmdline,我之前寫了一篇博文如下:
http://blog.csdn.net/skyflying2012/article/details/41142801
根據(jù)之前的分析,cmdline中有console=xxx,start_kernel中parse_args遍歷.init.setup段所有obs_kernel_param。
kernel/printk.c中注冊了‘console=’的解析函數(shù)console_setup(注冊了obs_kernel_param),所以匹配成功,會調(diào)用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);
參數(shù)是console=的值字符串,如“ttyS0,115200”,console_setup對console=參數(shù)值做解析,以ttyS0,115200為例,最后buf=“ttyS”,idx=0,options="115200",brl_options=NULL。調(diào)用__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利用結(jié)構(gòu)體數(shù)組console_cmdline[8],最多可支持8個cmdline傳入的console參數(shù)。
__add_preferred_console將name idx options保存到數(shù)組下一個成員console_cmdline結(jié)構(gòu)體中,如果數(shù)組中已有重名,則不添加,并置selected_console為最新添加的console_cmdline的下標(biāo)號。
比如cmdline中有“console=ttyS0,115200 console=ttyS1,9600”
則在console_cmdline[8]數(shù)組中console_cmdline[0]代表ttyS0,console_cmdline[1]代表ttyS1,而selected_console=1.
二 kernel下printk console的選擇
kernel下調(diào)試信息是通過printk輸出,如果要kernel正常打印,則需要搞明白printk怎么選擇輸出的設(shè)備。
關(guān)于printk的實現(xiàn)原理,我在剛工作的時候?qū)戇^一篇博文,kernel版本是2.6.21的,但是原理還是一致的,可供參考:
http://blog.csdn.net/skyflying2012/article/details/7970341
printk首先將輸出內(nèi)容添加到一個kernel緩沖區(qū)中,叫l(wèi)og_buf,log_buf相關(guān)代碼如下:
- #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內(nèi)容會一直存在log_buf中,log_buf滿了之后則會從頭在開始存,覆蓋掉原來的數(shù)據(jù)。
根據(jù)printk的實現(xiàn)原理,printk最后調(diào)用console_unlock實現(xiàn)log_buf數(shù)據(jù)刷出到指定設(shè)備。
這里先不關(guān)心printk如何處理log buf數(shù)據(jù)(比如添加內(nèi)容級別),只關(guān)心printk如何一步步找到指定的輸出設(shè)備,根據(jù)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,則調(diào)用與exclusive_console一致console的write,
如果exclusive_console為NULL,則調(diào)用所有ENABLE的console的write方法將log buf中start到end的內(nèi)容發(fā)出。
可以看出,execlusive_console來指定printk輸出唯一console,如果未指定,則向所有enable的console寫。
默認(rèn)情況下execlusive_console=NULL,所以printk默認(rèn)是向所有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函數(shù),在printk.c中,下面來分析下該函數(shù)。
- 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是同一個設(shè)備
- //則之前的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中刪掉)
- //并告訴使用者現(xiàn)在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是同一個物理設(shè)備時,log打印2次。
如果沒有bootconsole,則會指定exclusive_console=newcon,console_unlock時,刷新全部log到該指定exclusive console。
console_unlock結(jié)束時會將exclusive_console置NULL,所以exclusive console默認(rèn)情況下就是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已經(jīng)加入console_drivers,如果bootconsole和newconsole是同一個物理設(shè)備,我們會看到這句printk會出現(xiàn)2次哦!如果在cmdline指定2個I/O設(shè)備,如“console==ttyS0,115200 console=ttyS1,115200”,因ttyS設(shè)備都是serial driver中注冊的real console,所以會看到kernel的打印分別出現(xiàn)在2個串口上!
boot console和real console差別在于bootconsole注冊于kernel啟動早期,方便對于kernel早期啟動進行調(diào)試打印。
那這些console是在哪里調(diào)用register_console進行注冊的?
bootconsole的注冊,如arch/arm/kernel/early_printk.c,是在parse_args參數(shù)解析階段注冊bootconsole。
在start_kernel中console_init函數(shù)也會遍歷.con_initcall.init段中所有注冊函數(shù),而這些注冊函數(shù)也可以來注冊bootconsole。
.con_initcall.init段中函數(shù)的注冊可以使用宏定義console_initcall。這些函數(shù)中調(diào)用register_console,方便在kernel初期實現(xiàn)printk打印。
realconsole的注冊,是在各個driver,如serial加載時完成。
經(jīng)過上面分析,對于一個新實現(xiàn)的輸入輸出設(shè)備,如果要將其作為kernel下的printk調(diào)試輸出設(shè)備,需要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調(diào)試信息是單向的,沒有交互,所以只需要實現(xiàn)write即可,還需要實現(xiàn)setup函數(shù),進行設(shè)備初始化(如設(shè)置波特率等),以及標(biāo)志位flags(將所有l(wèi)og刷出),舉個例子,如下:
- 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;
- }
為了調(diào)試方便,可以在console_init調(diào)用該函數(shù)進行注冊,則需要
- console_initcall(u_console_init);
也可以在kernel加載driver時調(diào)用,則需要在driver的probe時調(diào)用u_console_init,但是這樣只能等driver調(diào)register_console之后,console_unlock才將所有l(wèi)og刷出,之前的log都會存在log buf中。
(2)cmdline指定調(diào)試console,在kernel的cmdline添加參數(shù)console=ttyS0,115200
四 user空間console的選擇
用戶空間的輸入輸出依賴于其控制臺使用的哪個,這里有很多名詞,如控制臺,tty,console等,這些名字我也很暈,不用管他們的真正含義,搞嵌入式,直接找到它的實現(xiàn),搞明白從最上層軟件,到最底層硬件,如何操作,還有什么會不清楚呢。
在start_kernel中最后起內(nèi)核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設(shè)備,console設(shè)備做了控制臺。
console設(shè)備文件的創(chuàng)建在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的操作函數(shù)都是使用的tty的操作函數(shù),看open的實現(xiàn),如何找到具體的操作設(shè)備:
- 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找該進程所對應(yīng)的tty,因為init進程我們并沒有制定tty,所以該函數(shù)返回NULL。
接下來調(diào)用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設(shè)備文件,次設(shè)備號是1,根據(jù)代碼,會調(diào)用console_device來獲取對應(yīng)的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,調(diào)用device方法,獲取tty_driver,退出遍歷。
之后對于該console設(shè)備的讀寫操作都是基于該tty_driver。
所有的輸入輸出設(shè)備都會注冊tty_driver。
所以,對于一個新實現(xiàn)的輸入輸出設(shè)備,如果想讓其即作為kernel的printk輸出設(shè)備,也作為user空間的控制臺,則需要在上面u_console基礎(chǔ)上再實現(xiàn)device方法成員,來返回該設(shè)備的tty_driver。
那么還有一個問題:
如果cmdline指定2個I/O設(shè)備,“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!
總結(jié)下,kernel和user空間下都有一個console,關(guān)系到kernel下printk的方向和user下printf的方向,實現(xiàn)差別還是很大的。
kernel下的console是輸入輸出設(shè)備driver中實現(xiàn)的簡單的輸出console,只實現(xiàn)write函數(shù),并且是直接輸出到設(shè)備。
user空間下的console,實際就是tty的一個例子,所有操作函數(shù)都繼承與tty,全功能,可以打開 讀寫 關(guān)閉,所以對于console的讀寫,都是由kernel的tty層來最終發(fā)送到設(shè)備。
kernel的tty層之下還有l(wèi)disc線路規(guī)程層,線路規(guī)程層之下才是具體設(shè)備的driver。
ldisc層處理一些對于控制臺來說有意義的輸入輸出字符,比如輸入的crtl+C,輸出的‘\n‘進過線路規(guī)程會變?yōu)椤痋n\r‘。
所以對于kernel下console的write方法,不要忘記,對于log buf中'\n'的處理,實現(xiàn)一個簡單的線路規(guī)程!