1. 程式人生 > >Linux核心很吊之 module_init解析 二

Linux核心很吊之 module_init解析 二

簡單來說上篇博文介紹module_init如何註冊驅動的init函式,這篇博文將詳細分析kernel啟動過程又是如何執行我們註冊的init函式。

如果瞭解過linux作業系統啟動流程,那麼當bootloader載入完kernel並解壓並放置與記憶體中準備開始執行,首先被呼叫的函式是start_kernel。start_kernel函式顧名思義,核心從此準備開啟了,但是start_kernel做的事情非常多,簡單來說為核心啟動做準備工作,複雜來說也是非常之多(包含了自旋鎖檢查、初始化棧、CPU中斷、立即數、初始化頁地址、記憶體管理等等等...)。所以這篇博文我們還是主要分析和module_init註冊函式的執行過程

start_kernel函式在 init/main.c檔案中,由於start_kernel本身功能也比較多,所以為了簡介分析過程我把函式從start_kernel到do_initcalls的呼叫過程按照如下方式展現出來

 

[cpp] view plain copy

  1. start_kernel -> reset_init -> kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);  
  2.                                                     |  
  3.                                                     |->static int __ref kernel_init(void *unused)  
  4.                                                         |  
  5.                                                         |-> kernel_init_freeable( )  
  6.                                                                 |  
  7.                                                                 |-> do_basic_setup();  
  8.                                                                         |  
  9.                                                                         |——> do_initcalls();  

在上面的呼叫過程中,通過kernel_thread註冊了一個任務kernel_init,kernel_thread的函式原型如下。

[cpp] view plain copy

  1. /* 
  2.  * Create a kernel thread. 
  3.  */  
  4. pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)  
  5. {  
  6.     return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,  
  7.         (unsigned long)arg, NULL, NULL);  
  8. }  

kernel_thread建立了一個核心執行緒,也就是建立一個執行緒完成kernel_init的任務。通過kernel_init的逐層呼叫,最後呼叫到我們目前最應該關心的函式do_initcalls

do_initcalls函式如下

[cpp] view plain copy

  1. static void __init do_initcalls(void)  
  2. {  
  3.     int level;  
  4.   
  5.     for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)  
  6.         do_initcall_level(level);  
  7. }  

這個函式看起來就非常簡單了,裡面有for迴圈,每迴圈一次就呼叫一次do_initcall_level(level);其實可以發現在我們分析kernel原始碼時,大部分函式都能從函式名猜到函式的功能,這也是一名優秀程式猿的體現,大道至簡,悟在天成。

接下來我們就開始具體分析do_initcalls函式啦~~

[cpp] view plain copy

  1. for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)  

這句for迴圈很簡單,迴圈執行條件是level < ARRAY_SIZE(initcall_levels)。

ARRAY_SIZE是一個巨集,用於求陣列元素個數,在檔案include\linux\kernel.h檔案中

[cpp] view plain copy

  1. #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))  

當然ARRAY_SIZE巨集裡面還多了一個__must_be_array(),這個主要是確保我們傳過來的arr是一個數組,防止ARRAY_SIZE的誤用。所以在我們寫kernel驅動程式時,遇到需要求一個數組的大小請記得使用ARRAY_SIZE。有安全感又高大上...哈哈

那麼,initcall_levels是不是陣列呢?如果是,裡面有什麼內容?

還是在檔案main.c中有陣列initcall_levels的定義

[cpp] view plain copy

  1. static initcall_t *initcall_levels[] __initdata = {  
  2.     __initcall0_start,  
  3.     __initcall1_start,  
  4.     __initcall2_start,  
  5.     __initcall3_start,  
  6.     __initcall4_start,  
  7.     __initcall5_start,  
  8.     __initcall6_start,  
  9.     __initcall7_start,  
  10.     __initcall_end,  
  11. };  

這個陣列可不能小看他,如果看過module_init解析(上)的朋友,對數組裡面的名字“__initcall0 __initcall1 ... __initcall7”有一點點印象吧。

