原文標題:The Kernel Boot Process
原文地址:http://duartes.org/gustavo/blog/
[注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來自己復習,二來與大家分享。]
上一篇文章解釋了計算機的引導過程,正好講到引導裝載程序把系統(tǒng)內核鏡像塞進內存,準備跳轉到內核入口點去執(zhí)行的時刻。作為引導啟動系列文章的最后一篇,就讓我們深入內核,去看看操作系統(tǒng)是怎么啟動的吧。由于我習慣以事實為依據(jù)討論問題,所以文中會出現(xiàn)大量的鏈接引用Linux 內核2.6.25.6版的源代碼(源自Linux Cross Reference)。如果你熟悉C的語法,這些代碼就會非常容易讀懂;即使你忽略一些細節(jié),仍能大致明白程序都干了些什么。最主要的障礙在于對一些代碼的理解需要相關的背景知識,比如機器的底層特性或什么時候、為什么它會運行。我希望能盡量給讀者提供一些背景知識。為了保持簡潔,許多有趣的東西,比如中斷和內存,文中只能點到為止了。在本文的最后列出了Windows的引導過程的要點。
當Intel x86的引導程序運行到此刻時,處理器處于實模式(可以尋址1MB的內存),(針對現(xiàn)代的Linux系統(tǒng))RAM的內容大致如下:
引導裝載完成后的RAM內容
引導裝載程序通過BIOS的磁盤I/O服務,已經把內核鏡像加載到內存當中。這個鏡像只是硬盤中內核文件(比如/boot/vmlinuz-2.6.22-14-server)的一份完全相同的拷貝。鏡像分為兩個部分:一個較小的部分,包含實模式的內核代碼,被加載到640KB內存邊界以下;另一部分是一大塊內核,運行在保護模式,被加載到低端1MB內存地址以上。
如上圖所示,之后的事情發(fā)生在實模式內核的頭部(kernel header)。這段內存區(qū)域用于實現(xiàn)引導裝載程序與內核之間的Linux引導協(xié)議。此處的一些數(shù)據(jù)會被引導裝載程序讀取。這些數(shù)據(jù)包括一些令人愉快的信息,比如包含內核版本號的可讀字符串,也包括一些關鍵信息,比如實模式內核代碼的大小。引導裝載程序還會向這個區(qū)域寫入數(shù)據(jù),比如用戶選中的引導菜單項對應的命令行參數(shù)所在的內存地址。之后就到了跳轉到內核入口點的時刻。下圖顯示了內核初始化代碼的執(zhí)行順序,包括源代碼的目錄、文件和行號:
與體系結構相關的Linux內核初始化過程
對于Intel體系結構,內核啟動前期會執(zhí)行arch/x86/boot/header.S文件中的程序。它是用匯編語言書寫的。一般說來匯編代碼在內核中很少出現(xiàn),但常見于引導代碼。這個文件的開頭實際上包含了引導扇區(qū)代碼。早期的Linux不需要引導裝載程序就可以工作,這段代碼是從那個時候留傳下來的。現(xiàn)今,如果這個引導扇區(qū)被執(zhí)行,它僅僅給用戶輸出一個“bugger_off_msg”之后就會重啟系統(tǒng)。現(xiàn)代的引導裝載程序會忽略這段遺留代碼。在引導扇區(qū)代碼之后,我們會看到實模式內核頭部(kernel header)最開始的15字節(jié);這兩部分合起來是512字節(jié),正好是Intel硬件平臺上一個典型的磁盤扇區(qū)的大小。
在這512字節(jié)之后,偏移量0x200處,我們會發(fā)現(xiàn)Linux內核的第一條指令,也就是實模式內核的入口點。具體的說,它在header.S:110,是一個2字節(jié)的跳轉指令,直接寫成了機器碼的形式0x3AEB。你可以通過對內核鏡像運行hexdump,并查看偏移量0x200處的內容來驗證這一點——這僅僅是一個對神志清醒程度的檢查,以確保這一切并不是在做夢。引導裝載程序運行完畢時就會跳轉執(zhí)行這個位置的指令,進而跳轉到header.S:229執(zhí)行一個普通的用匯編寫成的子程序,叫做start_of_setup。這個短小的子程序初始化??臻g(stack),把實模式內核的bss段清零(這個區(qū)域包含靜態(tài)變量,所以用0來初始化它們),之后跳轉執(zhí)行一段又老又好的C語言程序:arch/x86/boot/main.c:122。
main()會處理一些登記工作(比如檢測內存布局),設置顯示模式等。然后它會調用go_to_protected_mode()。然而,在把CPU置于保護模式之前,還有一些工作必須完成。有兩個主要問題:中斷和內存。在實模式中,處理器的中斷向量表總是從內存的0地址開始的,然而在保護模式中,這個中斷向量表的位置是保存在一個叫IDTR的CPU寄存器當中的。與此同時,從邏輯內存地址(在程序中使用)到線性內存地址(一個從0連續(xù)編號到內存頂端的數(shù)值)的翻譯方法在實模式和保護模式中是不同的。保護模式需要一個叫做GDTR的寄存器來存放內存全局描述符表的地址。所以go_to_protected_mode()調用了setup_idt() 和 setup_gdt(),用于裝載臨時的中斷描述符表和全局描述符表。
現(xiàn)在我們可以轉入保護模式啦,這是由另一段匯編子程序protected_mode_jump來完成的。這個子程序通過設定CPU的CR0寄存器的PE位來使能保護模式。此時,分頁功能還處于關閉狀態(tài);分頁是處理器的一個可選的功能,即使運行于保護模式也并非必要。真正重要的是,我們不再受制于640K的內存邊界,現(xiàn)在可以尋址高達4GB的RAM了。這個子程序進而調用壓縮狀態(tài)內核的32位內核入口點startup_32。startup32會做一些簡單的寄存器初始化工作,并調用一個C語言編寫的函數(shù)decompress_kernel(),用于實際的解壓縮工作。
decompress_kernel()會打印一條大家熟悉的信息“Decompressing Linux…”(正在解壓縮Linux)。解壓縮過程是原地進行的,一旦完成內核鏡像的解壓縮,第一張圖中所示的壓縮內核鏡像就會被覆蓋掉。因此解壓后的內核也是從1MB位置開始的。之后,decompress_kernel()會顯示“done”(完成)和令人振奮的“Booting the kernel”(正在引導內核)。這里“Booting”的意思是跳轉到整個故事的最后一個入口點,也是保護模式內核的入口點,位于RAM的第二個1MB開始處(偏移量0x100000,此值是由芬蘭Halti山巔之上的神靈授意給Linus的)。在這個神圣的位置含有一個子程序調用,名叫…呃…startup_32。但你會發(fā)現(xiàn)這一位是在另一個目錄中的。
這位startup_32的第二個化身也是一個匯編子程序,但它包含了32位模式的初始化過程:
1、 它清理了保護模式內核的bss段。(這回是真正的內核了,它會一直運行,直到機器重啟或關機。)
2、 為內存建立最終的全局描述符表。
3、 建立頁表以便可以開啟分頁功能。
4、 使能分頁功能。
5、 初始化??臻g。
6、 創(chuàng)建最終的中斷描述符表。
7、 最后,跳轉執(zhí)行一個體系結構無關的內核啟動函數(shù):start_kernel()。
下圖顯示了引導最后一步的代碼執(zhí)行流程:
與體系結構無關的Linux內核初始化過程
start_kernel()看起來更像典型的內核代碼,幾乎全用C語言編寫而且與特定機器無關。這個函數(shù)調用了一長串的函數(shù),用來初始化各個內核子系統(tǒng)和數(shù)據(jù)結構,包括調度器(scheduler),內存分區(qū)(memory zones),計時器(time keeping)等等。之后,start_kernel()調用rest_init(),此時幾乎所有的東西都可以工作了。rest_init()會創(chuàng)建一個內核線程,并以另一個函數(shù)kernel_init()作為此線程的入口點。之后,rest_init()會調用schedule()來激活任務調度功能,然后調用cpu_idle()使自己進入睡眠(sleep)狀態(tài),成為Linux內核中的一個空閑線程(idle thread)。cpu_idle()會在0號進程(process zero)中永遠的運行下去。一旦有什么事情可做,比如有了一個活動就緒的進程(runnable process),0號進程就會激活CPU去執(zhí)行這個任務,直到沒有活動就緒的進程后才返回。
但是,還有一個小麻煩需要處理。我們跟隨引導過程一路走下來,這個漫長的線程以一個空閑循環(huán)(idle loop)作為結尾。處理器上電執(zhí)行第一條跳轉指令以后,一路運行,最終會到達此處。從復位向量(reset vector)->BIOS->MBR->引導裝載程序->實模式內核->保護模式內核,跳轉跳轉再跳轉,經過所有這些雜七雜八的步驟,最后來到引導處理器(boot processor)中的空閑循環(huán)cpu_idle()??雌饋碚娴暮芸?。然而,這并非故事的全部,否則計算機就不會工作。
在這個時候,前面啟動的那個內核線程已經準備就緒,可以取代0號進程和它的空閑線程了。事實也是如此,就發(fā)生在kernel_init()開始運行的時刻(此函數(shù)之前被作為線程的入口點)。kernel_init()的職責是初始化系統(tǒng)中其余的CPU,這些CPU從引導過程開始到現(xiàn)在,還一直處于停機狀態(tài)。之前我們看過的所有代碼都是在一個單獨的CPU上運行的,它叫做引導處理器(boot processor)。當其他CPU——稱作應用處理器(application processor)——啟動以后,它們是處于實模式的,必須通過一些初始化步驟才能進入保護模式。大部分的代碼過程都是相同的,你可以參考startup_32,但對于應用處理器,還是有些細微的不同。最終,kernel_init()會調用init_post(),后者會嘗試啟動一個用戶模式(user-mode)的進程,嘗試的順序為:/sbin/init,/etc/init,/bin/init,/bin/sh。如果都不行,內核就會報錯。幸運的是init經常就在這些地方的,于是1號進程(PID 1)就開始運行了。它會根據(jù)對應的配置文件來決定啟動哪些進程,這可能包括X11 Windows,控制臺登陸程序,網絡后臺程序等。從而結束了引導進程,同時另一個Linux程序開始在某處運行。至此,讓我祝福您的電腦可以一直正常運行下去,不出毛病。
在同樣的體系結構下,Windows的啟動過程與Linux有很多相似之處。它也面臨同樣的問題,也必須完成類似的初始化過程。當引導過程開始后,一個最大的不同是,Windows把全部的實模式內核代碼以及一部分初始的保護模式代碼都打包到了引導加載程序(C:/NTLDR)當中。因此,Windows使用的二進制鏡像文件就不一樣了,內核鏡像中沒有包含兩個部分的代碼。另外,Linux把引導裝載程序與內核完全分離,在某種程度上自動的形成不同的開源項目。下圖顯示了Windows內核主要的啟動過程:
Windows內核初始化過程
自然而然的,Windows用戶模式的啟動就非常不同了。沒有/sbin/init程序,而是運行Csrss.exe和Winlogon.exe。Winlogon會啟動Services.exe(它會啟動所有的Windows服務程序)、Lsass.exe和本地安全認證子系統(tǒng)。經典的Windows登陸對話框就是運行在Winlogon的上下文中的。
本文是引導啟動系列話題的最后一篇。感謝每一位讀者,感謝你們的反饋。我很抱歉,有些內容只能點到為止;我打算把它們留在其他文章中深入討論,并盡量保持文章的長度適合blog的風格。下次我打算定期的撰寫關于“Software Illustrated”的文章,就像本系列一樣。最后,給大家一些參考資料:
l 最好也最重要的資料是實際的內核代碼,Linux或BSD的都成。
l Intel出版的杰出的軟件開發(fā)人員手冊,你可以免費下載到。
l 《理解Linux內核》是本好書,其中討論了大量的Linux內核代碼。這書也許有點過時有點枯燥,但我還是將它推薦給那些想要與內核心意相通的人們?!?/span>Linux設備驅動程序》讀起來會有趣得多,講的也不錯,但是涉及的內容有些局限性。最后,網友Patrick Moroney推薦Robert Love所寫的《Linux內核開發(fā)》,我曾聽過一些對此書的正面評價,所以還是值得列出來的。
l 對于Windows,目前最好的參考書是《Windows Internals》,作者是David Solomon和Mark Russinovich,后者是Sysinternals的知名專家。這是本特棒的書,寫的很好而且講解全面。主要的缺點是缺少源代碼的支持。
聯(lián)系客服