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
- start_kernel -> reset_init -> kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
- |
- |->static int __ref kernel_init(void *unused)
- |
- |-> kernel_init_freeable( )
- |
- |-> do_basic_setup();
- |
- |——> do_initcalls();
在上面的呼叫過程中,通過kernel_thread註冊了一個任務kernel_init,kernel_thread的函式原型如下。
[cpp] view plain copy
- /*
- * Create a kernel thread.
- */
- pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
- {
- return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
- (unsigned long)arg, NULL, NULL);
- }
kernel_thread建立了一個核心執行緒,也就是建立一個執行緒完成kernel_init的任務。通過kernel_init的逐層呼叫,最後呼叫到我們目前最應該關心的函式do_initcalls;
do_initcalls函式如下
[cpp] view plain copy
- static void __init do_initcalls(void)
- {
- int level;
- for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
- do_initcall_level(level);
- }
這個函式看起來就非常簡單了,裡面有for迴圈,每迴圈一次就呼叫一次do_initcall_level(level);其實可以發現在我們分析kernel原始碼時,大部分函式都能從函式名猜到函式的功能,這也是一名優秀程式猿的體現,大道至簡,悟在天成。
接下來我們就開始具體分析do_initcalls函式啦~~
[cpp] view plain copy
- 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
- #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
- static initcall_t *initcall_levels[] __initdata = {
- __initcall0_start,
- __initcall1_start,
- __initcall2_start,
- __initcall3_start,
- __initcall4_start,
- __initcall5_start,
- __initcall6_start,
- __initcall7_start,
- __initcall_end,
- };
這個陣列可不能小看他,如果看過module_init解析(上)的朋友,對數組裡面的名字“__initcall0 __initcall1 ... __initcall7”有一點點印象吧。
談到陣列,我們知道是元素的集合,那麼initcall_levels陣列中得元素是什麼???(看下面的分析前,請先弄清楚陣列指標 和指標陣列的區別,不然容易走火入魔...)
[cpp] view plain copy
- static initcall_t *initcall_levels[] __initdata = {
很顯然,這個陣列定義非常高大上。不管如何高大上,總離不開最基本的知識吧。所以我先從兩點去探索:
1. 陣列的名字,根據陣列標誌性的‘[ ]’,我們應該很容易知道陣列名字是initcall_levels
2.陣列的元素型別,由於定義中出現了指標的符號‘ * ’,也很容知道initcall_levels原來是一個指標陣列啦。
所以現在我們知道了initcall_levels數組裡面儲存的是指標啦,也就是指標的一個集合而已。掰掰腳趾數一下也能知道initcall_levels數組裡面有9個元素,他們都是指標。哈哈
對於這個陣列,我們先暫且到這兒,因為我們已經知道了陣列的個數了,也就知道for迴圈的迴圈次數。(後面還會繼續分析這個陣列,所以要由印象)
我們再回來看看do_initcalls:
[cpp] view plain copy
- static void __init do_initcalls(void)
- {
- int level;
- for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
- do_initcall_level(level);
- }
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
- static void __init do_initcall_level(int level)
- {
- extern const struct kernel_param __start___param[], __stop___param[];
- initcall_t *fn;
- strcpy(static_command_line, saved_command_line);
- parse_args(initcall_level_names[level],
- static_command_line, __start___param,
- __stop___param - __start___param,
- level, level,
- &repair_env_string);
- for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
- do_one_initcall(*fn);
- }
在do_initcall_level函式中,有如下部分是和核心初始化過程呼叫parse_args對選項進行解析並呼叫相關函式去處理的。其中的__start___param和__stop___param也是可以在核心連結指令碼vmlinux.lds中找到的。
[cpp] view plain copy
- extern const struct kernel_param __start___param[], __stop___param[];
- strcpy(static_command_line, saved_command_line);
- parse_args(initcall_level_names[level],
- static_command_line, __start___param,
- __stop___param - __start___param,
- level, level,
- &repair_env_string);
如果將上面初始化過程中命令列引數解析過程忽略,那麼就剩下的內容也就是我們最想看到的內容了
[cpp] view plain copy
- for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
- do_one_initcall(*fn);
這個也很簡單,不就是一個for迴圈嘛,so easy~!!
那麼接下來我們就開始分析這個for迴圈:
1. for迴圈開始,fn = initcall_levels[level],initcall_levels是上面分析過的陣列,數組裡面存放著指標,所以fn也應該是指標咯。那麼看看fn的定義
[cpp] view plain copy
- initcall_t *fn;
fn確實是一個initcall_t型別的指標,那initcall_t是什麼?
在檔案include\linux\init.h檔案中找到其定義
[cpp] view plain copy
- /*
- * Used for initialization calls..
- */
- typedef int (*initcall_t)(void);
- 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
- static initcall_t *initcall_levels[] __initdata = {
- __initcall0_start,
- __initcall1_start,
- __initcall2_start,
- __initcall3_start,
- __initcall4_start,
- __initcall5_start,
- __initcall6_start,
- __initcall7_start,
- __initcall_end,
- };
已經知道了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
- extern initcall_t __initcall_start[];
- extern initcall_t __initcall0_start[];
- extern initcall_t __initcall1_start[];
- extern initcall_t __initcall2_start[];
- extern initcall_t __initcall3_start[];
- extern initcall_t __initcall4_start[];
- extern initcall_t __initcall5_start[];
- extern initcall_t __initcall6_start[];
- extern initcall_t __initcall7_start[];
- 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
- __init_begin = .;
- . = ALIGN(4096); .init.text : AT(ADDR(.init.text) - 0) { _sinittext = .; *(.init.text) *(.cpuinit.text) *(.meminit.text) _einittext = .; }
- .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) }
- . = ALIGN(4);
所以在main.c檔案中extern宣告的那些陣列__initcall0_start ... __initcall7_start __initcall_end其實就是上面連結指令碼vmlinux.lds中定義的標號(也可以暫且簡單粗暴認為是地址)。
為了好理解,把其中的__initcall0_start單獨拿出來
[cpp] view plain copy
- __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
- for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
- <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
- for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
- do_one_initcall(*fn);
最後我們要了解的就是for迴圈每次執行的內容do_one_initcall(*fn),其函式原型如下
[cpp] view plain copy
- int __init_or_module do_one_initcall(initcall_t fn)
- {
- int count = preempt_count();
- int ret;
- if (initcall_debug)
- ret = do_one_initcall_debug(fn);
- else
- ret = fn();
- msgbuf[0] = 0;
- if (preempt_count() != count) {
- sprintf(msgbuf, "preemption imbalance ");
- preempt_count() = count;
- }
- if (irqs_disabled()) {
- strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
- local_irq_enable();
- }
- WARN(msgbuf[0], "initcall %pF returned with %s\n", fn, msgbuf);
- return ret;
- }
do_one_initcall函式就非常簡單了,讓我們看看最重要的內容如下
[cpp] view plain copy
- if (initcall_debug)
- ret = do_one_initcall_debug(fn);
- else
- 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的作用已經全部分析完畢~