在最早期的時候,引導一台計算機意味著需要給計算機提供一個帶有啟動程序的紙帶或者需要手動調整前端儀錶盤中的地址/數據/控制開關來載入啟動程序。而如今的計算機則自帶有啟動設備用於簡化啟動過程,但是這並不意味著啟動過程就變得簡單。

我們先從總體上看Linux啟動過程的各個階段,然後再深入各個階段,分別對其進行介紹。在這個過程中,對Linux內核源碼的引用將幫助你查找和理解內核源碼。

概述

下圖給出了Linux啟動的總體過程。

當系統第一次啟動或者重啟的時候,處理器將會從一個規定好的位置開始執行。對於PC而言,這個位置對應到主板flash晶元中的BIOS(Basic Input/Output System)。對於嵌入式系統而言,CPU的reset vector是一個已知的地址,該地址指向flash/ROM的內部。不管怎樣,結果都是一樣的,即初始化系統硬體,並啟動操作系統。由於PC具有很高的靈活性,所以BIOS需要尋找並決定哪些設備是可以啟動的,並且從哪個設備啟動。我們將會在後面進行詳細的介紹。

當一個啟動設備被發現時,Stage 1 bootloader將會被載入到內存中並執行。這個bootloader位於設備的第一個sector,其長度少於512B,它的作用就是載入Stage 2 bootloader。

當Stage 2 bootloader被載入到內存並執行的時候,屏幕將會顯示boot splash界面,Linux內核和可選的initial RAM disk(臨時根文件系統)將會被載入到內存。當內核鏡像載入完畢,Stage 2 bootloader會把控制權交給Linux內核,Linux內核這時候會將內核解壓並執行,對系統進行初始化。這時候,內核將會檢測系統硬體,枚舉連接到系統的硬體設備,掛載根設備,並載入必要的內核模塊。當內核初始化完畢後,第一個用戶空間程序(init)被啟動,更上層的系統初始化將被執行。

這就是Linux啟動的大致流程。下面我們再詳細看看Linux啟動的各個階段。

系統啟動(System startup)

系統啟動階段根據系統硬體平台的變化而變化。在嵌入式平台中,當系統上電或者複位的時候,它使用的是引導環境(bootstrap environment),如U-Boot,RedBoot和Lucent的MicroMonitor。嵌入式平台一般都會內置一個這種程序,統一稱作boot monitor,boot monitor位於目標硬體的flash memory的特殊區域,為用戶提供了載入Linux內核鏡像並執行的功能。除此之外,boot monitor還提供了系統檢測和硬體初始化的功能。對於嵌入式系統而言,這些boot monitor通常覆蓋了Stage 1 bootloader和Stage 2 bootloader。

在PC環境中,系統啟動開始於地址0xFFFF0,該地址位於BIOS中。BIOS程序的第一階段任務就是上電自檢POST(Power On Self Test),對系統硬體進行基本的檢測,確保正常工作。第二階段的任務就是本地設備枚舉和初始化。

從BIOS程序功能的角度來看,BIOS由兩部分組成:POST代碼和runtime services。當POST結束時,內存中POST相關的代碼將會被丟棄,但是runtime services代碼將繼續保持在內存中,為操作系統提供一些必要的信息和服務直到系統關閉。

為了啟動操作系統,BIOS runtime會根據用戶設定的偏好順序檢測可啟動的設備,並嘗試啟動存放在設備中的操作系統。典型的可啟動設備可以是軟盤、CD-ROM、硬碟的某個分區、網路設備或者是USB磁碟。

通常Linux會從磁碟啟動,而該磁碟的MBR(Master Boot Record)會包含有primary bootloader,也就是Stage 1 bootloader。MBR是一個512位元組的扇區,該扇區為磁碟的第一個扇區(sector 1,cylinder 0,head 0)。當將MBR讀取到內存後,BIOS就會嘗試執行該primary bootloader,並將控制權交給它。

為了查看MBR的內容,在Linux中可以通過以下命令獲取:

dd if=/dev/sda of=mbr.bin bs=512 count=1
od -xa mbr.bin

dd命令讀取了/dev/sda(第一個IDE磁碟)的前512位元組,並將其寫入mbr.bin文件中。od命令將二進位文件以16進位和ASCII碼的形式將mbr.bin文件列印出來。