談到陣列,我們知道是元素的集合,那麼initcall_levels陣列中得元素是什麼???(看下面的分析前,請先弄清楚陣列指標 和指標陣列的區別,不然容易走火入魔...偷笑

[cpp] view plain copy

  1. static initcall_t *initcall_levels[] __initdata = {  

很顯然,這個陣列定義非常高大上。不管如何高大上,總離不開最基本的知識吧。所以我先從兩點去探索:

1. 陣列的名字,根據陣列標誌性的‘[ ]’,我們應該很容易知道陣列名字是initcall_levels

2.陣列的元素型別,由於定義中出現了指標的符號‘ * ’,也很容知道initcall_levels原來是一個指標陣列啦。

所以現在我們知道了initcall_levels數組裡面儲存的是指標啦,也就是指標的一個集合而已。掰掰腳趾數一下也能知道initcall_levels數組裡面有9個元素,他們都是指標。哈哈

對於這個陣列,我們先暫且到這兒,因為我們已經知道了陣列的個數了,也就知道for迴圈的迴圈次數。(後面還會繼續分析這個陣列,所以要由印象)

我們再回來看看do_initcalls:

[cpp] view plain copy

  1. static void __init do_initcalls(void)  
  2. {  
  3.     int level;  
  4.   
  5.     for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)  
  6.         do_initcall_level(level);  
  7. }  

ARRAY_SIZE求出了陣列initcall_levels的元素個數為9,所以level變數從 0 ~ 7都是滿足level < ARRAY_SIZE(initcall_levels) - 1既level < 9 - 1。一共迴圈了8次。

迴圈8此就呼叫了do_initcall_level(level) 8次。
do_initcall_level函式原型如下:

[cpp] view plain copy

  1. static void __init do_initcall_level(int level)  
  2. {  
  3.     extern const struct kernel_param __start___param[], __stop___param[];  
  4.     initcall_t *fn;  
  5.   
  6.     strcpy(static_command_line, saved_command_line);  
  7.     parse_args(initcall_level_names[level],  
  8.            static_command_line, __start___param,  
  9.            __stop___param - __start___param,  
  10.            level, level,  
  11.            &repair_env_string);  
  12.   
  13.     for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)  
  14.         do_one_initcall(*fn);  
  15. }  

在do_initcall_level函式中,有如下部分是和核心初始化過程呼叫parse_args對選項進行解析並呼叫相關函式去處理的。其中的__start___param和__stop___param也是可以在核心連結指令碼vmlinux.lds中找到的。

[cpp] view plain copy

  1. extern const struct kernel_param __start___param[], __stop___param[];  
  2.   
  3. strcpy(static_command_line, saved_command_line);  
  4. parse_args(initcall_level_names[level],  
  5.        static_command_line, __start___param,  
  6.        __stop___param - __start___param,  
  7.        level, level,  
  8.        &repair_env_string);  

如果將上面初始化過程中命令列引數解析過程忽略,那麼就剩下的內容也就是我們最想看到的內容了

[cpp] view plain copy

  1. for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)  
  2.     do_one_initcall(*fn);  

這個也很簡單,不就是一個for迴圈嘛,so easy~!!罵人

那麼接下來我們就開始分析這個for迴圈:

1. for迴圈開始,fn = initcall_levels[level],initcall_levels是上面分析過的陣列,數組裡面存放著指標,所以fn也應該是指標咯。那麼看看fn的定義

[cpp] view plain copy

  1. initcall_t *fn;  

fn確實是一個initcall_t型別的指標,那initcall_t是什麼?

在檔案include\linux\init.h檔案中找到其定義

[cpp] view plain copy

  1. /* 
  2.  * Used for initialization calls.. 
  3.  */  
  4. typedef int (*initcall_t)(void);  
  5. typedef void (*exitcall_t)(void);  

從上面的定義可以知道,initcall_t原來是一個函式指標的型別定義。函式的返回值是int型別,引數是空 void。從註釋也可以看出,initcall_t是初始化呼叫的。
簡單來說,fn是一個函式指標。

