Linux內(nèi)核initcall機(jī)制:驅(qū)動初始化的幕后英雄
你想想,咱們每天使用的電腦、手機(jī)等各類智能設(shè)備,之所以能流暢運行形形色色的功能,背后離不開 Linux 內(nèi)核驅(qū)動著海量的硬件。而在這繁雜的硬件驅(qū)動初始化過程中,initcall 機(jī)制宛如一位運籌帷幄的指揮官,不動聲色地將一切安排得井井有條。
當(dāng)系統(tǒng)啟動的號角吹響,眾多硬件驅(qū)動就像等待檢閱的士兵,急切地需要按恰當(dāng)順序、在精準(zhǔn)時機(jī)完成初始化,才能確保整個系統(tǒng)順利啟航。要是沒有 initcall 機(jī)制,這亂糟糟的局面簡直不敢想象,可能系統(tǒng)還沒 “起跑” 就陷入崩潰泥潭?,F(xiàn)在,就跟著我一同揭開 initcall 機(jī)制那神秘的面紗,看看它究竟是如何施展魔力,讓 Linux 內(nèi)核世界有條不紊運轉(zhuǎn)的吧!
一、Linux驅(qū)動初始化的困境
寫過 Linux 驅(qū)動的朋友,想必對 module_init 宏都不陌生,它可是驅(qū)動初始化的關(guān)鍵入口。在 Linux 系統(tǒng)里,驅(qū)動程序的加載方式有靜態(tài)編譯進(jìn)內(nèi)核和動態(tài)加載兩種。要是采用靜態(tài)編譯,開發(fā)者通常得提供諸如 xxx_init() 這樣的函數(shù)接口,來啟動驅(qū)動并提供相關(guān)服務(wù)。按照常理,這個 xxx_init() 函數(shù)必須在系統(tǒng)啟動的某個節(jié)點被調(diào)用,驅(qū)動才能正常運作。
最容易想到的辦法,就是開發(fā)者手動在內(nèi)核啟動 init 程序的某個地方,添加對自己驅(qū)動程序 xxx_init() 函數(shù)的調(diào)用。就像下面這樣:
void init(void) {
a_init();
b_init();
//...
z_init();
}
不過,這種做法要是放在單人開發(fā)的小系統(tǒng)里,或許還能應(yīng)付得來。但 Linux 系統(tǒng)如此龐大復(fù)雜,驅(qū)動數(shù)量眾多,要是每添加一個驅(qū)動,都得去改動 kernel_init() 代碼,那簡直就是一場 “災(zāi)難”。一方面,這極易引入人為錯誤,稍有不慎就可能導(dǎo)致系統(tǒng)啟動故障;另一方面,代碼的可維護(hù)性會變得極差,后續(xù)排查問題、升級驅(qū)動都會讓人頭疼不已。
既然直接手動添加不靠譜,那換種思路,集中提供一個地方來管理驅(qū)動初始化程序怎么樣?比如,開發(fā)者把自己的初始化函數(shù)添加到這個統(tǒng)一的地方,內(nèi)核啟動時,就去掃描并執(zhí)行所有添加進(jìn)來的驅(qū)動程序。像下面這樣簡單用 C 文件做個列表:
#include <stdio.h>
void a_init(void) {
printf("%s\n", __func__);
}
void b_init(void) {
printf("%s\n", __func__);
}
void (*fun_list[])(void) = {a_init, b_init};
void init(void) {
int i;
void(*pfun)(void);
for (i = 0; i < sizeof(fun_list) / sizeof(fun_list[0]); ++i) {
printf("%d\n", i);
fun_list[i]();
}
}
但這個方法也并非盡善盡美,它需要開發(fā)者手動維護(hù)這個列表,一旦驅(qū)動數(shù)量增多或者有更新、刪除操作,管理成本就會直線上升,還容易出現(xiàn)遺漏、重復(fù)添加等問題。那么,Linux 內(nèi)核究竟是如何巧妙化解這個難題的呢?
二、Initcall機(jī)制登場
2.1核心概念
Linux 內(nèi)核里,為了解決驅(qū)動初始化的難題,引入了 initcall 機(jī)制。簡單來說,initcall 機(jī)制就是一套規(guī)范化、自動化的驅(qū)動初始化函數(shù)管理方案。它在內(nèi)核編譯階段 “大顯身手”,通過一系列精心設(shè)計的宏定義,巧妙地將不同驅(qū)動的初始化函數(shù)按照預(yù)設(shè)的優(yōu)先級順序,依次存放到特定的內(nèi)存段中。當(dāng)內(nèi)核啟動時,就如同一位訓(xùn)練有素的指揮官,有條不紊地遍歷這些內(nèi)存段,精準(zhǔn)地調(diào)用各個初始化函數(shù),確保每個驅(qū)動都能在恰當(dāng)?shù)臅r機(jī)完成初始化,順利 “上崗”,為系統(tǒng)的穩(wěn)定運行保駕護(hù)航。這一機(jī)制不僅讓驅(qū)動初始化變得井井有條,還極大地減輕了開發(fā)者的負(fù)擔(dān),提升了內(nèi)核的可維護(hù)性,可謂是 Linux 內(nèi)核中的一大 “得力助手”。
2.2源碼剖析
深入到 Linux 內(nèi)核源碼中,initcall 機(jī)制的實現(xiàn)可謂精妙絕倫。在 include/linux/init.h 文件里,藏著一系列讓人眼花繚亂卻又邏輯嚴(yán)密的宏定義,它們是 initcall 機(jī)制的 “幕后操控者”。
對于靜態(tài)加載的驅(qū)動,內(nèi)核定義了諸如 early_initcall、pure_initcall、core_initcall 等眾多宏。就拿 core_initcall 來說,它背后其實是 __define_initcall 宏在發(fā)揮關(guān)鍵作用。展開來看,__define_initcall(fn, 1)(這里以 core_initcall 的參數(shù) 1 為例),經(jīng)過層層解析,就像是一場奇妙的 “魔術(shù)表演”:先是定義了一個靜態(tài)的函數(shù)指針 initcall_t __initcall_##fn##1,這里的 ## 是連接符號的 “膠水”,把函數(shù)名 fn 和等級標(biāo)識 1 緊緊粘在一起,變成一個獨一無二的函數(shù)指針名稱。而 __attribute__((__section__(".initcall1.init"))) 則像是一個精準(zhǔn)的 “導(dǎo)航儀”,告訴編譯器把這個函數(shù)指針變量放到名為 .initcall1.init 的特定代碼段中,這個代碼段就像是一個為初始化函數(shù)精心準(zhǔn)備的 “候車室”,等待內(nèi)核啟動時的 “召喚”。并且,__used 這個屬性也很關(guān)鍵,它像是給函數(shù)指針穿上了一層 “保護(hù)衣”,防止編譯器在優(yōu)化過程中,把這個看似暫時沒被用到的符號給無情 “拋棄”,確保了機(jī)制的完整性。
再看動態(tài)加載的情況,以常用的 module_init 宏為例,當(dāng)我們在驅(qū)動代碼里寫下 module_init(xxx_init) 時,這背后的故事同樣精彩。module_init 宏在 include/linux/module.h 中被定義為 __initcall(x),而進(jìn)一步追溯,它其實就是 device_initcall(x),最終也會導(dǎo)向 __define_initcall(x, 6)。這意味著,通過 module_init 修飾的驅(qū)動初始化函數(shù),會被安排到優(yōu)先級為 6 的 .initcall6.init 這個 “候車室” 里,等待內(nèi)核按部就班地來 “檢票上車”,完成初始化流程。
在內(nèi)核啟動流程的 init/main.c 文件中,有一個至關(guān)重要的函數(shù) do_initcalls,它就是那位掌控全局的 “指揮官”。當(dāng)內(nèi)核啟動進(jìn)入這個環(huán)節(jié),do_initcalls 函數(shù)開始施展它的 “魔法”。它會依據(jù)預(yù)先設(shè)定好的優(yōu)先級順序,如同一位嚴(yán)謹(jǐn)?shù)牧熊囌{(diào)度員,依次 “調(diào)度” 各個等級的初始化函數(shù)。從早期初始化的 early_initcall 開始,逐步到后續(xù)各級別的 initcall,逐個檢查每個優(yōu)先級對應(yīng)的代碼段,一旦發(fā)現(xiàn)有初始化函數(shù)指針 “候車”,就立即調(diào)用執(zhí)行,確保驅(qū)動們有序地完成初始化,為系統(tǒng)正常運行搭建好堅實的基礎(chǔ)。
2.3實現(xiàn)原理
總體來說,initcall是基于以下思路設(shè)計出來的:
- 在生成vmlinux的鏈接階段為initcall創(chuàng)建特定的section
- 開發(fā)者創(chuàng)建相關(guān)的initcall函數(shù),并使用xxx_initcall聲明為不同類型
- 每一類initcall對應(yīng)一組section
- 遍歷執(zhí)行initcall section中的initcalls
xxx_initcall的定義位于include/linux/init.h中,從這個文件的名字也可以看出xxx_initcall是針對初始化操作的。
#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)
代碼解讀
從上面的宏定義可以發(fā)現(xiàn),所有的xxx_initcall都是基于__define_initcall的,后者的定義位于同一個文件中,通過__define_initcall將各個xxx_initcall統(tǒng)一到一起,基于ID編號鏈接到不同的subsection,在同一個subsection中各個initcall的排序以鏈接的順序為準(zhǔn)。另外,__define_initcall中的ID編號還有另外一個作用,就是防止不同類型的xxx_initcall調(diào)用相同的符號引起編譯錯誤。
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn; \
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
以rockchip_grf_init()為例拆解分析xxx_initcall的實現(xiàn)細(xì)節(jié),如下圖所示,注意,在倒數(shù)第二個框圖內(nèi)可以看出來initcall機(jī)制使用到了GNU編譯工具鏈的屬性。
圖片
執(zhí)行流程,根據(jù)前面的介紹,當(dāng)xxx_initcall被鏈接到目標(biāo)文件后,會生成不同類別的section,包含不同的initcall函數(shù),如下所示:
.initcallearly.init 0000000000000008 __initcall_trace_init_flags_sys_exitearly
.initcall0.init 0000000000000008 __initcall_ipc_ns_init0
.initcall1.init 0000000000000008 __initcall_map_entry_trampoline1
.initcall2.init 0000000000000008 __initcall_bdi_class_init2
.initcall3.init 0000000000000008 __initcall_dma_bus_init3
.initcall4.init 0000000000000008 __initcall_fbmem_init4
.initcall5.init 0000000000000008 __initcall_chr_dev_init5
.initcall6.init 0000000000000008 __initcall_hwrng_modinit6
.initcall7.init 0000000000000008 __initcall_deferred_probe_initcall7
.initcallrootfs.init 0000000000000008 __initcall_populate_rootfsrootfs
同一類的initcall執(zhí)行順序由編譯順序決定,不同類的initcall執(zhí)行順序在init/main.c中定義,如下所示:
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
在實際執(zhí)行時,內(nèi)核必須知道xxx_initcall section所在的位置,而在include/asm-generic/vmlinux.lds.h中將xxx_start和.initcall*.init鏈接到了一起,這樣的話,do_initcalls()遍歷不同ID的initcall時,基于xxx_start便可以找到想對應(yīng)的.initcall entry,然后循環(huán)遍歷里面的各個initcalls。
#define INIT_CALLS_LEVEL(level) \
VMLINUX_SYMBOL(__initcall##level##_start) = .; \
*(.initcall##level##.init) \
*(.initcall##level##s.init) \
#define INIT_CALLS \
VMLINUX_SYMBOL(__initcall_start) = .; \
*(.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) = .;
在arch/arm64/kernel/vmlinux.lds中可以看到initcall的符號排布如下圖所示,基于*_start可以定位到各個initcall函數(shù)所對應(yīng)的符號。
圖片
基于以上分析,整理出initcalls的完整執(zhí)行流程如下:
圖片
三、Initcall優(yōu)先級的奧秘
3.1分級規(guī)則
initcall 機(jī)制里,函數(shù)的優(yōu)先級可是 “暗藏玄機(jī)”。它一共劃分為 8 個等級,從 0 到 7,數(shù)字越小,優(yōu)先級越高,執(zhí)行順序也就越早。像 pure_initcall 對應(yīng)的優(yōu)先級是 0,意味著它會在內(nèi)核啟動的早期,搶在很多初始化任務(wù)之前被調(diào)用,適用于那些沒有復(fù)雜依賴、純粹進(jìn)行變量初始化的函數(shù),能快速完成一些基礎(chǔ)準(zhǔn)備工作;而 late_initcall 優(yōu)先級為 7,屬于 “慢性子”,要等到系統(tǒng)大部分關(guān)鍵初始化都完成,快接近穩(wěn)定運行狀態(tài)時才登場,通常用來處理一些對啟動順序不太敏感、可以稍后進(jìn)行的輔助性初始化,避免過早執(zhí)行影響系統(tǒng)前期關(guān)鍵流程。
其中,還有些特殊標(biāo)記,比如 arch_initcall 里的 “arch”,表明和硬件架構(gòu)緊密相關(guān),這類初始化函數(shù)在系統(tǒng)啟動初期,硬件初始化階段就會被調(diào)用,確保硬件能快速進(jìn)入可用狀態(tài),為后續(xù)驅(qū)動和軟件運行搭好硬件 “舞臺”;rootfs_initcall 涉及根文件系統(tǒng)相關(guān)初始化,它的優(yōu)先級介于 5 和 6 之間,在文件系統(tǒng)相關(guān)的初始化流程里找準(zhǔn)時機(jī)切入,保障文件系統(tǒng)布局、掛載等操作有序完成,讓系統(tǒng)能順利讀寫文件,為各種應(yīng)用程序和服務(wù)提供數(shù)據(jù)存儲 “根基”。
而且,帶 “sync” 后綴的,像 core_initcall_sync 相較于 core_initcall,多了同步操作的意味。它會在執(zhí)行完前一級初始化后,等待一些關(guān)鍵條件達(dá)成或資源準(zhǔn)備好,才繼續(xù)后續(xù)操作,保證系統(tǒng)狀態(tài)的一致性,避免因異步執(zhí)行可能帶來的資源競爭、數(shù)據(jù)不一致等隱患,讓初始化流程更加穩(wěn)健。
3.2實戰(zhàn)運用
假設(shè)我們現(xiàn)在有三個驅(qū)動:i2c_driver、video_driver 和 audio_driver。
i2c_driver 負(fù)責(zé)管理系統(tǒng)中的 I2C 總線設(shè)備,它需要在系統(tǒng)啟動早期就完成初始化,以便后續(xù)掛載在 I2C 總線上的各類傳感器、控制器等設(shè)備能及時被識別和配置,那我們就可以使用 arch_initcall(i2c_driver_init),把它的初始化函數(shù)優(yōu)先級設(shè)高,確保硬件層面的通信基礎(chǔ)盡早搭建好。
video_driver 用于驅(qū)動顯卡,讓顯示器能正常輸出圖像,但它依賴一些內(nèi)核子系統(tǒng)的基本框架搭建完成,比如內(nèi)存管理子系統(tǒng)要先準(zhǔn)備好顯存分配的機(jī)制,此時使用 subsys_initcall(video_driver_init) 較為合適,在子系統(tǒng)初始化中期階段介入,與依賴的子系統(tǒng)協(xié)同初始化,保障視頻輸出功能順利啟用。
audio_driver 相對來說,對啟動及時性要求沒那么高,只要在系統(tǒng)快要進(jìn)入用戶交互階段,能正常播放聲音即可,所以采用 late_initcall(audio_driver_init),放在較晚的優(yōu)先級,避免過早初始化占用資源,還可能因其他關(guān)鍵系統(tǒng)組件未就緒而出現(xiàn)異常,確保音頻服務(wù)在合適的時候 “低調(diào)登場”。
當(dāng)內(nèi)核啟動,執(zhí)行到 do_initcalls 函數(shù)時,就會按照 arch_initcall、subsys_initcall、late_initcall 的優(yōu)先級順序,依次檢查對應(yīng)的代碼段。先找到存放 i2c_driver_init 函數(shù)指針的 .initcall3.init 段(假設(shè) arch_initcall 對應(yīng) 3,實際依內(nèi)核版本和架構(gòu)而定),執(zhí)行 i2c_driver_init;接著在輪到 subsys_initcall 優(yōu)先級時,從 .initcall4.init 段調(diào)用 video_driver_init;最后在其他大部分初始化都收尾時,從 .initcall7.init 段執(zhí)行 audio_driver_init,有條不紊地讓各個驅(qū)動在恰當(dāng)?shù)臅r機(jī) “閃亮登場”,開啟各自的使命,保障系統(tǒng)從啟動到穩(wěn)定運行的每一步都穩(wěn)穩(wěn)當(dāng)當(dāng)。
3.3優(yōu)勢盡顯
initcall機(jī)制給 Linux 系統(tǒng)帶來的好處那可真是數(shù)不勝數(shù)。從開發(fā)的便利性來講,它就像是一位貼心的助手,大大簡化了驅(qū)動開發(fā)者的工作。以往那種繁瑣、易錯的手動添加驅(qū)動初始化函數(shù)調(diào)用的方式一去不復(fù)返?,F(xiàn)在,開發(fā)者只需輕松使用內(nèi)核提供的對應(yīng)宏,比如 module_init、arch_initcall 等,就能把驅(qū)動初始化函數(shù)妥妥地交給 initcall 機(jī)制 “托管”,編譯器會自動完成后續(xù)復(fù)雜的整理、存放工作,開發(fā)者無需再為函數(shù)調(diào)用順序、存放位置這些瑣碎細(xì)節(jié)煩惱,得以將更多精力聚焦在驅(qū)動核心功能的實現(xiàn)上,開發(fā)效率直線飆升。
在內(nèi)存管理方面,initcall 機(jī)制更是展現(xiàn)出了 “節(jié)約標(biāo)兵” 的特質(zhì)。要知道,內(nèi)核啟動時,那些用于初始化的代碼在完成使命后,若還一直占用寶貴的內(nèi)存空間,無疑是一種極大的浪費。而 initcall 機(jī)制巧妙地將初始化函數(shù)存放在特定的代碼段,待系統(tǒng)啟動,相關(guān)初始化工作順利結(jié)束,內(nèi)存管理器就能迅速回收這些代碼段占用的內(nèi)存,將其 “變廢為寶”,重新分配給系統(tǒng)后續(xù)運行中更急需的任務(wù),讓內(nèi)存資源得到高效利用,保障系統(tǒng)整體運行的流暢性。
對于系統(tǒng)穩(wěn)定性,initcall 機(jī)制更是筑起了一道堅固的 “防線”。它精心設(shè)計的優(yōu)先級體系,確保各個驅(qū)動、子系統(tǒng)嚴(yán)格按照合理的順序初始化。這就有效避免了因初始化順序混亂,可能導(dǎo)致的資源競爭沖突,比如兩個驅(qū)動同時爭搶同一硬件資源,造成系統(tǒng)死機(jī);或者數(shù)據(jù)依賴關(guān)系出錯,像某個驅(qū)動初始化時需要依賴另一個尚未初始化完成的子系統(tǒng)提供的數(shù)據(jù),進(jìn)而引發(fā)系統(tǒng)崩潰等嚴(yán)重問題,讓 Linux 系統(tǒng)在啟動和運行過程中穩(wěn)如泰山。
四、Initcall機(jī)制在Linux內(nèi)核中的實現(xiàn)
Linux 內(nèi)核提供了一組來自頭文件 include/linux/init.h 的宏,來標(biāo)記給定的函數(shù)為 initcall
。所有這些宏都相當(dāng)簡單:
#define early_initcall(fn) __define_initcall(fn, early)
#define core_initcall(fn) __define_initcall(fn, 1)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define device_initcall(fn) __define_initcall(fn, 6)
#define late_initcall(fn) __define_initcall(fn, 7)
我們可以看到,這些宏只是從同一個頭文件的 __define_initcall 宏的調(diào)用擴(kuò)展而來。此外,__define_initcall 宏有兩個參數(shù):
- fn - 在調(diào)用某個級別 initcalls 時調(diào)用的回調(diào)函數(shù);
- id - 識別 initcall 的標(biāo)識符,用來防止兩個相同的 initcalls 指向同一個處理函數(shù)時出現(xiàn)錯誤。
__define_initcall 宏的實現(xiàn)如下所示:
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn; \
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
要了解 __define_initcall 宏,首先讓我們來看下 initcall_t 類型。這個類型定義在同一個 頭文件 中,它表示一個返回 整形指針的函數(shù)指針,這將是 initcall 的結(jié)果:
typedef int (*initcall_t)(void);
現(xiàn)在讓我們回到 _-define_initcall 宏。## 提供了連接兩個符號的能力。在我們的例子中,__define_initcall 宏的第一行產(chǎn)生了 .initcall id .init ELF 部分 給定函數(shù)的定義,并標(biāo)記以下 gcc 屬性:__initcall_function_name_id 和 __used。如果我們查看表示內(nèi)核鏈接腳本數(shù)據(jù)的 include/asm-generic/vmlinux.lds.h 頭文件,我們會看到所有的 initcalls 部分都將放在 .data 段:
#define INIT_CALLS \
VMLINUX_SYMBOL(__initcall_start) = .; \
*(.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) = .;
#define INIT_DATA_SECTION(initsetup_align) \
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \
... \
INIT_CALLS \
... \
}
第二個屬性 -__used,定義在include/linux/compiler-gcc.h頭文件中,它擴(kuò)展了以下gcc定義:
#define __used __attribute__((__used__))
它防止 定義了變量但未使用 的告警。宏 __define_initcall 最后一行是:
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
這取決于 CONFIG_LTO 內(nèi)核配置選項,只為編譯器提供鏈接時間優(yōu)化存根:
#ifdef CONFIG_LTO
#define LTO_REFERENCE_INITCALL(x) \
static __used __exit void *reference_##x(void) \
{ \
return &x; \
}
#else
#define LTO_REFERENCE_INITCALL(x)
#endif
為了防止當(dāng)模塊中的變量沒有引用時而產(chǎn)生的任何問題,它被移到了程序末尾。這就是關(guān)于 __define_initcall 宏的全部了。所以,所有的 *_initcall 宏將會在Linux內(nèi)核編譯時擴(kuò)展,所有的 initcalls 會放置在它們的段內(nèi),并可以通過 .data 段來獲取,Linux 內(nèi)核在初始化過程中就知道在哪兒去找到 initcall 并調(diào)用它。
既然 Linux 內(nèi)核可以調(diào)用 initcalls,我們就來看下 Linux 內(nèi)核是如何做的。這個過程從 init/main.c 頭文件的 do_basic_setup 函數(shù)開始:
static void __init do_basic_setup(void)
{
...
...
...
do_initcalls();
...
...
...
}
該函數(shù)在 Linux 內(nèi)核初始化過程中調(diào)用,調(diào)用時機(jī)是主要的初始化步驟,比如內(nèi)存管理器相關(guān)的初始化、CPU子系統(tǒng)等完成之后。do_initcalls函數(shù)只是遍歷initcall級別數(shù)組,并調(diào)用每個級別的do_initcall_level函數(shù):
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
initcall_levels數(shù)組在同一個源碼文件中定義,包含了定義在__define_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,
};
如果你有興趣,你可以在 Linux 內(nèi)核編譯后生成的鏈接器腳本arch/x86/kernel/vmlinux.lds
中找到這些段:
.init.data : AT(ADDR(.init.data) - 0xffffffff80000000) {
...
...
...
...
__initcall_start = .;
*(.initcallearly.init)
__initcall0_start = .;
*(.initcall0.init)
*(.initcall0s.init)
__initcall1_start = .;
...
...
}
如果你對這些不熟,可以在本書的某些部分了解更多關(guān)于鏈接器的信息。
正如我們剛看到的,do_initcall_level 函數(shù)有一個參數(shù) - initcall 的級別,做了以下兩件事:首先這個函數(shù)拷貝了 initcall_command_line,這是通常內(nèi)核包含了各個模塊參數(shù)的命令行的副本,并用 kernel/params.c源碼文件的 parse_args 函數(shù)解析它,然后調(diào)用各個級別的 do_on_initcall 函數(shù):
for (fn = initcall_levels[level];
fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
do_on_initcall為我們做了主要的工作。我們可以看到,這個函數(shù)有一個參數(shù)表示initcall回調(diào)函數(shù),并調(diào)用給定的回調(diào)函數(shù):
int __init_or_module do_one_initcall(initcall_t fn)
{
int count = preempt_count();
int ret;
char msgbuf[64];
if (initcall_blacklisted(fn))
return -EPERM;
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_set(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_on_initcall函數(shù)做了什么。首先我們增加preemption計數(shù),以便我們稍后進(jìn)行檢查,確保它不是不平衡的。這步以后,我們可以看到initcall_backlist函數(shù)的調(diào)用,這個函數(shù)遍歷包含了initcalls黑名單的blacklisted_initcalls鏈表,如果initcall在黑名單里就釋放它:
list_for_each_entry(entry, &blacklisted_initcalls, next) {
if (!strcmp(fn_name, entry->buf)) {
pr_debug("initcall %s blacklisted\n", fn_name);
kfree(fn_name);
return true;
}
}
黑名單的 initcalls 保存在 blacklisted_initcalls 鏈表中,這個鏈表是在早期 Linux 內(nèi)核初始化時由 Linux 內(nèi)核命令行來填充的。處理完進(jìn)入黑名單的 initcalls,接下來的代碼直接調(diào)用 initcall:
if (initcall_debug)
ret = do_one_initcall_debug(fn);
else
ret = fn();
取決于 initcall_debug 變量的值,do_one_initcall_debug 函數(shù)將調(diào)用 initcall,或直接調(diào)用 fn()。initcall_debug 變量定義在同一個源碼文件:
bool initcall_debug;
該變量提供了向內(nèi)核日志緩沖區(qū)打印一些信息的能力??梢酝ㄟ^ initcall_debug
參數(shù)從內(nèi)核命令行中設(shè)置這個變量的值。從Linux內(nèi)核命令行文檔可以看到:
initcall_debug [KNL] Trace initcalls as they are executed. Useful
for working out where the kernel is dying during
startup.
確實如此。如果我們看下 do_one_initcall_debug 函數(shù)的實現(xiàn),我們會看到它與 do_one_initcall 函數(shù)做了一樣的事,也就是說,do_one_initcall_debug 函數(shù)調(diào)用了給定的 initcall,并打印了一些和 initcall 相關(guān)的信息(比如當(dāng)前任務(wù)的 pid、initcall 的持續(xù)時間等):
static int __init_or_module do_one_initcall_debug(initcall_t fn)
{
ktime_t calltime, delta, rettime;
unsigned long long duration;
int ret;
printk(KERN_DEBUG "calling %pF @ %i\n", fn, task_pid_nr(current));
calltime = ktime_get();
ret = fn();
rettime = ktime_get();
delta = ktime_sub(rettime, calltime);
duration = (unsigned long long) ktime_to_ns(delta) >> 10;
printk(KERN_DEBUG "initcall %pF returned %d after %lld usecs\n",
fn, ret, duration);
return ret;
}
由于initcall被do_one_initcall或do_one_initcall_debug調(diào)用,我們可以看到在do_one_initcall函數(shù)末尾做了兩次檢查。第一個檢查在initcall執(zhí)行內(nèi)部__preempt_count_add和__preempt_count_sub可能的執(zhí)行次數(shù),如果這個值和之前的可搶占計數(shù)不相等,我們就把preemption imbalance字符串添加到消息緩沖區(qū),并設(shè)置正確的可搶占計數(shù):
if (preempt_count() != count) {
sprintf(msgbuf, "preemption imbalance ");
preempt_count_set(count);
}
稍后這個錯誤字符串就會被打印出來。最后檢查本地IRQs的狀態(tài),如果它們被禁用了,我們就將disabled interrupts字符串添加到我們的消息緩沖區(qū),并為當(dāng)前處理器使能IRQs,以防出現(xiàn)IRQs被initcall禁用了但不再使能的情況出現(xiàn):
if (irqs_disabled()) {
strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
local_irq_enable();
}
這就是全部了。通過這種方式,Linux 內(nèi)核以正確的順序完成了很多子系統(tǒng)的初始化?,F(xiàn)在我們知道 Linux 內(nèi)核的 initcall 機(jī)制是怎么回事了。在這部分中,我們介紹了 initcall 機(jī)制的主要部分,但遺留了一些重要的概念。讓我們來簡單看下這些概念。
首先,我們錯過了一個級別的 initcalls,就是 rootfs initcalls。和我們在本部分看到的很多宏類似,你可以在 include/linux/init.h 頭文件中找到 rootfs_initcall 的定義:
#define rootfs_initcall(fn)
__define_initcall(fn, rootfs)
從這個宏的名字我們可以理解到,它的主要目的是保存和 rootfs 相關(guān)的回調(diào)。除此之外,只有在與設(shè)備相關(guān)的東西沒被初始化時,在文件系統(tǒng)級別初始化以后再初始化一些其它東西時才有用。例如,發(fā)生在源碼文件 init/initramfs.c 中 populate_rootfs 函數(shù)里的解壓 initramfs:
rootfs_initcall(populate_rootfs);
在這里,我們可以看到熟悉的輸出:
[ 0.199960] Unpacking initramfs...
除了 rootfs_initcall 級別,還有其它的 console_initcall、 security_initcall 和其他輔助的 initcall 級別。我們遺漏的最后一件事,是 *_initcall_sync 級別的集合。在這部分我們看到的幾乎每個 *_initcall 宏,都有 _sync 前綴的宏伴隨:
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
這些附加級別的主要目的是,等待所有某個級別的與模塊相關(guān)的初始化例程完成。