Stage 1 bootloader

位於MBR中的primary bootloader是一個位於512位元組image中,該image包含了primary bootloader的可執行代碼,同時也包含了一個分區表(partition table)。如下圖所示:

前446位元組是primary bootloader,包含了可執行代碼和錯誤信息字元串。接下去64位元組是磁碟的分區表,該分區表中包含了四條分區記錄,每條分區記錄為16位元組,分區記錄可以為空,若為空則表示分區不存在。最後是2個位元組的magic number,這兩個位元組是固定的0xAA55,這兩個位元組的magic number可以用於判斷該MBR記錄是否存在。

primary bootloader的作用就是用於尋找並定位secondary bootloader,也就是Stage 2 bootloader。它通過遍歷分區表尋找可用的分區,當它發現可用的分區的時候,還是會繼續掃描其他分區,確保其他分區是不可用的。然後從可用的分區中讀取secondary bootloader到內存中,並執行。

Stage 2 bootloader

Stage 2 bootloader也稱作secondary bootloader,也可以更恰當地稱作kernel loader,它的任務就是將Linux內核載入到內存中,並根據設置,有選擇性地將initial RAM disk也載入到內存中。

在x86 PC環境中,Stage 1 bootloader和Stage 2 bootloader合併起來就是 LILO (Linux Loader)或者GRUB(GRand Unified Bootloader)。因為LILO中存在一些缺點,並且這些缺點在GRUB中得到了比較好的解決,所以這裡將會以GRUB為準進行講解。

GRUB的一大優點是,它能夠正確識別到Linux文件系統。相對於像LILO那樣只能讀取原始扇區數據,GRUB則可以從ext2和ext3的文件系統中讀取到Linux內核。為了實現這個功能,GRUB將原本2個步驟的bootloader變成了3個步驟,多了Stage 1.5 bootloader,即在Stage 1 bootloader和Stage 2 bootload中間載入一個可以識別Linux文件系統的bootloader(Stage 1.5 bootloader),例如reiserfs_stage1_5(用於識別Reiser日誌文件系統)或者e2fs_stage1_5(用於識別ext2和ext3文件系統)。當Stage 1.5 bootloader被載入和執行後,就可以繼續Stage 2 bootloader的載入和執行了。

當Stage 2 bootloader被載入到內存後,GRUB就能夠顯示一系列可啟動的內核(這些可啟動的內核定義於/etc/grub.conf文件中,該文件是指向/etc/grub/menu.lst和/etc/grub.conf的軟鏈接)。你可以在這些文件中配置,讓系統自己默認選擇某一個內核啟動,並且可以配置內核啟動的相應參數。

當Stage 2 bootloader已經被載入到內存中,文件系統被識別到,並且默認的內核鏡像和initrd鏡像被載入到內存中,這就意味著鏡像都已經準備好了,可以直接調用內核鏡像開始內核的啟動了。

在Ubuntu中bootloader的相關信息可以在/boot/grub/目錄下找到,主要是/boot/grub/grub.cfg,但是該文件是自讀的,需要在其他地方(如/etc/default/grub)更改,然後執行update-grub。

Kernel階段

既然內核鏡像已經準備好,並且控制權已經從Stage 2 bootloader傳遞過來,啟動過程的Kernel階段就可以開始了。內核鏡像並非直接可以運行,而是一個被壓縮過的。通常情況下,它是一個通過zlib壓縮的zImage(compressed image小於51KB)或者bzImage(big compressed image,大於512KB)文件。在內核鏡像的開頭是一個小程序,該程序對硬體進行簡單的配置並將壓縮過的內核解壓到高內存地址空間中。如果initial RAM disk存在,則它將initial RAM disk載入到內存中,做好標記等待後面使用。這個時候,就可以真正調用內核,開始真正的內核啟動了。

在GRUB中可以通過以下方法手動啟動內核:

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 2上載入內核的命令已經由kernel變成了linux,所以需要用到的是下面的命令

grub> linux /vmlinuz
grub> initrd /initrd.img
grub> boot

而/vmlinuz和/initrd.img其實是鏈接到了/boot/目錄下特定版本的內核

