Linux kernel printk的分析


记得在编译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;
}

相关内容