linux的initcall機制

轉載:https://www.cnblogs.com/downey-blog/p/10486653.html

linux的initcall機制(針對編譯進內核的驅動)

initcall機制的由來

我們都知道,linux對驅動程序提供靜態編譯進內核和動態加載兩種方式,當我們試圖將一個驅動程序編譯進內核時,開發者通常提供一個xxx_init()函數接口以啓動這個驅動程序同時提供某些服務。

那麼,根據常識來說,這個xxx_init()函數肯定是要在系統啓動的某個時候被調用,才能啓動這個驅動程序。

最簡單直觀地做法就是:開發者試圖添加一個驅動程序時,在內核啓動init程序的某個地方直接添加調用自己驅動程序的xxx_init()函數,在內核啓動時自然會調用到這個程序。

但是,回頭一想,這種做法在單人開發的小系統中或許可以,但是在linux中,如果驅動程序是這麼個添加法,那就是一場災難,這個道理我想不用我多說。

不難想到另一種方式,就是集中提供一個地方,如果你要添加你的驅動程序,你就將你的初始化函數在這個地方進行添加,在內核啓動的時候統一掃描這個地方,再執行這一部分的所有被添加的驅動程序。

那到底怎麼添加呢?直接在C文件中作一個列表,在裏面添加初始化函數?我想隨着驅動程序數量的增加,這個列表會讓人頭昏眼花。

當然,對於linus大神而言,這些都不是事,linux的做法是:

底層實現上,在內核鏡像文件中,自定義一個段,這個段裏面專門用來存放這些初始化函數的地址,內核啓動時,只需要在這個段地址處取出函數指針,一個個執行即可。

對上層而言,linux內核提供xxx_init(init_func)宏定義接口,驅動開發者只需要將驅動程序的init_func使用xxx_init()來修飾,這個函數就被自動添加到了上述的段中,開發者完全不需要關心實現細節。
對於各種各樣的驅動而言,可能存在一定的依賴關係,需要遵循先後順序來進行初始化,考慮到這個,linux也對這一部分做了分級處理。

initcall的源碼

在平臺對應的init.h文件中,可以找到xxx_initcall的定義:

/*Only for built-in code, not modules.*/
#define early_initcall(fn)		__define_initcall(fn, early)

#define pure_initcall(fn)		__define_initcall(fn, 0)
#define core_initcall(fn)		__define_initcall(fn, 1)
#define core_initcall_sync(fn)		__define_initcall(fn, 1s)
#define postcore_initcall(fn)		__define_initcall(fn, 2)
#define postcore_initcall_sync(fn)	__define_initcall(fn, 2s)
#define arch_initcall(fn)		__define_initcall(fn, 3)
#define arch_initcall_sync(fn)		__define_initcall(fn, 3s)
#define subsys_initcall(fn)		__define_initcall(fn, 4)
#define subsys_initcall_sync(fn)	__define_initcall(fn, 4s)
#define fs_initcall(fn)			__define_initcall(fn, 5)
#define fs_initcall_sync(fn)		__define_initcall(fn, 5s)
#define rootfs_initcall(fn)		__define_initcall(fn, rootfs)
#define device_initcall(fn)		__define_initcall(fn, 6)
#define device_initcall_sync(fn)	__define_initcall(fn, 6s)
#define late_initcall(fn)		__define_initcall(fn, 7)
#define late_initcall_sync(fn)		__define_initcall(fn, 7s)

xxx_init_call(fn)的原型其實是__define_initcall(fn, n),n是一個數字或者是數字+s,這個數字代表這個fn執行的優先級,數字越小,優先級越高,帶s的fn優先級低於不帶s的fn優先級。
繼續跟蹤代碼,看看__define_initcall(fn,n):

#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn;  

值得注意的是,_attribute_()是gnu C中的擴展語法,它可以用來實現很多靈活的定義行爲,這裏不細究。

