張建幫 原創作品轉載請註明出處 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000
1. 實驗要求
- 使用
gdb
跟蹤調試內核從start_kernel
到init
進程啓動 - 結合實驗截圖,分析從
start_kernel
到init
進程啓動的過程 - 實驗使用的是
linux-3.18.6
的內核源碼,點擊 這裏 進行源碼閱讀
2. 實驗思路與過程
- 使用實驗樓的虛擬機打開shell
在命令行中輸入下列命令:
cd LinuxKernel/ qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
啓動後的
menuOS
如下:可以看到程序中有些小bug:在輸入
version
命令前,會自動彈出一些莫名其妙的信息- 使用gdb跟蹤調試內核
輸入下列命令:
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 關於-s和-S選項的說明:
# -S freeze CPU at startup (use ’c’ to start execution)
# -s shorthand for -gdb tcp::1234 若不想使用1234端口,則可以使用-gdb tcp:xxxx來取代-s選項
另開一個shell
窗口,輸入下面的命令:
gdb
(gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加載符號表
(gdb)target remote:1234 # 建立gdb和gdbserver之間的連接,按c 讓qemu上的Linux繼續運行
(gdb)break start_kernel # 斷點的設置可以在target remote之前,也可以在之後
(gdb)c #讓內核執行至執行start_kernel之前
(gdb)list #查看當前即將執行的源代碼
截圖如下:
當電腦上電後時,BIOS的代碼開始執行,然後是Linux初始化的代碼,這其中大約很長一段時間Linux都沒有進程這一概念,但是這不影響CPU執行它的二進制代碼,但是由於多任務以及進程調度的需要,內核初始化過程中必須爲進程以及進程調度做準備。準備工作在init/main.c
中的 start_kernel
函數中:
/*
*該函數是Linux內核的入口,其前面的代碼都是用匯編編寫,用以完成一些
*最基本的初始化與環境設置工作,如CPU的基本初始化,爲C代碼的運行設置環境等
*/
asmlinkage __visible void __init start_kernel(void)
{
..... //完成各種初始化工作,比如內存管理,進程調度,進程通信等
rest_init(); //Linux初始化的尾聲,也是我們重點關注的函數
}
從start_kernel
函數的最後一個函數調用rest_init
開始,Linux開始產生進程,在 rest_init
函數中,內核將通過下面的代碼產生第一個真正的進程:
static noinline void __init_refok rest_init(void)
{
.....
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*/
kernel_thread(kernel_init, NULL, CLONE_FS); //創建一個內核線程或者說是內核進程,該進程創建完成後,就開始運行括號中的kernel_init函數
.....
}
kernel_init函數函數最有意思的地方在於它會通過調用kernel_execve來執行根文件系統下的/sbin/init文件(所以此前系統根文件系統必須已經就緒),其代碼如下:
930 static int __ref kernel_init(void *unused)
931 {
......
//啓動1號進程,init process,它也是所有用戶態進程的祖先
965 if (!try_to_run_init_process("/sbin/init") ||
966 !try_to_run_init_process("/etc/init") ||
967 !try_to_run_init_process("/bin/init") ||
968 !try_to_run_init_process("/bin/sh"))
969 return 0;
970
971 panic("No working init found. Try passing init= option to kernel. "
972 "See Linux Documentation/init.txt for guidance.");
973 }
這裏的try_to_run_init_process
就是通過execve()
來運行init程序。這裏首先運行“/sbin/init”,如果失敗再運行“/etc/init”,然後是“/bin/init”,然後是“/bin/sh”(也就是說,init可執行文件可以放在上面代碼中尋找的4個目錄中都可以)。這裏是內核初始化結束並開始用戶態初始化的陰陽界。
至於0號進程init_task,它屬於一個比較特殊的進程,它的初始化工作在init_task.c
文件中完成,其內核棧通過靜態方式分配。在start_kernel
函數中的sched_init()
函數調用中,會通過init_idle(current, smp_processor_id())
函數把init_task
初始化成爲一個idle task
,init_idle
函數的第一個參數current就是&init_task
,在init_idle
中將會把init_task
加入到cpu的運行隊列中,這樣當運行隊列中沒有別的就緒進程時,init_task
(也就是idle task
)將會被調用,它的核心是一個死循環,在循環中它將會調用schedule函數以便在運行隊列中有新進程加入時切換到該新進程上。具體的過程如下所示:
3. 實驗總結
這次實驗重點在於linux中啓動過程的進程產生於分析,正如孟寧老師所說,“道生一(start_kernel….cpu_idle),一生二(kernel_init和kthreadd),二生三(即前面0、1和2三個進程),三生萬物(1號進程是所有用戶態進程的祖先,2號進程是所有內核線程的祖先),這些“都符合中國傳統文化精神了”,也就不難讓人理解了。
參考文章:
http://blog.csdn.net/hardy_2009/article/details/7383815
http://blog.csdn.net/gatieme/article/details/51484562