2. 每迴圈一次,fn++。迴圈執行的條件是fn < initcall_levels[level+1];

這裡fn++就不是很容易理解了,畢竟不是一個普通的變數而是一個函式指標,那麼fn++有何作用呢??

首先,fn = initcall_levels[level],所以我們還是有必要去再看看initcall_levels陣列了(之前暫時沒有分析的,現在開始分析了)

[cpp] view plain copy

  1. static initcall_t *initcall_levels[] __initdata = {  
  2.     __initcall0_start,  
  3.     __initcall1_start,  
  4.     __initcall2_start,  
  5.     __initcall3_start,  
  6.     __initcall4_start,  
  7.     __initcall5_start,  
  8.     __initcall6_start,  
  9.     __initcall7_start,  
  10.     __initcall_end,  
  11. };  

已經知道了initcall_levels是一個指標陣列,也就是說陣列的元素都是指標,指標是指向什麼型別的資料呢? 是initcall_t型別的,上面剛剛分析過initcall_t是函式指標的型別定義。

這樣一來,initcall_levels數組裡面儲存的元素都是陣列指標啦。

很顯然這是通過列舉的方式定義了陣列initcall_levels,那麼元素值是多少??(陣列中元素是分別是 __initcall0_start __initcall1_start __initcall2_start ... __initcall7_start __initcall_end)

通過尋找會發現在main.c檔案中有如下的宣告

[cpp] view plain copy

  1. extern initcall_t __initcall_start[];  
  2. extern initcall_t __initcall0_start[];  
  3. extern initcall_t __initcall1_start[];  
  4. extern initcall_t __initcall2_start[];  
  5. extern initcall_t __initcall3_start[];  
  6. extern initcall_t __initcall4_start[];  
  7. extern initcall_t __initcall5_start[];  
  8. extern initcall_t __initcall6_start[];  
  9. extern initcall_t __initcall7_start[];  
  10. extern initcall_t __initcall_end[];  

所以__initcall0_start __initcall1_start __initcall2_start ... __initcall7_start __initcall_end都是initcall_t型別的陣列名,陣列名也就是指標。只是這些都是extern宣告的,所以在本檔案裡面找不到他們的定義出。那麼他們在哪一個檔案??答案還是 連結指令碼 vmlinux.lds,而且我們已經看過這些名字很多次了...

下面再次把連結指令碼中相關的內容拿出來:(相關的解釋請參考 module_init 解析--上

[cpp] view plain copy

  1. __init_begin = .;  
  2.  . = ALIGN(4096); .init.text : AT(ADDR(.init.text) - 0) { _sinittext = .; *(.init.text) *(.cpuinit.text) *(.meminit.text) _einittext = .; }  
  3.  .init.data : AT(ADDR(.init.data) - 0) { *(.init.data) *(.cpuinit.data) *(.meminit.data) *(.init.rodata) *(.cpuinit.rodata) *(.meminit.rodata) . = ALIGN(32); __dtb_start = .; *(.dtb.init.rodata) __dtb_end = .; . = ALIGN(16); __setup_start = .; *(.init.setup) __setup_end = .; __initcall_start = .; *(.initcallearly.init) __initcall0_start = .; *(.initcall0.init) *(.initcall0s.init) __initcall1_start = .; *(.initcall1.init) *(.initcall1s.init) __initcall2_start = .; *(.initcall2.init) *(.initcall2s.init) __initcall3_start = .; *(.initcall3.init) *(.initcall3s.init) __initcall4_start = .; *(.initcall4.init) *(.initcall4s.init) __initcall5_start = .; *(.initcall5.init) *(.initcall5s.init) __initcallrootfs_start = .; *(.initcallrootfs.init) *(.initcallrootfss.init) __initcall6_start = .; *(.initcall6.init) *(.initcall6s.init) __initcall7_start = .; *(.initcall7.init) *(.initcall7s.init) __initcall_end = .; __con_initcall_start = .; *(.con_initcall.init) __con_initcall_end = .; __security_initcall_start = .; *(.security_initcall.init) __security_initcall_end = .; . = ALIGN(4); __initramfs_start = .; *(.init.ramfs) . = ALIGN(8); *(.init.ramfs.info) }  
  4.  . = ALIGN(4);  

所以在main.c檔案中extern宣告的那些陣列__initcall0_start  ... __initcall7_start __initcall_end其實就是上面連結指令碼vmlinux.lds中定義的標號(也可以暫且簡單粗暴認為是地址)。
為了好理解,把其中的__initcall0_start單獨拿出來

[cpp] view plain copy

  1. __initcall0_start = .; *(.initcall0.init) *(.initcall0s.init)  

這裡的意思是,__initcall0_start 是一段地址的開始,從這個地址開始連結所有.initcall0.init和.initcall0s.init段的內容。那.initcall0.init和.initcall0s.init段有什麼東東??這就是上篇博文中解釋的。簡單來說,就是我們通過module_init(xxx)新增的內容,只是module_init對應的level值預設為6而已。

總而言之,__initcallN_start(其中N = 0,1,2...7)地址開始存放了一系列優先順序為N的函式。我們通過module_init註冊的函式優先順序為6

現在我們回過頭再去看看上面的for迴圈

[cpp] view plain copy

  1. for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)  
  2. <span style="white-space: pre;">      </span>do_one_initcall(*fn);  

