最近工作在調試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來解析,如下:
[cpp]?view plaincopy
static?int?__init?console_setup(char?*str)??{??????char?buf[sizeof(console_cmdline[0].name)?+?4];???????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????????????????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如下:
[cpp]?view plaincopy
????static?struct?console?*exclusive_console;????????struct?console_cmdline??{????????????????????????char????name[8];??????????????????int?index;????????????????????char????*options;?????????????#ifdef?CONFIG_A11Y_BRAILLE_CONSOLE??????char????*brl_options;?????????????#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;?????????????????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相關代碼如下:
[cpp]?view plaincopy
#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?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;???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代碼。如下:
[cpp]?view plaincopy
????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定義如下:
[cpp]?view plaincopy
?????????????????#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中,下面來分析下該函數。
[cpp]?view plaincopy
void?register_console(struct?console?*newcon)??{??????int?i;??????unsigned?long?flags;??????struct?console?*bcon?=?NULL;????????????????????if?(console_drivers?&&?newcon->flags?&?CON_BOOT)?{????????????????????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;??????????????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;??????????????}??????????}??????}??????????????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->flags?|=?CON_CONSDEV;??????????????preferred_console?=?selected_console;??????????}??????????break;??????}??????????????if?(!(newcon->flags?&?CON_ENABLED))??????????return;????????????????????if?(bcon?&&?((newcon->flags?&?(CON_CONSDEV?|?CON_BOOT))?==?CON_CONSDEV))??????????newcon->flags?&=?~CON_PRINTBUFFER;??????????????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)?{????????????????????raw_spin_lock_irqsave(&logbuf_lock,?flags);??????????con_start?=?log_start;??????????raw_spin_unlock_irqrestore(&logbuf_lock,?flags);??????????????????????????????exclusive_console?=?newcon;??????}????????????console_unlock();??????console_sysfs_notify();????????????????????if?(bcon?&&??????????((newcon->flags?&?(CON_CONSDEV?|?CON_BOOT))?==?CON_CONSDEV)?&&??????????!keep_bootcon)?{??????????????????????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:
[cpp]?view plaincopy
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如下:
[cpp]?view plaincopy
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刷出),舉個例子,如下:
[cpp]?view plaincopy
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調用該函數進行注冊,則需要
[cpp]?view plaincopy
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進程時,如下:
[cpp]?view plaincopy
??????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中,如下:
[cpp]?view plaincopy
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的實現,如何找到具體的操作設備:
[cpp]?view plaincopy
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;??????????}????????????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,如下:
[cpp]?view plaincopy
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)?{????????????????????????????????????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,如下:
[cpp]?view plaincopy
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'的處理,實現一個簡單的線路規程!
總結
以上是生活随笔為你收集整理的kernel 下串口serial输入输出控制,屏蔽log的输出的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。