近期工作在調試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=\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'的處理。實現一個簡單的線路規程!