lrwxrwxrwx 1 root root 33 8月 10 06:48 initrd.img -> boot/initrd.img-4.15.0-30-generic
lrwxrwxrwx 1 root root 33 8月 10 06:48 initrd.img.old -> boot/initrd.img-4.15.0-29-generic
lrwxrwxrwx 1 root root 30 8月 10 06:48 vmlinuz -> boot/vmlinuz-4.15.0-30-generic
lrwxrwxrwx 1 root root 30 8月 10 06:48 vmlinuz.old -> boot/vmlinuz-4.15.0-29-generic

bzImage(對於i386鏡像而言)被調用的入口點位於./arch/i386/boot/head.S的彙編函數start。這個函數先進行一個基本的硬體設置然後調用./arch/i386/boot/compressed/head.S中的startup_32函數。startup_32函數建立一個基本的運行環境(堆棧、寄存器等),並且清除BSS(Block Started by Symbol)。然後內核調用./arch/i386/boot/compressed/misc.c:decompress_kernel()函數對內核進行解壓縮。當內核解壓縮完畢後,就會調用另外一個startup_32函數開始內核的啟動,該函數為./arch/i386/kernel/head.S:startup_32。

在新的startup_32函數(也叫做swapper或者process 0),頁表將被初始化並且內存的分頁機制將會被使能。物理CPU的類型將會被檢測,並且FPU(floating-point unit)也會被檢測以便後面使用。然後./init/main.c:start_kernel()函數將會被調用,開始通用的內核初始化(不針對某一具體的處理器架構)。其基本流程留下所示:

在./init/main.c:start_kernl()函數中,一長串的初始化函數將會被調用到用於設置中斷、執行更詳細的內存配置、載入initial RAM disk等。接著,將會調用./arch/i386/kernel/process.c:kernel_thread()函數來啟動第一個用戶空間進程,該進程的執行函數是init。最後,idle進程(cpu_idle)將會被啟動,並且調度器其將接管整個系統。當中斷使能時,可搶佔的調度器周期性地接管系統,用於提供多任務同時運行的能力。

在內核啟動的時候,原本由Stage 2 bootloader載入到內核的initial RAM disk(initrd)將會被掛載上。這個位於RAM裡面的initrd將會臨時充當根文件系統,並且允許內核直接啟動,而不需要掛載任何的物理磁碟。因為那些用於跟外設交互的內核模塊可以被放置到initrd中,所以內核可以做得非常小,並且還能支持很多的外設配置。當內核啟動起來後,這個臨時的根文件系統將會被丟棄(通過pivot_root()函數),即initrd文件系統將會被卸載,而真正的根文件系統將會被掛載。

initrd功能讓驅動不需要直接整合到內存中,而是以可載入的模塊存在,從而讓Linux內核能夠做到很小。這些可載入模塊為內核提供訪問磁碟和文件系統的方法,同時也提供了訪問其他硬體設備的方法。因為根文件系統其實是位於磁碟的一個文件系統,initrd提供了訪問磁碟和掛載真正根文件系統的方法。在沒有磁碟的嵌入式文件系統中,initrd可以作為最終的根文件系統,或者最終的根文件系統可以通過NFS(Network File System)掛載。

Init

當內核啟動並初始化完畢後,內核就會開始啟動第一個用戶空間程序,這個被調用的程序是第一個使用標準C庫編譯的程序,在這之前,所有的程序都不是使用標準C庫編譯得到的。

在Linux桌面系統中,雖然不是強制規定的,但是第一個啟動的應用程序通常是/sbin/init。嵌入式系統中通常很少要求init程序通過/etc/inittab提供大量的初始化工作。很多情況下,用戶可以通過調用一個簡單的shell腳本來啟動所需的應用程序。

總結

就像Linux本身一樣,Linux啟動過程也是一個特別靈活的過程,該過程支持了各種各樣的處理器和硬體平台。最早的時候,loadlin bootloader提供了最簡單直接的方法來啟動Linux。後來LILO bootloader擴展強化了啟動的能力,但是卻還是沒法獲取文件系統的信息。最新的bootloader,比如GRUB,則允許從一些列的文件系統(從Minix到Reiser)中啟動Linux。

註:本文主要是從該鏈接(Inside the Linux boot process)翻譯過來的


推薦閱讀:
相关文章