自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Linux內(nèi)核initcall機(jī)制:驅(qū)動初始化的幕后英雄

開發(fā) 前端
當(dāng)系統(tǒng)啟動的號角吹響,眾多硬件驅(qū)動就像等待檢閱的士兵,急切地需要按恰當(dāng)順序、在精準(zhǔn)時機(jī)完成初始化,才能確保整個系統(tǒng)順利啟航。要是沒有 initcall 機(jī)制,這亂糟糟的局面簡直不敢想象,可能系統(tǒng)還沒 “起跑” 就陷入崩潰泥潭。

你想想,咱們每天使用的電腦、手機(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)的初始化例程完成。

責(zé)任編輯:武曉燕 來源: 深度Linux
相關(guān)推薦

2022-01-29 22:27:31

內(nèi)核子線程應(yīng)用

2015-06-09 17:14:11

容器LinuxLinux原子主機(jī)

2023-11-17 08:52:26

2013-11-11 16:15:15

云計算

2012-08-07 08:55:40

2012-05-22 10:10:41

2013-11-14 09:41:30

2018-09-13 10:37:54

思科幕后培訓(xùn)

2010-05-25 09:35:28

智能通信移動

2013-07-09 10:12:27

2013-09-18 15:20:49

用友U8

2014-03-19 15:05:14

數(shù)據(jù)中心蘋果

2014-08-08 11:21:15

浪潮GSP+大數(shù)據(jù)

2018-01-19 10:12:03

UCloudAI競賽人工智能

2015-09-09 09:10:58

項目英雄開源

2021-01-26 09:14:19

Linux內(nèi)核模塊

2015-09-28 18:44:25

容聯(lián)云通訊

2012-05-30 15:15:13

2025-01-06 23:33:04

點贊
收藏

51CTO技術(shù)棧公眾號