解析基于ARM64的init用戶進(jìn)程究竟如何啟動?
跳轉(zhuǎn)內(nèi)核前基本準(zhǔn)備
參考./Documentation/arm64/booting.txt
Bootloader至少完成以下基本的初始化準(zhǔn)備:
設(shè)置并初始化RAM(必須),引導(dǎo)加載程序應(yīng)找到并初始化內(nèi)核將用于系統(tǒng)中易失性數(shù)據(jù)存儲的所有RAM。它以機(jī)器相關(guān)的方式執(zhí)行此操作。(它可以使用內(nèi)部算法來自動定位和調(diào)整所有RAM的大小,或者可以使用機(jī)器中RAM的知識或引導(dǎo)加載程序設(shè)計(jì)者認(rèn)為合適的任何其他方法。)
設(shè)置設(shè)備樹dtb(必須) , 設(shè)備樹blob(dtb)必須8字節(jié)對齊,并且大小不能超過2兆字節(jié)。由于dtb將使用最大2 MB的塊進(jìn)行映射以可緩存,因此它不能放置在必須使用任何特定屬性進(jìn)行映射的任何2M區(qū)域內(nèi)。注意:v4.2之前的版本還要求將DTB放置在512 MB區(qū)域內(nèi),從內(nèi)核映像下方的text_offset字節(jié)開始計(jì)算。
解壓縮內(nèi)核映像(可選),AArch64內(nèi)核當(dāng)前不提供解壓縮器,因此如果使用壓縮的Image目標(biāo)(例如Image.gz),則需要由引導(dǎo)加載程序執(zhí)行解壓縮(gzip等)。對于未實(shí)現(xiàn)此要求的引導(dǎo)加載程序,可以使用未經(jīng)壓縮內(nèi)核編譯。
調(diào)用內(nèi)核映像(必須)。壓縮內(nèi)核頭部如下:
u32 code0; /* 可執(zhí)行code */
u32 code1; /* 可執(zhí)行code */
u64 text_offset; /* 加載偏移,小端 */
u64 image_size; /* 有效映象尺寸,小端 */
u64 flags; /* 內(nèi)核標(biāo)志, 小端 */
u64 res2 = 0; /* 保留 */
u64 res3 = 0; /* 保留 */
u64 res4 = 0; /* 保留 */
u32 magic = 0x644d5241; /* 幻數(shù),小端, "ARM\x64" */
u32 res5; /* 保留(用于PE COFF偏移量) */
進(jìn)入內(nèi)核之前,必須滿足以下條件:
禁止所有具有DMA功能的設(shè)備,以免內(nèi)存被虛假錯誤的網(wǎng)絡(luò)數(shù)據(jù)包或磁盤數(shù)據(jù)損壞。
主CPU通用寄存器設(shè)置:
x0 =系統(tǒng)RAM中設(shè)備樹Blob(dtb)的物理地址。
x1/x2/x3 = 0(保留供將來使用)
CPU模式
所有形式的中斷都必須在PSTATE.DAIF中屏蔽(調(diào)試,SError,IRQ和FIQ)。
CPU必須位于EL2(推薦使用,以便可以訪問虛擬化擴(kuò)展)或非安全EL1中。
Caches, MMUs
MMU必須關(guān)閉。
指令緩存可以打開或關(guān)閉。
與加載的內(nèi)核映像相對應(yīng)的地址范圍必須清除到PoC。如果存在系統(tǒng)緩存或啟用了緩存的其他相關(guān)主服務(wù)器,則通常需要通過VA而不是通過設(shè)置/方式操作來維護(hù)緩存。
遵循VA對架構(gòu)化緩存維護(hù)的系統(tǒng)緩存。必須配置并啟用操作。
不遵循VA對架構(gòu)化混存維護(hù)的系統(tǒng)緩存,必須配置和禁用操作(不推薦)。
架構(gòu)定時(shí)器
必須在所有CPU上以定時(shí)器頻率設(shè)置CNTFRQ,并且必須以一致的值設(shè)置CNTVOFF。如果在EL1處進(jìn)入內(nèi)核,則CNTHCTL_EL2必須在可用時(shí)設(shè)置EL1PCTEN(位0)。
連貫性
內(nèi)核啟動時(shí),所有要由內(nèi)核引導(dǎo)的CPU都必須屬于同一一致性域。需要初始化定義的實(shí)現(xiàn),才能在每個CPU上接收維護(hù)操作。
系統(tǒng)寄存器
所有將在其中輸入內(nèi)核映像的異常級別的可寫體系結(jié)構(gòu)系統(tǒng)寄存器都必須由更高級別的異常級別的軟件初始化,以防止在UNKNOWN狀態(tài)下執(zhí)行。
對CPU模式,高速緩存,MMU,架構(gòu)計(jì)時(shí)器,一致性和系統(tǒng)寄存器的要求適用于所有CPU。所有CPU必須以相同的異常級別進(jìn)入內(nèi)核。
主CPU必須直接跳轉(zhuǎn)到內(nèi)核映像的第一條指令。此CPU傳遞的設(shè)備樹Blob必須為每個cpu節(jié)點(diǎn)包含一個“啟用方法”屬性。支持的啟用方法如下所述。引導(dǎo)加載程序?qū)⑸蛇@些設(shè)備樹屬性,并將其插入內(nèi)核入口之前的blob中。
具有“旋轉(zhuǎn)表”啟用方法的CPU在其cpu節(jié)點(diǎn)中必須具有“ cpu-release-addr”屬性。此屬性標(biāo)識自然對齊的64位零初始化內(nèi)存位置。
具有“ psci”啟用方法的CPU應(yīng)該保留在內(nèi)核之外(即,在內(nèi)存節(jié)點(diǎn)中描述給內(nèi)核的內(nèi)存區(qū)域之外,或者在內(nèi)核中通過/ memreserve /描述給內(nèi)核描述的內(nèi)存保留區(qū)域之外)。設(shè)備樹)。內(nèi)核將按照ARM文檔編號ARM DEN 0022A(“ ARM處理器上的電源狀態(tài)協(xié)調(diào)接口系統(tǒng)軟件”)中的說明發(fā)出CPU_ON調(diào)用,以將CPU帶入內(nèi)核。設(shè)備樹應(yīng)包含一個“ psci”節(jié)點(diǎn),參考/bindings/arm/psci.txt.
第二CPU通用寄存器設(shè)置的x0/x1/x2/x3都為0,保留。
內(nèi)核啟動init總過程
內(nèi)核啟動有兩種方式,壓縮格式或不壓縮格式,壓縮模式所不同的就是其入口位于arch/
本文僅分析不壓縮方式啟動內(nèi)核,通過分析內(nèi)核代碼,整理出內(nèi)核啟動過程的部分順序如下:
內(nèi)核的啟動與U-Boot一樣,前面一段是匯編代碼,然后跳轉(zhuǎn)到C代碼。匯編的入口在
./arm/kernel/head.S中,符號名為__HEAD,該文件包含了head-common.S。
所以從啟動用戶首進(jìn)程init而言,我將其分成大致分為四大步:
head.S ,初始化通用部分環(huán)境,與芯片無關(guān)
start_kernel, head.S完成后,調(diào)準(zhǔn)到start_kernel,進(jìn)入C函數(shù)執(zhí)行,該函數(shù)為于./init/main.c中
rest_init,創(chuàng)建init進(jìn)程,以及kthredd進(jìn)程,其中Init進(jìn)程號為1,kthredd為內(nèi)核進(jìn)程。
啟動調(diào)度器,執(zhí)行kernel_init,該函數(shù)將調(diào)用根文件系統(tǒng)中的init執(zhí)行文件,至此用戶空間的init進(jìn)程就啟動起來了。
head.S/head-common.S作用
剖析匯編代碼比較枯燥,這里就不進(jìn)行描述了。僅就其作用進(jìn)行總結(jié):
檢查架構(gòu),處理器和機(jī)器類型。
配置MMU,創(chuàng)建頁表?xiàng)l目并啟用虛擬內(nèi)存。
在init / main.c中調(diào)用start_kernel函數(shù)。
所有架構(gòu)的代碼相同。這也是為什么采用匯編代碼的原因,規(guī)避針對不同芯片管理大量重復(fù)代碼。
start_kernel階段
該函數(shù)主要完成以下以下工作:
lockdep 死鎖檢測模塊初始化,
RCU機(jī)制初始化:RCU(Read-Copy Update),顧名思義就是讀-拷貝修改,它是基于其原理命名的。對于被RCU保護(hù)的共享數(shù)據(jù)結(jié)構(gòu),讀者不需要獲得任何鎖就可以訪問它,但寫者在訪問它時(shí)首先拷貝一個副本,然后對副本進(jìn)行修改,最后使用一個回調(diào)(callback)機(jī)制在適當(dāng)?shù)臅r(shí)機(jī)把指向原來數(shù)據(jù)的指針重新指向新的被修改的數(shù)據(jù)。這個時(shí)機(jī)就是所有引用該數(shù)據(jù)的CPU都退出對共享數(shù)據(jù)的操作。
SMP初始化,對稱多處理"(Symmetrical Multi-Processing)簡稱SMP,完成CPU ID的創(chuàng)建。
debug_objects_early_init,負(fù)責(zé)調(diào)試對象初始化,以便于內(nèi)核調(diào)試
lockdep死鎖檢測模塊初始化,lockdep的工作方式是在內(nèi)核中的鎖定調(diào)用包起來。每次采用或釋放特定類型的鎖時(shí),都會記錄該事實(shí)以及輔助詳細(xì)信息,例如處理器當(dāng)時(shí)是否正在處理中斷。Lockdep還記錄了使用新鎖時(shí)還持有哪些其他鎖;這是lockdep能夠執(zhí)行的許多檢查的關(guān)鍵。
調(diào)用setup_arch(&command_line),該函數(shù)位于arch/
/kernel/setup.c,用于解析從bootloader傳入的引導(dǎo)命令行。 初始化控制臺,以打印啟動日志。
初始化其他各子系統(tǒng),如VFS,trace,內(nèi)存管理子系統(tǒng),F(xiàn)ORK子系統(tǒng),cgroup,acpi,proc文件系統(tǒng),內(nèi)核服務(wù),緩存等等。
……
調(diào)用rest_init,以創(chuàng)建init進(jìn)程以及內(nèi)核進(jìn)程,并啟動內(nèi)核調(diào)度器。
rest_init階段
代碼如下,其注釋如下,主要作用就是先創(chuàng)建init進(jìn)程使其進(jìn)程號為1,這是第一個用戶空間進(jìn)程,該進(jìn)程執(zhí)行后在衍生出一系列的應(yīng)用進(jìn)程。具體取決于啟動腳本或者Init的具體實(shí)現(xiàn)。然后創(chuàng)建內(nèi)核進(jìn)程kthreadd,該進(jìn)程用于管理內(nèi)核進(jìn)程。該進(jìn)程進(jìn)程號為2。所有內(nèi)核進(jìn)程都是kthreadd的后代, kthreadd枚舉其他內(nèi)核線程;它提供了接口例程,內(nèi)核服務(wù)可以在運(yùn)行時(shí)動態(tài)生成其他內(nèi)核進(jìn)程。通過kthread_create_list維護(hù)其他內(nèi)核進(jìn)程??梢允褂胮s -ef命令從命令行查看內(nèi)核線程-它們顯示在[方括號]中:
static noinline void __init_refok rest_init(void)
{
int pid;
rcu_scheduler_starting();
smpboot_thread_init();
/*創(chuàng)建init進(jìn)程,第一個用戶空間進(jìn)程我們
*需要首先生成init,以便它獲得pid 1,但是
*init任務(wù)最終將要創(chuàng)建kthread,如果在創(chuàng)建
*kthreadd之前對其進(jìn)行調(diào)度,則OOPS。*/
kernel_thread(kernel_init, NULL, CLONE_FS);
numa_default_policy();
/*創(chuàng)建kthreadd用于管理內(nèi)核線程*/
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
/*RCU 鎖*/
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
/*讓內(nèi)核進(jìn)程kthreadd處于就緒態(tài)TASK_NORMAL*/
complete(&kthreadd_done);
/* 啟動調(diào)度器 */
init_idle_bootup_task(current);
schedule_preempt_disabled();
/* 禁用搶占的情況下調(diào)用cpu_idle */
cpu_startup_entry(CPUHP_ONLINE);
}
kernel_init階段
當(dāng)內(nèi)核調(diào)度器運(yùn)行后,就會執(zhí)行kernel_init函數(shù):
static int __ref kernel_init(void *unused)
{
int ret;
kernel_init_freeable();
/* 同步完成所有初始化操作 */
async_synchronize_full();
#ifndef CONFIG_INITCALLS_THREAD
free_initmem();
#endif
mark_readonly();
system_state = SYSTEM_RUNNING;
numa_default_policy();
flush_delayed_fput();
/*如果使能了ramdisk執(zhí)行命令啟動init*/
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
/* 如果execute_command使能,則按命令啟動init*/
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
/*如果前面兩項(xiàng)都沒有使能,則依次在根文件系統(tǒng)下尋找并啟動Init*/
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
從而init用戶進(jìn)程就啟動起來了,至于最終執(zhí)行的是哪一個Init可執(zhí)行文件,取決于系統(tǒng)移植的配置,如前文描述,常見的有busybox init,systemV init,systemD init等等。
—END—
如果喜歡右下點(diǎn)個在看,也會讓我倍感鼓舞
關(guān)注置頂:掃描左下二維碼關(guān)注公眾號加星
關(guān)注 |
加群 |
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!