Linux 引導過程內幕
Linux 引導過程內幕
從主引導記錄到第一個用戶空間應用程序的指導
早期時,啟動一臺計算機意味著要給計算機餵一條包含引導程序的紙帶,或者手工使用前端面板地址/數據/控制開關來加載引導程序。盡管目前的計算機已經裝備了很多工具來簡化引導過程,但是這一切並沒有對整個過程進行必要的簡化。
讓我們先從高級的視角來查看 Linux 引導過程,這樣就可以看到整個過程的全貌了。然後將回顧一下在各個步驟到底發生了什麽。在整個過程中,參考一下內核源代碼可以幫助我們更好地了解內核源代碼樹,並在以後對其進行深入分析。
概述
圖 1 是我們在 20,000 英尺的高度看到的視圖。
圖 1. Linux 引導過程在 20,000 英尺處的視圖
當系統首次引導時,或系統被重置時,處理器會執行一個位於已知位置處的代碼。在個人計算機(PC)中,這個位置在基本輸入/輸出系統(BIOS)中,它保存在主板上的閃存中。嵌入式系統中的中央處理單元(CPU)會調用這個重置向量來啟動一個位於閃存/ROM 中的已知地址處的程序。在這兩種情況下,結果都是相同的。因為 PC 提供了很多靈活性,BIOS 必須確定要使用哪個設備來引導系統。稍後我們將詳細介紹這個過程。
當找到一個引導設備之後,第一階段的引導加載程序就被裝入 RAM 並執行。這個引導加載程序在大小上小於 512 字節(一個扇區),其作用是加載第二階段的引導加載程序。
當第二階段的引導加載程序被裝入 RAM 並執行時,通常會顯示一個動畫屏幕,並將 Linux 和一個可選的初始 RAM 磁盤(臨時根文件系統)加載到內存中。在加載映像時,第二階段的引導加載程序就會將控制權交給內核映像,然後內核就可以進行解壓和初始化了。在這個階段中,第二階段的引導加載程序會檢測系統硬件、枚舉系統鏈接的硬件設備、掛載根設備,然後加載必要的內核模塊。完成這些操作之後啟動第一個用戶空間程序(init
這就是 Linux 引導的整個過程。現在讓我們深入挖掘一下這個過程,並深入研究一下 Linux 引導過程的一些詳細信息。
系統啟動
系統啟動階段依賴於引導 Linux 系統上的硬件。在嵌入式平臺中,當系統加電或重置時,會使用一個啟動環境。這方面的例子包括 U-Boot、RedBoot 和 Lucent 的 MicroMonitor。嵌入式平臺通常都是與引導監視器搭配銷售的。這些程序位於目標硬件上的閃存中的某一段特殊區域,它們提供了將 Linux 內核映像下載到閃存並繼續執行的方法。除了可以存儲並引導 Linux 映像之外,這些引導監視器還執行一定級別的系統測試和硬件初始化過程。在嵌入式平臺中,這些引導監視器通常會涉及第一階段和第二階段的引導加載程序。
提取 MBR 的信息
要查看 MBR 的內容,請使用下面的命令:
# dd if=/dev/hda of=mbr.bin bs=512 count=1 # od -xa mbr.bin
這個 dd
命令需要以 root 用戶的身份運行,它從 /dev/hda(第一個 IDE 盤) 上讀取前 512 個字節的內容,並將其寫入 mbr.bin
文件中。od
命令會以十六進制和 ASCII 碼格式打印這個二進制文件的內容。
在 PC 中,引導 Linux 是從 BIOS 中的地址 0xFFFF0 處開始的。BIOS 的第一個步驟是加電自檢(POST)。POST 的工作是對硬件進行檢測。BIOS 的第二個步驟是進行本地設備的枚舉和初始化。
給定 BIOS 功能的不同用法之後,BIOS 由兩部分組成:POST 代碼和運行時服務。當 POST 完成之後,它被從內存中清理了出來,但是 BIOS 運行時服務依然保留在內存中,目標操作系統可以使用這些服務。
要引導一個操作系統,BIOS 運行時會按照 CMOS 的設置定義的順序來搜索處於活動狀態並且可以引導的設備。引導設備可以是軟盤、CD-ROM、硬盤上的某個分區、網絡上的某個設備,甚至是 USB 閃存。
通常,Linux 都是從硬盤上引導的,其中主引導記錄(MBR)中包含主引導加載程序。MBR 是一個 512 字節大小的扇區,位於磁盤上的第一個扇區中(0 道 0 柱面 1 扇區)。當 MBR 被加載到 RAM 中之後,BIOS 就會將控制權交給 MBR。
第一階段引導加載程序
MBR 中的主引導加載程序是一個 512 字節大小的映像,其中包含程序代碼和一個小分區表(參見圖 2)。前 446 個字節是主引導加載程序,其中包含可執行代碼和錯誤消息文本。接下來的 64 個字節是分區表,其中包含 4 個分區的記錄(每個記錄的大小是 16 個字節)。MBR 以兩個特殊數字的字節(0xAA55)結束。這個數字會用來進行 MBR 的有效性檢查。
圖 2. MBR 剖析
主引導加載程序的工作是查找並加載次引導加載程序(第二階段)。它是通過在分區表中查找一個活動分區來實現這種功能的。當找到一個活動分區時,它會掃描分區表中的其他分區,以確保它們都不是活動的。當這個過程驗證完成之後,就將活動分區的引導記錄從這個設備中讀入 RAM 中並執行它。
第二階段引導加載程序
次引導加載程序(第二階段引導加載程序)可以更形象地稱為內核加載程序。這個階段的任務是加載 Linux 內核和可選的初始 RAM 磁盤。
GRUB 階段引導加載程序
/boot/grub
目錄中包含了 stage1
、stage1.5
和 stage2
引導加載程序,以及很多其他加載程序(例如,CR-ROM 使用的是 iso9660_stage_1_5
)。
在 x86 PC 環境中,第一階段和第二階段的引導加載程序一起稱為 Linux Loader(LILO)或 GRand Unified Bootloader(GRUB)。由於 LILO 有一些缺點,而 GRUB 克服了這些缺點,因此下面讓我們就來看一下 GRUB。(有關 GRUB、LILO 和相關主題的更多內容,請參閱本文後面的 參考資料 部分的內容。)
關於 GRUB,很好的一件事情是它包含了有關 Linux 文件系統的知識。GRUB 不像 LILO 一樣使用裸扇區,而是可以從 ext2 或 ext3 文件系統中加載 Linux 內核。它是通過將兩階段的引導加載程序轉換成三階段的引導加載程序來實現這項功能的。階段 1 (MBR)引導了一個階段 1.5 的引導加載程序,它可以理解包含 Linux 內核映像的特殊文件系統。這方面的例子包括 reiserfs_stage1_5
(要從 Reiser 日誌文件系統上進行加載)或 e2fs_stage1_5
(要從 ext2 或 ext3 文件系統上進行加載)。當階段 1.5 的引導加載程序被加載並運行時,階段 2 的引導加載程序就可以進行加載了。
當階段 2 加載之後,GRUB 就可以在請求時顯示可用內核列表(在 /etc/grub.conf
中進行定義,同時還有幾個軟符號鏈接 /etc/grub/menu.lst
和 /etc/grub.conf
)。我們可以選擇內核甚至修改附加內核參數。另外,我們也可以使用一個命令行的 shell 對引導過程進行高級手工控制。
將第二階段的引導加載程序加載到內存中之後,就可以對文件系統進行查詢了,並將默認的內核映像和 initrd
映像加載到內存中。當這些映像文件準備好之後,階段 2 的引導加載程序就可以調用內核映像了。
內核
GRUB 中的手工引導
在 GRUB 命令行中,我們可以使用 initrd
映像引導一個特定的內核,方法如下:
grub> kernel /bzImage-2.6.14.2
[Linux-bzImage, setup=0x1400, size=0x29672e]
grub> initrd /initrd-2.6.14.2.img
[Linux-initrd @ 0x5f13000, 0xcc199 bytes]
grub> boot
Uncompressing Linux... Ok, booting the kernel.
如果您不知道要引導的內核的名稱,只需使用斜線(/)然後按下 Tab 鍵即可。GRUB 會顯示內核和 initrd
映像列表。
當內核映像被加載到內存中,並且階段 2 的引導加載程序釋放控制權之後,內核階段就開始了。內核映像並不是一個可執行的內核,而是一個壓縮過的內核映像。通常它是一個 zImage(壓縮映像,小於 512KB)或一個 bzImage(較大的壓縮映像,大於 512KB),它是提前使用 zlib 進行壓縮過的。在這個內核映像前面是一個例程,它實現少量硬件設置,並對內核映像中包含的內核進行解壓,然後將其放入高端內存中,如果有初始 RAM 磁盤映像,就會將它移動到內存中,並標明以後使用。然後該例程會調用內核,並開始啟動內核引導的過程。
當 bzImage(用於 i386 映像)被調用時,我們從 ./arch/i386/boot/head.S
的 start
匯編例程開始執行(主要流程圖請參看圖 3)。這個例程會執行一些基本的硬件設置,並調用 ./arch/i386/boot/compressed/head.S
中的 startup_32
例程。此例程會設置一個基本的環境(堆棧等),並清除 Block Started by Symbol(BSS)。然後調用一個叫做 decompress_kernel
的 C 函數(在 ./arch/i386/boot/compressed/misc.c
中)來解壓內核。當內核被解壓到內存中之後,就可以調用它了。這是另外一個 startup_32
函數,但是這個函數在 ./arch/i386/kernel/head.S
中。
在這個新的 startup_32
函數(也稱為清除程序或進程 0)中,會對頁表進行初始化,並啟用內存分頁功能。然後會為任何可選的浮點單元(FPU)檢測 CPU 的類型,並將其存儲起來供以後使用。然後調用 start_kernel
函數(在 init/main.c
中),它會將您帶入與體系結構無關的 Linux 內核部分。實際上,這就是 Linux 內核的 main
函數。
圖 3. Linux 內核 i386 引導的主要函數流程
通過調用 start_kernel
,會調用一系列初始化函數來設置中斷,執行進一步的內存配置,並加載初始 RAM 磁盤。最後,要調用 kernel_thread
(在 arch/i386/kernel/process.c
中)來啟動 init
函數,這是第一個用戶空間進程(user-space process)。最後,啟動空任務,現在調度器就可以接管控制權了(在調用 cpu_idle
之後)。通過啟用中斷,搶占式的調度器就可以周期性地接管控制權,從而提供多任務處理能力。
在內核引導過程中,初始 RAM 磁盤(initrd
)是由階段 2 引導加載程序加載到內存中的,它會被復制到 RAM 中並掛載到系統上。這個 initrd
會作為 RAM 中的臨時根文件系統使用,並允許內核在沒有掛載任何物理磁盤的情況下完整地實現引導。由於與外圍設備進行交互所需要的模塊可能是 initrd
的一部分,因此內核可以非常小,但是仍然需要支持大量可能的硬件配置。在內核引導之後,就可以正式裝備根文件系統了(通過 pivot_root
):此時會將 initrd
根文件系統卸載掉,並掛載真正的根文件系統。
decompress_kernel 輸出
函數 decompress_kernel
就是顯示我們通常看到的解壓消息的地方:
Uncompressing Linux... Ok, booting the kernel.
initrd
函數讓我們可以創建一個小型的 Linux 內核,其中包括作為可加載模塊編譯的驅動程序。這些可加載的模塊為內核提供了訪問磁盤和磁盤上的文件系統的方法,並為其他硬件提供了驅動程序。由於根文件系統是磁盤上的一個文件系統,因此 initrd
函數會提供一種啟動方法來獲得對磁盤的訪問,並掛載真正的根文件系統。在一個沒有硬盤的嵌入式環境中,initrd
可以是最終的根文件系統,或者也可以通過網絡文件系統(NFS)來掛載最終的根文件系統。
Init
當內核被引導並進行初始化之後,內核就可以啟動自己的第一個用戶空間應用程序了。這是第一個調用的使用標準 C 庫編譯的程序。在此之前,還沒有執行任何標準的 C 應用程序。
在桌面 Linux 系統上,第一個啟動的程序通常是 /sbin/init
。但是這不是一定的。很少有嵌入式系統會需要使用 init
所提供的豐富初始化功能(這是通過 /etc/inittab
進行配置的)。在很多情況下,我們可以調用一個簡單的 shell 腳本來啟動必需的嵌入式應用程序。
結束語
與 Linux 本身非常類似,Linux 的引導過程也非常靈活,可以支持眾多的處理器和硬件平臺。最初,加載引導加載程序提供了一種簡單的方法,不用任何花架子就可以引導 Linux。LILO 引導加載程序對引導能力進行了擴充,但是它卻缺少文件系統的感知能力。最新一代的引導加載程序,例如 GRUB,允許 Linux 從一些文件系統(從 Minix 到 Reise)上進行引導。
Linux 引導過程內幕