姓名:江軍
ID:fuchen1994
實驗日期:2016.3.13
實驗指導
-
使用實驗樓的虛擬機打開shell
- cd LinuxKernel/
- qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
內核啟動完成后進入menu程序(《軟件工程C編碼實踐篇》的課程項目),支持三個命令help、version和quit,您也可以添加更多的命令,對選修過《軟件工程C編碼實踐篇》的童鞋應該是a piece of cake.
-
使用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跟蹤調試內核從start_kernel到init進程啟動
-
詳細分析從start_kernel到init進程啟動的過程並結合實驗截圖撰寫一篇署名博客,並在博客文章中注明“真實姓名(與最后申請證書的姓名務必一致) + 原創作品轉載請注明出處 + 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000 ”,博客內容的具體要求如下:
-
題目自擬,內容圍繞Linux內核的啟動過程,即從start_kernel到init進程啟動;
-
博客中需要使用實驗截圖
-
博客內容中需要仔細分析start_kernel函數的執行過程
-
總結部分需要闡明自己對“Linux系統啟動過程”的理解,尤其是idle進程、1號進程是怎么來的。
-
3)請提交博客文章URL到網易雲課堂MOOC平台Linux內核分析MOOC課程,編輯成一個鏈接可以直接點擊打開。
Linux內核目錄:
arch目錄包括了所有和體系結構相關的核心代碼。它下面的每一個子目錄都代表一種Linux支持的體系結構,例如i386就是Intel CPU及與之相兼容體系結構的子目錄。PC機一般都基於此目錄。
COPYING目錄下是GPL版權申明。對具有GPL版權的源代碼改動而形成的程序,或使用GPL工具產生的程序,具有使用GPL發表的義務,如公開源代碼。
CREDITS目錄下是光榮榜。對Linux做出過很大貢獻的一些人的信息。
documentation目錄下是一些文檔,沒有內核代碼,可惜都是English的,是對每個目錄作用的具體說明。
drivers目錄中是系統中所有的設備驅動程序。它又進一步划分成幾類設備驅動,每一種有對應的子目錄,如聲卡的驅動對應於drivers/sound; block 下為塊設備驅動程序,比如ide(ide.c)。如果你希望查看所有可能包含文件系統的設備是如何初始化的,你可以看drivers/block/genhd.c中的device_setup()。它不僅初始化硬盤,也初始化,因為安裝nfs文件系統的時候需要網絡其他: 如, Lib放置核心的庫代碼; Net,核心與網絡相關的代碼; Ipc,這個目錄包含核心的進程間通訊的代碼; Fs,所有的文件系統代碼和各種類型的文件操作代碼,它的每一個子目錄支持一個文件系統,例如fat和ext2。
fs目錄存放Linux支持的文件系統代碼和各種類型的文件操作代碼。每一個子目錄支持一個文件系統,如ext3文件系統對應的就是ext3子目錄。
include目錄包括編譯核心所需要的大部分頭文件,例如與平台無關的頭文件在include/linux子目錄下,與 intel cpu相關的頭文件在include/asm-i386子目錄下,而include/scsi目錄則是有關scsi設備的頭文件目錄。
init目錄包含核心的初始化代碼(不是系統的引導代碼),有main.c和Version.c兩個文件。這是研究核心如何工作的好起點。
ipc目錄包含了核心進程間的通信代碼。
Kernel內核管理的核心代碼,此目錄下的文件實現了大多數linux系統的內核函數,其中最重要的文件當屬sched.c;同時與處理器結構相關代碼都放在archlib/目錄下。
MAINTAINERS目錄存放了維護人員列表,對當前版本的內核各部分都有誰負責。
Makefile目錄第一個Makefile文件。用來組織內核的各模塊,記錄了個模塊間的相互這間的聯系和依托關系,編譯時使用;仔細閱讀各子目錄下的Makefile文件對弄清各個文件這間的聯系和依托關系很有幫助。
mm目錄包含了所有獨立於 cpu 體系結構的內存管理代碼,如頁式存儲管理內存的分配和釋放等。與具體硬件體系結構相關的內存管理代碼位於arch/*/mm目錄下,例如arch/i386/mm/Fault.c 。
modules目錄存放了已建好的、可動態加載的模塊文件目錄,是個空目錄,用於存放編譯時產生的模塊目標文件。
net目錄里是核心的網絡部分代碼,其每個子目錄對應於網絡的一個方面。
ReadMe目錄里是核心及其編譯配置方法簡單介紹
REPORTING-BUGS目錄里是有關報告Bug 的一些內容
Rules.make目錄里是各種Makefilemake所使用的一些共同規則
scripts目錄包含用於配置核心的腳本文件等。
一般在每個目錄下都有一個.depend文件和一個Makefile文件。這兩個文件都是編譯時使用的輔助文件。仔細閱讀這兩個文件對弄清各個文件之間的聯系和依托關系很有幫助。另外有的目錄下還有Readme文件,它是對該目錄下文件的一些說明,同樣有利於對內核源碼的理解。
命令說明:
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) //在CPU啟動之前凍結CPU # -s shorthand for -gdb tcp::1234 若不想使用1234端口,則可以使用-gdb tcp:xxxx來取代-s選項 //在TCP1234這個端口上創建了一個gdp 服務
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服務
加載符號表
鏈接gdbserver
設置一個斷點
按c系統開始執行到斷點處暫停
使用list命令,可以看到start_kernel函數的上下文
在rest_init處設置斷點
start_kernel()是內核的匯編代碼和c代碼的交接處,在此之前,全是由匯編代碼完成各種環境的初始化工作,包括將內核代碼載入內存,設置C運行環境等等。
當運行到start_kernel()的時候,我們可以大致分析如下:
1.手工創建0號進程init_task(),他最終變成idle進程
set_task_stack_end_magic(&init_task);
init_idle()函數會把init_task加入到cpu的運行隊列中去,在沒有其他進程加入cpu隊列的時候,init_task會一直運行,當其他進程加入進來的時候,init_task就會被設置成idle,並使用調度函數將切換到新加入進來的進程上。
2.初始化各個模塊
模塊如下:內存管理模塊 中斷 調度模塊等等
3.運行到rest_init(),初始化進程
其中rest_init()是內核初始化的最后一步,它也是所有進程的祖先。
下面我們分析這個函數
注意這段代碼:
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND); //初始化第一個用戶態進程,也就是1號進程
OK,接下來我們繼續分析
static void noinline rest_init(void) 417 __releases(kernel_lock) 418 { 419 kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND); 420 numa_default_policy(); 421 unlock_kernel();423 /* 424 * The boot idle thread must execute schedule() 425 * at least one to get things moving: 426 */ 427 preempt_enable_no_resched(); 428 schedule(); 429 preempt_disable(); 430 431 /* Call into cpu_idle with preempt disabled */ 432 cpu_idle(); 433 }
在這段代碼中,我們只需要分析
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
cpu_idle(); //cpu隊列進程的切換,將0號進程設置idle
這兩句就行了,其他的可以暫時忽略
kernel_thread中傳入的函數init需要進行分析一下,我們截取部分代碼如下:
run_init_process("/sbin/init"); run_init_process()實際上是通過嵌入匯編構建一個類似用戶態代碼
run_init_process("/etc/init"); 一樣的 sys_execve()調用,其參數就是要執行的可執行文件名,也就
run_init_process("/bin/init"); 是這里的 init process 在磁盤上的文件。
run_init_process("/bin/sh");
因為實驗樓的爛環境老是卡,所以我們就不截圖分析了。
在run_init_procrss()處斷點調試,run_init_process 就是通過 execve()來運行 init 程序。
到這里idle_task()的任務完成; 將會被調度函數設置為空閑的進程。
總結:Linux在start_kernel執行之前都是匯編代碼,在他執行后,各種環境初始化后,執行c代碼。0號進程是作者手工創建的,它的任務就是在CPU的隊列中沒有進程的時候一直執行,在有進程的時候切換到新進程,而后被設置為空閑狀態。start_kernel最后一部分是第一個用戶態進程PID=1的正式生成,就是rest_init(),這個進程是系統的1號進程,這個時候0號進程會被設置成idle進程。1號進程執行,生成系統所需的所有進程,其實就是調用了run_init_process()函數加載文件,生成進程