萬字解析如何調(diào)試內(nèi)核啟動流程!
內(nèi)核生命周期
uboot 打印完?Starting kernel . . .
,就完成了自己的使命,控制權(quán)便交給了 kernel 的第一條指令,也就是下面這個(gè)函數(shù)
init/main.c
start_kernel 相當(dāng)于內(nèi)核的 main 函數(shù),內(nèi)核的生命周期就是從執(zhí)行這個(gè)函數(shù)的第一條語句開始的,直到最后一個(gè)函數(shù)?reset_init()
,內(nèi)核將不再從這個(gè)函數(shù)中返回,而是陷入這個(gè)函數(shù)里面的一個(gè) while(1) 死循環(huán),這個(gè)死循環(huán)被作為 idle 進(jìn)程,也就是 0 號進(jìn)程。
所以,內(nèi)核的生命周期,就是一個(gè)完整的 start_kernel 函數(shù)。始于 start_kernel 函數(shù)的第一條語句,停留在最后的死循環(huán)。
init 進(jìn)程
kernel 會創(chuàng)建眾多內(nèi)核線程,來持續(xù)致力于內(nèi)存、磁盤、CPU 的管理,其中有兩個(gè)內(nèi)核線程比較重要,需要我們重點(diǎn)講解,那就是 1 號內(nèi)核線程 kernel_init 和 2 號內(nèi)核線程 kthreadd。1 號內(nèi)核線程最終會被用戶的第一個(gè)進(jìn)程 init 代替,也就成了 1 號進(jìn)程。如下:
COMMAND 這一列,帶中括號的是內(nèi)核線程,不帶中括號的是用戶進(jìn)程。從 PID 統(tǒng)一編址就可以看出,它倆地位是一樣的。
下面我們深入分析一下從 start_kernel 到最終運(yùn)行 init 進(jìn)程,kernel 都經(jīng)歷了什么
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ? ?


打印
添加打印,是分析流程的好方法。
一開始嘗試在函數(shù)剛開始就添加 printk 打印,結(jié)果發(fā)現(xiàn)添加完 printk 后內(nèi)核起不來,最后保守起見,在 131 行 console_init(); 后才開始添加打印。
可是,發(fā)現(xiàn)在 console_init() 之前就有不少打印了,這個(gè)地方還是有些不解。猜測要么是在 console_init() 之前就已經(jīng)可以打印了;要么是在 console_init() 之前先將打印緩存著,等初始化之后再打印。這點(diǎn)以后再研究吧。先插個(gè)眼 。
接著看打印
從 133 行到 206 行,代碼不少,打印卻只有寥寥 5 行。少就說明不重要,那就不分析了,哈哈 。
看下面的代碼,乖乖,就剩一句了:rest_init();
,并且第 210 行的 printk 等到系統(tǒng)完全起來都沒有打印,說明?rest_init()
?就沒返回??磥硎莻€(gè)扛把子。
對應(yīng)打印如下:
打印了 kernel_init(),是因?yàn)檎{(diào)用了 schedule_preempt_disabled(),其內(nèi)部調(diào)用了 schedule() 執(zhí)行進(jìn)程切換,進(jìn)入睡眠。當(dāng)然,在當(dāng)前進(jìn)程被喚醒時(shí),程序也是從這個(gè)斷點(diǎn)開始運(yùn)行。
執(zhí)行進(jìn)程切換后,kernel_init() 才有機(jī)會運(yùn)行
對應(yīng)打印如下:
可以看到從第 5 行到第 12 行區(qū)區(qū)幾個(gè)函數(shù),竟然打印了那么多,而且還看到了眾多驅(qū)動相關(guān)的打印,如?usb
、Console
、Key
、io
、pinctrl
、Serial
、CAN
、RTC
、input
、mmc
、mdio_bus
,說明我們整天開發(fā)的驅(qū)動,是在這個(gè)階段初始化的呀。這里我直接給出調(diào)用關(guān)系,可以看到,驅(qū)動相關(guān)的打印是在?do_initcalls()
?中產(chǎn)生的
do_initcalls() 函數(shù)循環(huán)將編譯進(jìn)內(nèi)核的內(nèi)核模塊的初始化函數(shù)依次調(diào)用一遍。這些初始化函數(shù)也是有等級的,總共有 7 個(gè)級別 1-7,數(shù)字越大,級別越低,被調(diào)用的就越晚。其中我們最長用的 module_init() 為第 6 級,等級算是比較低的了,調(diào)用也會較晚,想想也是,好多外部模塊都是使用的 module_init(),它們可都是在內(nèi)核完全啟動后,再手動安裝的呀,更晚。
這個(gè)階段打印了很多 log,說明內(nèi)核中大部分的代碼都是驅(qū)動,內(nèi)核中寫了大量的內(nèi)核模塊初始化的函數(shù)。
驅(qū)動初始化完,內(nèi)核也就接近尾聲了
掛載根文件系統(tǒng)
內(nèi)核收尾工作
kernel_init 內(nèi)核線程調(diào)用 do_execve 函數(shù),去執(zhí)行 init 程序,產(chǎn)生 init 進(jìn)程(1 號進(jìn)程)。該內(nèi)核線程終結(jié)。
rest_init() 函數(shù)的最后一句?cpu_startup_entry(CPUHP_ONLINE);
?最終變成了 idle 進(jìn)程(0 號進(jìn)程)。
而在 kernel_init 同期稍晚創(chuàng)建的 kthreadd 內(nèi)核線程,則一直存在,成為 2 號進(jìn)程
原文作者:Li-Yongjun
