作者:良知猶存,轉(zhuǎn)載授權(quán)以及圍觀:歡迎關(guān)注微信公眾號:羽林君
linux以及嵌入式一些代碼,我們看到core_initcall、device_initcall等等需要鏈接器分配各個section,并且在啟動該模塊時候執(zhí)行。下面我們詳細追溯一下執(zhí)行過程。
fs_initcall函數(shù)介紹:
Linux內(nèi)核中的fs_initcall
函數(shù):用于在引導(dǎo)過程中進行文件系統(tǒng)等初始化。
初始化注冊:當(dāng)文件系統(tǒng)模塊被加載時,它使用fs_initcall
宏注冊其初始化函數(shù)。該宏將初始化函數(shù)添加到__initcall_fs部分。
內(nèi)核引導(dǎo)過程:在引導(dǎo)過程中,在基本硬件初始化和內(nèi)存設(shè)置之后,內(nèi)核開始執(zhí)行初始化函數(shù)。
執(zhí)行fs_initcall函數(shù):fs_initcall
函數(shù)按照其注冊順序依次執(zhí)行。這些函數(shù)初始化各種文件系統(tǒng)并執(zhí)行必要的設(shè)置任務(wù)。
文件系統(tǒng)初始化:每個fs_initcall函數(shù)負責(zé)設(shè)置和初始化特定的內(nèi)容。這可能涉及初始化數(shù)據(jù)結(jié)構(gòu)、注冊文件系統(tǒng)類型、準(zhǔn)備緩存和其他相關(guān)任務(wù)。
完成和交接:一旦所有fs_initcall
函數(shù)都執(zhí)行完畢,內(nèi)核會繼續(xù)完成引導(dǎo)過程,包括啟動用戶空間和初始化設(shè)備。
下面是一個簡單的示例代碼,展示了fs_initcall
函數(shù)的使用和文件系統(tǒng)初始化的過程:
#include?<linux/init.h>
#include?<linux/module.h>
static?int?__init?my_filesystem_init(void)?{
????//?執(zhí)行文件系統(tǒng)特定的初始化任務(wù)
????printk(KERN_INFO?"My?Filesystem:?Initializingn");
????//?其他初始化操作...
????return?0;
}
fs_initcall(my_filesystem_init);
MODULE_LICENSE("GPL");
這其中my_filesystem_init
函數(shù)被注冊為fs_initcall
函數(shù)。當(dāng)模塊加載時,該初始化函數(shù)將被執(zhí)行,完成特定文件系統(tǒng)的初始化任務(wù)。實際的文件系統(tǒng)模塊會包含更多復(fù)雜的初始化邏輯,這個例子只是用來展示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)的,它們共同構(gòu)成了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)核版本和架構(gòu)而異,但通??梢栽趦?nèi)核源代碼的arch/<architecture>/kernel/vmlinux.lds.S
或類似的文件中找到相關(guān)的鏈接腳本定義。在這些文件中,我們可以看到下面的內(nèi)容:
.initcall.init?:?{
????INIT_CALLS
}
文件位置:linux-xxxarcharmkernelvmlinux.lds.S
文件位置:linux-xxxincludeasm-genericvmlinux.lds.h
:
???????????__initcall_start?=?.;
???????????.initcall.init?:?{
?????????????????*(.initcall1.init)
????????????????...
?????????????????*(.initcall7.init)
?????????????????}
???????????__initcall_end?=?.;
上面是INIT_CALLS對應(yīng)的函數(shù),*(.initcall##level##.init)
,這個函數(shù)就對應(yīng)了__define_initcall
宏里面的__section__(".initcall" #id ".init")
,繼續(xù)查看fs_initcall
,對應(yīng)的level就是這個部分__define_initcall(fn, 5)
的5。
上述示例中的INIT_CALLS
通常會包含對.initcall
節(jié)(section)的定義,規(guī)定了將哪些符號放入該節(jié)中。這些定義可能會隨著不同的內(nèi)核版本和架構(gòu)而有所不同,但其基本思想是相似的:將初始化函數(shù)的地址放入特定的節(jié)(section)中,以便在啟動時按順序執(zhí)行這些函數(shù)。
__define_initcall
這個宏也是可以設(shè)置多個初始化函數(shù),并將它們放置在不同的.initcall
節(jié)(section)中。
假設(shè)我們有兩個初始化函數(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è)置多個函數(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)用。
通過使用帶有不同標(biā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")))
,這將導(dǎo)致這些函數(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ā)者設(shè)置好
在一般情況下,.initcall.init
這樣的特殊節(jié)(section)的大小是由鏈接器自動分配的,而不是由開發(fā)者手動設(shè)置的。當(dāng)鏈接器處理可執(zhí)行文件時,它會根據(jù)各個節(jié)(section)中的內(nèi)容以及鏈接腳本中的規(guī)則來確定每個節(jié)(section)的大小和排布。
對于.initcall.init
節(jié)(section),其大小將取決于其中包含的初始化函數(shù)的數(shù)量和大小。鏈接器會根據(jù)這些函數(shù)的地址和大小來動態(tài)地分配空間,以便容納所有的初始化函數(shù)。
因此,開發(fā)者通常無需手動設(shè)置.initcall.init
節(jié)(section)的大小。相反,鏈接器會根據(jù)實際情況自動進行分配,確保所有的初始化函數(shù)都能被正確地安置在這個特定的節(jié)(section)中,并且在內(nèi)核啟動時按照順序被調(diào)用。
如何自己設(shè)置section的大小
在一般情況下,開發(fā)者通常不需要手動設(shè)置節(jié)(section)的大小。鏈接器會根據(jù)鏈接腳本中的規(guī)則和可執(zhí)行文件中各個部分的大小自動進行分配。
如果我們有特殊需求,希望手動設(shè)置某個節(jié)(section)的大小,可以通過鏈接腳本來實現(xiàn)。在鏈接腳本中,我們可以定義節(jié)(section)的起始位置、大小以及其他屬性。
以下簡單的模板,在鏈接腳本中手動設(shè)置一個名為.my_section
的節(jié)(section)的大小:
.my_section : {
/* 定義節(jié)(section)的起始位置 */
start = .;
/* 設(shè)置節(jié)(section)的大小為固定值(例如0x1000)*/
input_section(.text);
input_section(.data);
/* 其他內(nèi)容... */
end = .;
} > RAM
在這個示例中,.my_section
節(jié)(section)被手動設(shè)置為包含.text
和.data
節(jié)(section)的內(nèi)容,并且其大小被設(shè)置為固定值。當(dāng)鏈接器處理可執(zhí)行文件時,它將按照這些規(guī)則來分配空間并確定這個特定節(jié)(section)的大小。
需要注意的是,手動設(shè)置節(jié)(section)的大小可能需要對鏈接腳本和鏈接過程有深入的了解,因此在大多數(shù)情況下,開發(fā)者無需手動設(shè)置節(jié)(section)的大小,而是依賴于鏈接器自動進行分配。
這個是我實際應(yīng)用的一款芯片的鏈接修改:
FUN?0x400?(0x10000-0x400)
{
????;cpu.o?(+RO)
????xlib.a?(+RO)
}
上面這部分我使用的鏈接腳本中一部分內(nèi)容。為FUN
的節(jié)(section)中,它的起始地址為0x10000
,大小為0x400
。
在這個節(jié)(section)中包含了兩個文件:cpu.o
和 xlib.a
,它們都被標(biāo)記為只讀(Read-Only)。鏈接器會將這兩個文件的只讀部分放置在由FUN
定義的地址范圍內(nèi)。
鏈接腳本用于指導(dǎo)鏈接器如何組織可執(zhí)行文件的各個部分,包括節(jié)(section)的排布和屬性。這個片段也是屬于鏈接腳本的一部分,這個里面鏈接器會將cpu.o
和xlib.a
的只讀部分放置在從0x10000
開始、大小為0x400
的范圍內(nèi)。
只是一個demo示例,如果進一步操作這個鏈接腳本,我們要參考特定的鏈接器文檔以及相關(guān)的目標(biāo)平臺和工具鏈的文檔,以確保正確地設(shè)置節(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)的基本設(shè)置。
-
- 在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)用,來完成相應(yīng)初始化。而函數(shù)指針放置到的子section由宏定義的level確定,對應(yīng)level較小的子section位于較前面。而位于同一個子section內(nèi)的函數(shù)指針順序不定,將由編譯器按照編譯的順序隨機指定。同理,如果我們想先執(zhí)行一些定義的函數(shù),那就可以把它們放置于level比較小的定義中。
結(jié)語
這就是我自己對于linux內(nèi)核initcall放置在各個section中函數(shù)執(zhí)行流程的一些分享。如果大家有更好的想法,也歡迎大家加我好友交流。