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