Linux內(nèi)核中的fs_initcall函數(shù):用于在引導過程中進行文件系統(tǒng)等初始化。
-
初始化注冊:
- 當文件系統(tǒng)模塊被加載時,它使用
fs_initcall宏注冊其初始化函數(shù)。 - 該宏將初始化函數(shù)添加到
__initcall_fs部分。
- 當文件系統(tǒng)模塊被加載時,它使用
-
內(nèi)核引導過程:
- 在引導過程中,在基本硬件初始化和內(nèi)存設置之后,內(nèi)核開始執(zhí)行初始化函數(shù)。
-
執(zhí)行fs_initcall函數(shù):
fs_initcall函數(shù)按照其注冊順序依次執(zhí)行。- 這些函數(shù)初始化各種文件系統(tǒng)并執(zhí)行必要的設置任務。
-
文件系統(tǒng)初始化:
- 每個
fs_initcall函數(shù)負責設置和初始化特定的內(nèi)容。 - 這可能涉及初始化數(shù)據(jù)結構、注冊文件系統(tǒng)類型、準備緩存和其他相關任務。
- 每個
-
完成和交接:
- 一旦所有
fs_initcall函數(shù)都執(zhí)行完畢,內(nèi)核會繼續(xù)完成引導過程,包括啟動用戶空間和初始化設備。
- 一旦所有
下面是一個簡單的示例代碼,展示了fs_initcall函數(shù)的使用和文件系統(tǒng)初始化的過程:
#include <linux/init.h>
#include <linux/module.h>
static int __init my_filesystem_init(void) {
// 執(zhí)行文件系統(tǒng)特定的初始化任務
printk(KERN_INFO "My Filesystem: Initializing\n");
// 其他初始化操作...
return 0;
}
fs_initcall(my_filesystem_init);
MODULE_LICENSE("GPL");
這其中my_filesystem_init函數(shù)被注冊為fs_initcall函數(shù)。當模塊加載時,該初始化函數(shù)將被執(zhí)行,完成特定文件系統(tǒng)的初始化任務。實際的文件系統(tǒng)模塊會包含更多復雜的初始化邏輯,這個例子只是用來展示fs_initcall函數(shù)的基本用法。
fs_initcall函數(shù)調(diào)用的層次:
在Linux內(nèi)核中,fs_initcall宏實際上是通過__define_initcall來定義的。下面是__define_initcall的定義:
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn
這段代碼展示了__define_initcall的定義方式。在這里,__define_initcall宏創(chuàng)建了一個靜態(tài)的initcall_t類型變量,并將其放置在特定的.initcall節(jié)(section)中。這樣,在內(nèi)核初始化時,這些函數(shù)就會按照其在源代碼中出現(xiàn)的順序被依次調(diào)用。
fs_initcall實際上是通過__define_initcall宏來實現(xiàn)的,它們共同構成了Linux內(nèi)核中初始化調(diào)用機制的一部分。
fs_initcall函數(shù)被放置的section的位置
在Linux內(nèi)核中,.initcall節(jié)(section)是通過鏈接腳本(linker script)定義的。鏈接腳本指定了可執(zhí)行文件的內(nèi)存布局,包括代碼、數(shù)據(jù)和其他段的放置位置。
對于.initcall節(jié)(section),它通常由鏈接腳本中的一些規(guī)則來定義。這個節(jié)用于存放初始化函數(shù)的地址,以便在內(nèi)核啟動時按照順序執(zhí)行這些初始化函數(shù)。
具體的定義可能會因內(nèi)核版本和架構而異,但通常可以在內(nèi)核源代碼的arch/<architecture>/kernel/vmlinux.lds.S或類似的文件中找到相關的鏈接腳本定義。在這些文件中,我們可以看到下面的內(nèi)容:
.initcall.init : {
INIT_CALLS
}
文件位置:\linux-xxx\arch\arm\kernel\vmlinux.lds.S
文件位置:\linux-xxx\include\asm-generic\vmlinux.lds.h
:
__initcall_start = .;
.initcall.init : {
*(.initcall1.init)
...
*(.initcall7.init)
}
__initcall_end = .;
上面是INIT_CALLS對應的函數(shù),*(.initcall##level##.init),這個函數(shù)就對應了__define_initcall宏里面的__section__(".initcall" #id ".init"),繼續(xù)查看fs_initcall,對應的level就是這個部分__define_initcall(fn, 5)的5。
上述示例中的INIT_CALLS通常會包含對.initcall節(jié)(section)的定義,規(guī)定了將哪些符號放入該節(jié)中。這些定義可能會隨著不同的內(nèi)核版本和架構而有所不同,但其基本思想是相似的:將初始化函數(shù)的地址放入特定的節(jié)(section)中,以便在啟動時按順序執(zhí)行這些函數(shù)。
__define_initcall這個宏也是可以設置多個初始化函數(shù),并將它們放置在不同的.initcall節(jié)(section)中。
假設我們有兩個初始化函數(shù):init_function_1和init_function_2,我們可以使用上述宏定義來將它們分別放置在不同的.initcall節(jié)(section)中。
// 定義多個初始化函數(shù)
static void __init init_function_1(void) {
// 初始化函數(shù)1的內(nèi)容
}
static void __init init_function_2(void) {
// 初始化函數(shù)2的內(nèi)容
}
// 使用 __define_initcall 宏定義來設置多個函數(shù)
__define_initcall(init_function_1, 1);
__define_initcall(init_function_2, 2);
在這個例子中,init_function_1被放置在.initcall1.init節(jié)(section)中,而init_function_2則被放置在.initcall2.init節(jié)(section)中。這樣,在內(nèi)核啟動時,這些函數(shù)就會按照其在源文件中出現(xiàn)的順序依次被調(diào)用。
通過使用帶有不同標識符的宏定義,可以將多個初始化函數(shù)放置在不同的.initcall節(jié)(section)中,從而實現(xiàn)按順序執(zhí)行多個初始化函數(shù)的目的。
以af_inet.c里面的fs_initcall(inet_init);fs_initcall(ipv4_offload_init);介紹放置的情況: 怎么在section放置的
在這個例子中,fs_initcall宏用于將inet_init和ipv4_offload_init函數(shù)放置在.initcall.init節(jié)(section)中。這樣,在內(nèi)核啟動時,這些函數(shù)就會按照其在源文件中出現(xiàn)的順序依次被調(diào)用。
下面是簡化版本的代碼:
// 定義要初始化的函數(shù)
static void __init inet_init(void) {
// inet_init的初始化內(nèi)容
}
static void __init ipv4_offload_init(void) {
// ipv4_offload_init的初始化內(nèi)容
}
// 使用 fs_initcall 宏將函數(shù)放置在 .initcall.init 節(jié)(section)中
fs_initcall(inet_init);
fs_initcall(ipv4_offload_init);
上述代碼,inet_init和ipv4_offload_init函數(shù)會被放置在.initcall.init節(jié)(section)中,以便在內(nèi)核啟動時按照其在源文件中出現(xiàn)的順序依次被調(diào)用。
通過這個例子,我們明白了如何使用fs_initcall宏將這兩個函數(shù)放置在.initcall.init節(jié)(section)中.
每個section空間排布情況是如何的?
還是上面inet_init和ipv4_offload_init函數(shù)來介紹,由于fs_initcall宏使用了__attribute__((__section__(".initcall.init"))),這將導致這些函數(shù)被放置在.initcall.init節(jié)(section)中。這樣,在鏈接時,這些函數(shù)的地址將按照其在源文件中出現(xiàn)的順序排布在該特定的節(jié)(section)內(nèi)。
這些函數(shù)位于可執(zhí)行文件的內(nèi)存中的某個位置,它們的排布情況如下所示:
|---------------------|
| .text section |
|---------------------|
| ... other sections ... |
|---------------------|
| .initcall.init section |
|---------------------|
| inet_init |
|---------------------|
| ipv4_offload_init |
|---------------------|
| ... other functions ... |
|---------------------|
| .data section |
|---------------------|
| ... other sections ... |
|---------------------|
在這個示例中,.initcall.init節(jié)(section)包含了inet_init和ipv4_offload_init函數(shù),它們會按照它們在源代碼中出現(xiàn)的順序排布在該節(jié)(section)中。這樣,在內(nèi)核啟動時,這些函數(shù)就會按照它們在.initcall.init節(jié)(section)中的排布順序依次被調(diào)用。
這里section的大小是隨機按照大小自動分配還是需要開發(fā)者設置好
在一般情況下,.initcall.init這樣的特殊節(jié)(section)的大小是由鏈接器自動分配的,而不是由開發(fā)者手動設置的。當鏈接器處理可執(zhí)行文件時,它會根據(jù)各個節(jié)(section)中的內(nèi)容以及鏈接腳本中的規(guī)則來確定每個節(jié)(section)的大小和排布。
對于.initcall.init節(jié)(section),其大小將取決于其中包含的初始化函數(shù)的數(shù)量和大小。鏈接器會根據(jù)這些函數(shù)的地址和大小來動態(tài)地分配空間,以便容納所有的初始化函數(shù)。
因此,開發(fā)者通常無需手動設置.initcall.init節(jié)(section)的大小。相反,鏈接器會根據(jù)實際情況自動進行分配,確保所有的初始化函數(shù)都能被正確地安置在這個特定的節(jié)(section)中,并且在內(nèi)核啟動時按照順序被調(diào)用。
如何自己設置section的大小
在一般情況下,開發(fā)者通常不需要手動設置節(jié)(section)的大小。鏈接器會根據(jù)鏈接腳本中的規(guī)則和可執(zhí)行文件中各個部分的大小自動進行分配。
如果我們有特殊需求,希望手動設置某個節(jié)(section)的大小,可以通過鏈接腳本來實現(xiàn)。在鏈接腳本中,我們可以定義節(jié)(section)的起始位置、大小以及其他屬性。
以下簡單的模板,在鏈接腳本中手動設置一個名為.my_section的節(jié)(section)的大小:
.my_section : {
/* 定義節(jié)(section)的起始位置 */
start = .;
/* 設置節(jié)(section)的大小為固定值(例如0x1000)*/
input_section(.text);
input_section(.data);
/* 其他內(nèi)容... */
end = .;
} > RAM
在這個示例中,.my_section節(jié)(section)被手動設置為包含.text和.data節(jié)(section)的內(nèi)容,并且其大小被設置為固定值。當鏈接器處理可執(zhí)行文件時,它將按照這些規(guī)則來分配空間并確定這個特定節(jié)(section)的大小。
需要注意的是,手動設置節(jié)(section)的大小可能需要對鏈接腳本和鏈接過程有深入的了解,因此在大多數(shù)情況下,開發(fā)者無需手動設置節(jié)(section)的大小,而是依賴于鏈接器自動進行分配。
這個是我實際應用的一款芯片的鏈接修改:
FUN 0x400 (0x10000-0x400)
{
;cpu.o (+RO)
xlib.a (+RO)
}
上面這部分我使用的鏈接腳本中一部分內(nèi)容。為FUN的節(jié)(section)中,它的起始地址為0x10000,大小為0x400。
在這個節(jié)(section)中包含了兩個文件:cpu.o 和 xlib.a,它們都被標記為只讀(Read-Only)。鏈接器會將這兩個文件的只讀部分放置在由FUN定義的地址范圍內(nèi)。
鏈接腳本用于指導鏈接器如何組織可執(zhí)行文件的各個部分,包括節(jié)(section)的排布和屬性。這個片段也是屬于鏈接腳本的一部分,這個里面鏈接器會將cpu.o和xlib.a的只讀部分放置在從0x10000開始、大小為0x400的范圍內(nèi)。
只是一個demo示例,如果進一步操作這個鏈接腳本,我們要參考特定的鏈接器文檔以及相關的目標平臺和工具鏈的文檔,以確保正確地設置節(jié)(section)的屬性和排布。芯片之間區(qū)別挺大的。
內(nèi)核執(zhí)行順序是?
介紹完了section片段,我們再來說一下,這些函數(shù)的初始化位置以及執(zhí)行順序。
上面我們介紹了vmlinux.lds.S中的INIT_CALLS就是我們定義好的那些函數(shù),那他們怎么被調(diào)用的呢 在Linux內(nèi)核啟動過程中,INIT_CALLS(包括subsys_initcall,fs_initcall,device_initcall等)會在不同的階段被執(zhí)行。這些初始化調(diào)用是通過鏈接器腳本和特定的內(nèi)核宏來安排的。
具體來說,INIT_CALLS的執(zhí)行時機如下:
- 在內(nèi)核啟動的早期階段,
start_kernel函數(shù)會調(diào)用rest_init。 - 在
rest_init中,會觸發(fā)do_basic_setup函數(shù)的執(zhí)行,其中包括對文件系統(tǒng)的基本設置。
- 在
do_basic_setup函數(shù)中,會調(diào)用do_initcalls函數(shù)。
- 在
do_initcalls函數(shù)中,各種初始化函數(shù)會按照鏈接器腳本中的順序被執(zhí)行。
fs_initcall函數(shù)是其中之一。
看到了執(zhí)行過程,其中是按照各個level進行調(diào)用的,而__define_initcall(level,fn)的作用就是指示編譯器把一些初始化函數(shù)的指針(即:函數(shù)起始地址)按照順序放置一個名為 .initcall.init 的section中,這個section又被分成了n個子section,它們按順序排列。在內(nèi)核初始化階段,這些放置到這個section中的函數(shù)指針將供do_initcalls() 按順序依次調(diào)用,來完成相應初始化。
而函數(shù)指針放置到的子section由宏定義的level確定,對應level較小的子section位于較前面。而位于同一個子section內(nèi)的函數(shù)指針順序不定,將由編譯器按照編譯的順序隨機指定。同理,如果我們想先執(zhí)行一些定義的函數(shù),那就可以把它們放置于level比較小的定義中。
結語
這就是我自己對于linux內(nèi)核initcall放置在各個section中函數(shù)執(zhí)行流程的一些分享。如果大家有更好的想法,也歡迎大家加我好友交流。
作者:良知猶存,白天努力工作,晚上原創(chuàng)公號號主。公眾號內(nèi)容除了技術還有些人生感悟,一個認真輸出內(nèi)容的職場老司機,也是一個技術之外豐富生活的人,攝影、音樂 and 籃球。關注我,與我一起同行。
???????????????? END ????????????????