一開始fn = initcall_levels[level],假設level = 0。也就是fn = initcall_levels[0] = __initcall0_start。所以fn指向了連結指令碼中的__initcall0_start地址,每當fn++也就是fn逐次指向註冊到.initcall0.init和.initcall0s.init段中的函式地址了。for迴圈的條件是fn < initcall_levels[level + 1] = initcall_levels[0 + 1] = initcall_level[1] = __initcall1_start。

為了能直觀看出fn增加的範圍,用如下的簡易方式表達一下。

__initcall0_start  __initcall1_start  __initcall2_start  __initcall3_start ... ... __initcall7_start  __initcall_end

| <----- fn++ ---->|| <----- fn++ --->| | <----- fn++ --->| | <----- fn++ --->|... ... | <----- fn++ --->| END

瞭解這一點,我們已經接近勝利的彼岸~~

[cpp] view plain copy

  1. for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)  
  2.     do_one_initcall(*fn);  

最後我們要了解的就是for迴圈每次執行的內容do_one_initcall(*fn),其函式原型如下

[cpp] view plain copy

  1. int __init_or_module do_one_initcall(initcall_t fn)  
  2. {  
  3.     int count = preempt_count();  
  4.     int ret;  
  5.   
  6.     if (initcall_debug)  
  7.         ret = do_one_initcall_debug(fn);  
  8.     else  
  9.         ret = fn();  
  10.   
  11.     msgbuf[0] = 0;  
  12.   
  13.     if (preempt_count() != count) {  
  14.         sprintf(msgbuf, "preemption imbalance ");  
  15.         preempt_count() = count;  
  16.     }  
  17.     if (irqs_disabled()) {  
  18.         strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));  
  19.         local_irq_enable();  
  20.     }  
  21.     WARN(msgbuf[0], "initcall %pF returned with %s\n", fn, msgbuf);  
  22.   
  23.     return ret;  
  24. }  

do_one_initcall函式就非常簡單了,讓我們看看最重要的內容如下

[cpp] view plain copy

  1. if (initcall_debug)  
  2.     ret = do_one_initcall_debug(fn);  
  3. else  
  4.     ret = fn();  

這裡就是判斷是不是debug模式,無非debug會多一些除錯的操作。但是不管是哪一種,他們都執行 ret = fn( );
因為fn就是函式指標,fn指向的是我們註冊到__initcall0_start  ... __initcall7_start的一系列函式。所以 fn( ); 就是呼叫這些函式。當然也包括了驅動中module_init註冊的函式啦,只是通過module_init註冊的level等級是6,for迴圈是從level = 0開始的,這也能看出0是優先順序最高,7是優先順序最低的。

到現在,module_init的作用已經全部分析完畢~