相信很多在研究linux內核源碼的同學,經常會發現一些模塊的初始化函數找不到調用者,比如下面的網絡模塊的初始化函數:
// net/ipv4/af_inet.c
static int __init inet_init(void)
{
...
/*
* Set the IP module up
*/
ip_init();
/* Setup TCP slab cache for open requests. */
tcp_init();
/* Setup UDP memory threshold */
udp_init();
...
}
fs_initcall(inet_init);
即使你在整個內核代碼中搜索,也找不到任何地方調用這個函數,那這個函數到底是怎麼調用的呢?
祕密就在這個函數之後的一行代碼裏:
fs_initcall( inet_init);
在該行代碼中,fs_initcall是一個宏,具體定義如下:
// include/linux/init.h
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__p__(#__sec ".init"))) = fn;
...
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
...
#define fs_initcall(fn) __define_initcall(fn, 5)
在該宏展開後,上面宏調用的結果,大致像下面這個樣子:
static initcall_t __initcall_inet_init5 __attribute__((__p__(".initcall5.init"))) = inet_init;
由上可見,fs_initcall宏最終是定義了一個靜態變量,該變量的類型是initcall_t,值是宏參數表示的函數地址。
initcall_t類型的定義如下:
typedef int (*initcall_t)(void);
由上可見,initcall_t是一個函數指針類型,它定義的變量會指向一個函數,該函數的參數要爲空,返回類型要爲int。
我們可以再看下上面的 inet_init 方法,該方法確實符合這些要求。
綜上可知,fs_initcall宏定義了一個變量 __initcall_inet_init5,它的類型爲initcall_t,它的值爲inet_init函數的地址。
到這裏我相信很多同學會想,linux內核一定是通過這個變量來調用inet_init函數的,對嗎?
對,也不對。
對是因爲內核確實是通過該變量指向的內存來獲取inet_init方法的地址並調用該方法的。
不對是因爲內核並不是通過上面的__initcall_inet_init5變量來訪問這個內存的。
那不用這個變量,還能通過其他方式訪問這個內存嗎?
當然可以,這正是linux內核設計的巧妙之處。
我們再來看下上面的宏展開之後,靜態變量__initcall_inet_init5的定義,在該定義中有如下的一些代碼:
__attribute__((__p__(".initcall5.init")))
該部分代碼並不屬於c語言標準,而是gcc對c語言的擴展,它的作用是聲明該變量屬於 .initcall5.init這個p。
所謂p,我們可以簡單的理解爲對程序所佔內存區域的一種佈局和規劃,比如我們常見的 p有 .text用來存放我們的代碼,.data或.bss用來存放我們的變量。
通過這些p的定義,我們可以把程序中的相關功能放到同一塊內存區域中,這樣來方便內存管理。
除了這些默認的p之外,我們還可以通過gcc的attribute來自定義p,這樣我們就可以把相關的函數或變量放到相同的p中了。
比如上面的__initcall_inet_init5變量就屬於.initcall5.init這個自定義p。
在定義了這些p之後,我們可以在鏈接腳本中告訴linker,這些p在內存中的位置及佈局是什麼樣子的。
對於x86平臺來說,內核的鏈接腳本是:
arch/x86/kernel/vmlinux.lds.S
在該腳本中,對.initcall5.init等這些p做了相關定義,具體邏輯如下:
// include/asm-generic/vmlinux.lds.h
#define INIT_CALLS_LEVEL(level) \
__initcall##level##_start = .; \
KEEP(*(.initcall##level##.init)) \
KEEP(*(.initcall##level##s.init)) \
#define INIT_CALLS \
__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) \
__initcall_end = .;
由上可見,initcall相關的p有很多,我們上面例子中的.initcall5.init只是其中一個,除此之外還有 .initcall0.init,.initcall1.init等等這些p。
這些p都是通過宏INIT_CALLS_LEVEL來定義其處理規則的,相同level的p被放到同一塊內存區域,不同level的p的內存區域按level大小依次連接在一起。
對於上面的__initcall_inet_init5變量來說,它的p是.initcall5.init,它的level是5。
假設我們還有其他方法調用了宏fs_initcall,那該宏爲該方法定義的靜態變量所屬的p也是.initcall5.init,level也是5。
由於該變量和__initcall_inet_init5變量所屬的initcall的level都相同,所以它們被連續放在同一塊內存區域裏。
也就是說,這些level爲5的靜態變量所佔的內存區域是連續的,又因爲這些變量的類型都爲initcall_t,所以它們正好構成了一個類型爲initcall_t的數組,而數組的起始地址也在INIT_CALLS_LEVEL宏中定義了,就是__initcall5_start。
如果我們想要調用這些level爲5的initcall,只要先拿到__initcall5_start地址,把其當成元素類型爲initcall_t的數組的起始地址,然後遍歷數組中的元素,獲取該元素對應的函數指針,就可以通過該指針調用對應的函數了。
來看下具體代碼:
// init/main.c
extern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];
static initcall_entry_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
static void __init do_initcall_level(int level)
{
initcall_entry_t *fn;
...
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
}
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
在上面的代碼中,do_initcalls方法遍歷了所有的合法level,對於每個level,do_initcall_level方法又調用了該level裏所有函數指針指向的函數。
我們上面示例中的inet_init方法就屬於level 5,也是在這裏被調用到的。
linux內核就是通過這種方式來調用各個模塊的初始化方法的,很巧妙吧。
最後我們再來總結下:
1. 在各模塊的初始化方法之後,一般都會調用一個類似於fs_initcall(inet_init)的宏,該宏的參數是該模塊的初始化方法的方法名。
2. 該宏展開後的結果是定義一個靜態變量,該變量通過gcc的attribute來聲明其所屬的initcall level的p,比如inet_init方法對應的靜態變量就屬於.initcall5.init這個p。
3. 在linux的鏈接腳本里,通過INIT_CALLS_LEVEL宏告知linker,將屬於同一level的所有靜態變量放到連續的一塊內存中,組成一個元素類型爲initcall_t的數組,該數組的起始地址放在類似__initcall5_start的變量中。
4. 在內核的初始化過程中,會通過調用 do_initcalls方法,遍歷各個level裏的各個函數指針,然後調用該指針指向的方法,即各模塊的初始化方法。
各個模塊的初始化方法就是這樣被調用的。
希望你喜歡。
(END)
相關閱讀:深入淺出Linux內核模塊
更多精彩,盡在"Linux閱碼場",掃描下方二維碼關注