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