Linux內核很吊之 module_init解析 (下)

忙了一段時間,終於有時間把inux內核很吊之 module_init解析 (下)整理完畢。

從上一篇博文http://blog.csdn.net/richard_liujh/article/details/45669207介紹了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的調用過程按照如下方式展現出來

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的函數原型如下。

/*
 * 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函數如下

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函數啦~~

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

這句for循環很簡單,循環執行條件是level < ARRAY_SIZE(initcall_levels)

ARRAY_SIZE是一個宏,用於求數組元素個數,在文件include\linux\kernel.h文件中

#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的定義

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數組中得元素是什麼???(看下面的分析前,請先弄清楚數組指針 和指針數組的區別,不然容易走火入魔…偷笑

static initcall_t *initcall_levels[] __initdata = {

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

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

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

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

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

我們再回來看看do_initcalls:

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) - 1level < 9 - 1。一共循環了8次。

循環8此就調用了do_initcall_level(level) 8次。
do_initcall_level函數原型如下:


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中找到的。

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);
如果將上面初始化過程中命令行參數解析過程忽略,那麼就剩下的內容也就是我們最想看到的內容了
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的定義

initcall_t *fn;

fn確實是一個initcall_t類型的指針,那initcall_t是什麼?

在文件include\linux\init.h文件中找到其定義

/*
 * 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數組了(之前暫時沒有分析的,現在開始分析了)

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文件中有如下的聲明

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 解析–上

__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單獨拿出來

__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循環

for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
		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

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

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

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

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函數就非常簡單了,讓我們看看最重要的內容如下

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的作用已經全部分析完畢~





發佈了9 篇原創文章 · 獲贊 1 · 訪問量 1109
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章