1. 程式人生 > >linux printk工作原理

linux 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;
}

printk打印出的資訊是輸出到console,在嵌入式系統中它一般是基於串列埠的,也就是串列埠控制檯。 控制檯有專用的驅動程式,也就是struct console結構的實現。 這也就是說,雖然控制檯可以是基於串列埠的控制檯,但是它並不使用串列埠驅動程式來輸出資訊,而是使用它自己的輸出函式(也即console->write(...)函式). 需要注意的一點是,基於串列埠實現的console->write()函式通常在寫串列埠時是要關閉串列埠的中斷的,這主要是為了避免與正常的、基於中斷方式的串列埠讀寫相互干擾。