linux內核啓動的進程淺析

張建幫 原創作品轉載請註明出處 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000

1. 實驗要求

  • 使用gdb跟蹤調試內核從 start_kernelinit 進程啓動
  • 結合實驗截圖,分析從start_kernelinit進程啓動的過程
  • 實驗使用的是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 taskinit_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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章