_attribute_((_section_(".initcall" #id ".init")))表示編譯時將目標符號放置在括號指定的段中。

而#在宏定義中的作用是將目標字符串化,##在宏定義中的作用是符號連接,將多個符號連接成一個符號,並不將其字符串化。

__used是一個宏定義,

#define  __used  __attribute__((__used__))

使用前提是在編譯器編譯過程中,如果定義的符號沒有被引用,編譯器就會對其進行優化,不保留這個符號,而__attribute__((_used_))的作用是告訴編譯器這個靜態符號在編譯的時候即使沒有使用到也要保留這個符號。

爲了更方便地理解,我們拿舉個例子來說明,開發者聲明瞭這樣一個函數:pure_initcall(test_init);

所以pure_initcall(test_init)的解讀就是:

首先宏展開成:__define_initcall(test_init, 0)  

然後接着展開:static initcall_t __initcall_test_init0 = test_init;這就是一個簡單的變量定義。  

同時聲明__initcall_test_init0這個變量即使沒被引用也保留符號,且將其放置在內核鏡像的.initcall0.init段處。  

需要注意的是,根據官方註釋可以看到early_initcall(fn)只針對內置的核心代碼,不能描述模塊。

xxx_initcall修飾函數的調用

既然我們知道了xxx_initcall是怎麼定義而且目標函數的放置位置,那麼使用xxx_initcall()修飾的函數是怎麼被調用的呢?

我們就從內核C函數起始部分也就是start_kernel開始往下挖,這裏的調用順序爲:

start_kernel  
	-> rest_init();
		-> kernel_thread(kernel_init, NULL, CLONE_FS);
			-> kernel_init()
				-> kernel_init_freeable();
					-> do_basic_setup();
						-> do_initcalls();  

這個do_initcalls()就是我們需要尋找的函數了,在這個函數中執行所有使用xxx_initcall()聲明的函數,接下來我們再來看看它是怎麼執行的:

static initcall_t *initcall_levels[] __initdata = {
	__initcall0_start,
	__initcall1_start,
	__initcall2_start,
	__initcall3_start,
	__initcall4_start,
	__initcall5_start,
	__initcall6_start,
	__initcall7_start,
	__initcall_end,
};

int __init_or_module do_one_initcall(initcall_t fn)
{
	...
	if (initcall_debug)
		ret = do_one_initcall_debug(fn);
	else
		ret = fn();
	...
	return ret;
}

static void __init do_initcall_level(int level)
{
	initcall_t *fn;
	...		
	for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
		do_one_initcall(*fn);
}

static void __init do_initcalls(void)
{
	int level;
	for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
		do_initcall_level(level);
}

在上述代碼中,定義了一個靜態的initcall_levels數組,這是一個指針數組,數組的每個元素都是一個指針.

do_initcalls()循環調用do_initcall_level(level),level就是initcall的優先級數字,由for循環的終止條件ARRAY_SIZE(initcall_levels) - 1可知,總共會調用do_initcall_level(0)~do_initcall_level(7),一共七次。

而do_initcall_level(level)中則會遍歷initcall_levels[level]中的每個函數指針,initcall_levels[level]實際上是對應的__initcall##level##_start指針變量,然後依次取出__initcall##level##_start指向地址存儲的每個函數指針,並調用do_one_initcall(*fn),實際上就是執行當前函數。

可以猜到的是,這個__initcall##level##start所存儲的函數指針就是開發者用xxx_initcall()宏添加的函數,對應".initcall##level##.init"段。

do_one_initcall(*fn)的執行:判斷initcall_debug的值,如果爲真,則調用do_one_initcall_debug(fn);如果爲假,則直接調用fn。事實上,調用do_one_initcall_debug(fn)只是在調用fn的基礎上添加一些額外的打印信息,可以直接看成是調用fn。

那麼,在initcall源碼部分有提到,在開發者添加xxx_initcall(fn)時,事實上是將fn放置到了".initcall##level##.init"的段中,但是在do_initcall()的源碼部分,卻是從initcall_levelslevel取出,initcall_levels[level]是怎麼關聯到".initcall##level##.init"段的呢?

答案在vmlinux.lds.h中:

#define INIT_CALLS_LEVEL(level)						\
	VMLINUX_SYMBOL(__initcall##level##_start) = .;		\
	KEEP(*(.initcall##level##.init))			\
	KEEP(*(.initcall##level##s.init))			\

#define INIT_CALLS							\
	VMLINUX_SYMBOL(__initcall_start) = .;			\
	KEEP(*(.initcallearly.init))				\
	INIT_CALLS_LEVEL(0)					\
	INIT_CALLS_LEVEL(1)					\
	INIT_CALLS_LEVEL(2)					\
	INIT_CALLS_LEVEL(3)					\
	INIT_CALLS_LEVEL(4)					\
	INIT_CALLS_LEVEL(5)					\
	INIT_CALLS_LEVEL(rootfs)				\
	INIT_CALLS_LEVEL(6)					\
	INIT_CALLS_LEVEL(7)					\
	VMLINUX_SYMBOL(__initcall_end) = .;   

在這裏首先定義了__initcall_start,將其關聯到".initcallearly.init"段。

然後對每個level定義了INIT_CALLS_LEVEL(level),將INIT_CALLS_LEVEL(level)展開之後的結果是定義__initcall##level##_start,並將
__initcall##level##_start關聯到".initcall##level##.init"段和".initcall##level##s.init"段。

到這裏,__initcall##level##_start和".initcall##level##.init"段的對應就比較清晰了,所以,從initcall_levels[level]部分一個個取出函數指針並執行函數就是執行xxx_init_call()定義的函數。

總結

便於理解,我們需要一個示例來梳理整個流程,假設我是一個驅動開發者,開發一個名爲beagle的驅動,在系統啓動時需要調用beagle_init()函數來啓動啓動服務。

我需要先將其添加到系統中:

core_initcall(beagle_init)

core_initcall(beagle_init)宏展開爲__define_initcall(beagle_init, 1),所以beagle_init()這個函數被放置在".initcall1.init"段處。

在內核啓動時,系統會調用到do_initcall()函數。 根據指針數組initcall_levels[1]找到__initcall1_start指針,在vmlinux.lds.h可以查到:__initcall1_start對應".initcall1.init"段的起始地址,依次取出段中的每個函數指針,並執行函數。

添加的服務就實現了啓動。

可能有些C語言基礎不太好的朋友不太理解do_initcall_level()函數中依次取出地址並執行的函數執行邏輯:

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

fn爲函數指針,fn++相當於函數指針+1,相當於:內存地址+sizeof(fn),sizeof(fn)根據平臺不同而不同,一般來說,32位機上是4字節,64位機則是8字節(關於指針在操作系統中的大小可以參考另一篇博客:不同平臺下指針大小 )。

而initcall_levels[level]指向當前".initcall##level##s.init"段,initcall_levels[level+1]指向".initcall##(level+1)##s.init"段,兩個段之間的內存就是存放所有添加的函數指針。
也就是從".initcall##level##s.init"段開始,每次取一個函數出來執行,並累加指針,直到取完。

好了,關於linux中initcall系統的討論就到此爲止啦,如果朋友們對於這個有什麼疑問或者發現有文章中有什麼錯誤,歡迎留言

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章