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