一、導讀
在linux內核啟動過程中,會向終端打印出很多的日志信息,從這些信息中可以得到許多內核的行為。如果在啟動階段出現了問題,那么很多的提示信息也會從終端打印出。
這些信息的輸出與具體模塊功能的執行都歸功于一個函數:do_initcalls,本文將主要分析這個函數的執行邏輯,且從這個函數延伸到linux各個子系統初始化背后的機制。
本文所有源碼分析基于linux內核版本:4.1.15
二、do_initcalls
do_initcalls由do_basic_setup()調用:
do_basic_setup()由kernel_init()代表的內核init線程函數間接調用(在kernel_init_freeable()被調用)。
在調用do_basic_setup之前,處理器已經被初始化了,CPU子系統已經啟動并且運行,內存和進程管理也工作正常,但是系統中的設備還沒有被初始化,故而do_basic_setup正作用于此,本文主要描述do_initcalls,所以不再進而分析其他的函數。
do_initcalls在/init/main.c文件中實現:
staticvoid__initdo_initcalls(void) { intlevel; for(level=0;level
函數中內容比較少,是一個for循環結構,循環的對象是initcall_levels數組,該數組用于描述初始化調用的級別,定義如下:
externinitcall_t__initcall_start[]; externinitcall_t__initcall0_start[]; externinitcall_t__initcall1_start[]; externinitcall_t__initcall2_start[]; externinitcall_t__initcall3_start[]; externinitcall_t__initcall4_start[]; externinitcall_t__initcall5_start[]; externinitcall_t__initcall6_start[]; externinitcall_t__initcall7_start[]; externinitcall_t__initcall_end[]; staticinitcall_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類型的指針,回到do_initcalls()函數中,該函數的核心操作是:按順序從__initcall0_start開始,到__initcall_end結束的節段(稱為初始化調用段)中取出不同段之間的函數,并執行。
存在這幾個初始化調用段之間的函數都是內核中各個模塊的初始化函數,而這些函數是如何加入到初始化調用段中的呢?又是如何設置調用級別的,會在后文中描述到。在do_initcalls()函數中,會根據initcall_levels初始化調用級別的數量調用do_initcall_level(),該函數實現如下:
staticvoid__initdo_initcall_level(intlevel) { initcall_t*fn; strcpy(initcall_command_line,saved_command_line); parse_args(initcall_level_names[level], initcall_command_line,__start___param, __stop___param-__start___param, level,level, &repair_env_string); for(fn=initcall_levels[level];fn
從上述代碼可見,在函數的最后也是一個for循環結構,該循環的操作對象為函數指針,且會將對應的函數指針傳遞到do_one_initcall中,在該函數執行函數指針所指向的函數:
三、構造section并添加函數
(3-1)構造初始化調用section
在linux內核中,不同架構(ARCH)下的kernel目錄中,都會有一個名為vmlinux.lds.S的鏈接腳本,初始化調用section的構造則在這個鏈接腳本中完成。
本文以ARM32架構為例
在/arch/arm/kernel/vmlinux.lds.S中的鏈接腳本中,.init.data輸出節段則需要INIT_CALLS作為輸入節段:
INIT_CALLS定義在/include/asm-generic/vmlinux.lds.h文件中:
而在內核的makefile中有以下語句:
LDFLAGS_vmlinux+=-Tarch/$(ARCH)/kernel/vmlinux.lds.s
用于指定構建linux內核鏡像時所使用的鏈接腳本,基于此,則會構造好初始化調用section。
當初始化調用section構造完成后,是如何向該section中添加函數的呢?繼續往下看。(3-2)向section中添加函數
向section中添加函數的本質操作則是__define_initcall(),定義如下:
并且linux內核基于__define_initcall()封裝出了多個宏定義接口,供內核中各個模塊使用,接口如下:
__define_initcall()宏定義的本質則是定義一個initcall_t函數指針類型的變量并命名為__initcall_##fn##id,其中fn為賦值給該變量的函數名稱,id為初始化調用級別,然后將fn賦值給該變量。接著就是最為重要的技術點:使用__attribute__將該變量加入到命名為"initcall##id.init"的section中,其中id為初始化調用級別,所以將fn添加到初始化調用section中則是通過這一點實現。例如:如果有以下類似的代碼:
staticvoid__initshow_info(void) { printk("I'miriczhao ") } core_initcall(show_info);
經過層層宏替換后,本質上則變成:
staticinitcall_t__initcall_core_initcall1__used __attribute__((__section__(".initcall1.init")))=show_info;
四、總結
綜上,linux內核中使用基于__define_initcall封裝出的多個接口API初始化內核的各個模塊,使用這些API接口會將指定的函數放到名稱為.initcall##id.init的section中,id為初始化調用級別,內核中定義了14種調用級別:分別為1~7和1s~7s(linux 3.0后增加的擴展)。這些調用級別是按照先后順序依次排列的。
(4-1)linux內核中,對于內核的各個模塊的初始化,正是通過使用__define_initcall()的衍生宏定義API將初始化函數放置到__initcall##id.initsection中,不同模塊的初始化函數按照調用級別順序排列。在內核啟動階段,這些放置到這個section中的函數指針將被do_initcalls()按順序依次調用,進而完成各個模塊的初始化操作。
linux內核系統非常龐大,各個子系統也非常多,他們的初始化函數從源碼上是不需要在內核啟動過程中去主動調用的,從設計上這一點也不現實,隨著內核功能的增加,越來越復雜的驅動程序,從而linux內核基于編譯器section技術,設計了初始化調用機制,將各個模塊的初始化與linux內核啟動主線分離。
(4-2)當使用基于__define_initcall封裝出的多個API接口時,函數指針放置到哪個子section由具體的宏定義API接口的level參數確定,較小的level參數對應的函數指針則被放置在前面。而位于同一個子section內的函數指針順序不定,由編譯器按照編譯的順序隨機指定。所以,如果一個模塊的初始化函數想要越早被調用執行,則需要有較小的調用級別。
審核編輯:劉清
-
處理器
+關注
關注
68文章
19259瀏覽量
229652 -
Arch
+關注
關注
0文章
18瀏覽量
9649 -
LINUX內核
+關注
關注
1文章
316瀏覽量
21644
原文標題:驚呆了,linux內核中的這機制 | do_initcalls
文章出處:【微信號:嵌入式小生,微信公眾號:嵌入式小生】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論