記得在編譯linux內核make menuconfig的時候設定輸出信息到console,要修改CONFIG_CMDLINE的內容,但是自始至終也沒搞懂為何這樣設置就可以把打印信息從串口輸出呢? 帶著這個疑問,我查看了linux的printk函數,最後找到了答案 .
一 printk 函數
printk 函數首先把要打印的信息放到buffer裡面,然後調用release_console_sem最後調用到相關驅動的write函數,如果你設定了 CONFIG_CMDLINE="console=ttySL0,19200,那麼printk信息就會調用ttySL這個驅動的write函數,也就是從串口輸出數據了.在__call_console_drivers裡面有一個很重要的變量console_drivers,它決定了調用哪支 driver輸出printk信息.
printk()
{
..............
//把待輸出文字放入buffer
va_start(args, fmt);
printed_len = vsnprintf(printk_buf, sizeof(printk_buf), fmt, args);
va_end(args);
..............
release_console_sem();
|
----------void release_console_sem(void)
{
.............
_call_console_drivers
|
----------static void __call_console_drivers(unsigned long start, unsigned long end)
{
struct console *con;
for (con = console_drivers; con; con = con->next) {
if ((con->flags & CON_ENABLED) && con->write)
con->write(con, &LOG_BUF(start), end - start);
}
}
}
二 選擇console driver
下面就是printk如果確定調用哪個driver的write函數輸出信息過程,或者說一個console driver選擇的過程.
首先看一下linux內核啟動代碼:
[Main.c]
asmlinkage void __init start_kernel(void)
{
..........
setup_arch(&command_line);
printk("Kernel command line: %s\n", saved_command_line);
parse_options(command_line);
..........
}
parse_options
{
//關鍵調用
checksetup(line)
|
--------int __init checksetup(char *line)
{
struct kernel_param *p;
if (line == NULL)
return 0;
p = &__setup_start;
do {
int n = strlen(p->str);
if (!strncmp(line,p->str,n)) {
if (p->setup_func(line+n))
return 1;
}
p++;
} while (p < &__setup_end);
return 0;
}
}
setup_arch根據CONFIG_CMDLINE指定的內容設定command_line指針. parse_options會遍歷一個kernel_param結構數組,起始於__setup_start, 終止於__setup_end,
struct kernel_param {
const char *str;
int (*setup_func)(char *);
};
此數組裡面的數據均來自於__setup(str, fn)這個宏.
[Init.h]
#define __setup(str, fn) \
static char __setup_str_##fn[] __initdata = str; \
static struct kernel_param __setup_##fn __attribute__((unused)) __initsetup = { __setup_str_##fn, fn }
在[printk.c]裡面有一個很重要的語句 __setup("console=", console_setup);這句話也就是說當CONFIG_CMDLINE含有"console="字符的話,就調用console_setup函數, 所以在parse_options調用的時候,
就會調用到 console_setup函數, console_setup就會記錄下來console驅動的name,以及一些選項參數到console_cmdline數組中(如波特率),設置 preferred_console參數,這樣console driver已經選擇好一半了.
console_setup()
{
..............
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) {
preferred_console = i;
return 1;
}
if (i == MAX_CMDLINECONSOLES)
return 1;
preferred_console = i;
c = &console_cmdline[i];
memcpy(c->name, name, sizeof(c->name));
c->options = options;
c->index = idx;
..............
}
選擇console driver的另一半來自於register_console(linux啟動後也會調用此函數),register_console
最重要的一句話是console->next = console_drivers;這樣就完成了選擇console driver的全過程.
void register_console(struct console * console)
{
..............
//找到console_driver的過程
for(i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++) {
if (strcmp(console_cmdline[i].name, console->name) != 0)
continue;
if (console->index >= 0 &&
console->index != console_cmdline[i].index)
continue;
if (console->index < 0)
console->index = console_cmdline[i].index;
if (console->setup &&
console->setup(console, console_cmdline[i].options) != 0) //此時做了setup的動作.
break;
console->flags |= CON_ENABLED;
console->index = console_cmdline[i].index;
if (i == preferred_console)
console->flags |= CON_CONSDEV;
break;
}
if (!(console->flags & CON_ENABLED))
return; //屏蔽掉其他非console的driver
if ((console->flags & CON_CONSDEV) || console_drivers == NULL) {
console->next = console_drivers;
console_drivers = console; //設定console_driver
} else {
console->next = console_drivers->next;
console_drivers->next = console;
}
..............
}
[後記]
bootloader也可以傳遞參數給Kernel, 原理就是bootloader向一塊內存中寫入具有特定結構的數據,然後Kernel在調用時分析此內存數據,最後也會放到標准command_line緩存中,像上面一樣處理.
[Setup.c]
__tagtable(ATAG_CMDLINE, parse_tag_cmdline);
static int __init parse_tag_cmdline(const struct tag *tag)
{
strncpy(default_command_line, tag->u.cmdline.cmdline, COMMAND_LINE_SIZE);
default_command_line[COMMAND_LINE_SIZE - 1] = '\0'; //放到default_command_line中
return 0;
}