Analyzing the Linux boot process-分析Linux啓動過程

本文翻譯自Analyzing the Linux boot process.

箴言:瞭解運行良好的系統是將來處理不可避免的故障的很好準備

image

開源軟件領域中流行的最爲古老笑話:"the code is selfdocumenting."經驗表明:閱讀源碼類似於收聽天氣預報(但明智的人仍會走出去檢查天氣)。下面是一些關於如何利用熟悉的調試工具在引導時檢查和觀察Linux系統的技巧。瞭解運行良好的系統是爲將來處理不可避免的故障的很好準備。

在某些方面,內核的啓動過程是十分簡單的。內核在單個內核上以單線程和同步的方式啓動,對於可憐的人類來說,這幾乎是可以理解的。但是,內核是如何自舉的?initrd(初始ramdisk)和引導加載程序執行哪些功能?等等,爲什麼以太網端口上的LED始終亮着?

繼續閱讀以獲取這些問題和其他問題的答案;所描述的演示和練習的代碼也可以在GitHub上獲得。

啓動的開始:網卡“關閉”狀態

網絡喚醒

網關的狀態燈OFF是意味着系統沒有加電,對吧?表面上的簡單往往具有欺騙性。例如,以太網指示燈亮着可能是因爲你的系統開啓着wake-on-LAN(WOL)功能。輸入以下內容檢查是否是這種情況:

$# sudo ethtool <interface name>

可能是,例如,“eth0”。(ethtool可在同名的Linux軟件包中找到)。如果輸出中顯示“Wake-on”的狀態爲g,那麼遠程主機可以通過MagicPacket來引導系統。如果您無意遠程喚醒系統並且不希望其他人這樣做,請在系統BIOS菜單中關閉WOL,或者通過以下方式關閉WOL:

$# sudo ethtool -s <interface name> wol d

響應MagicPacket的處理器可以是網絡接口的一部分,也可以是底板管理控制器(BMC)。

英特爾管理引擎,平臺控制器中心和Minix

當系統名義上關閉時,BMC不是唯一可以監聽的微控制器(MCU)。x86_64系統還包括用於遠程管理系統的英特爾管理引擎(Intel Management Engine IME) IME)軟件套件。從服務器到筆記本電腦的各種設備都包含此技術,可實現 KVM遠程控制和英特爾功能許可服務等功能。根據英特爾自己的檢測工具,IME尚未修補漏洞。壞消息是,很難禁用IME。Trammell Hudson創建了一個me_cleaner項目,它可以擦除一些更糟糕的IME組件,比如嵌入式web服務器,但是也可以破壞運行它的系統。IME固件和系統管理模式(SMM)軟件在引導時都是基於Minix操作系統,運行在獨立的平臺控制器集線器處理器上,而不是主系統CPU上。然後SMM在主處理器上啓動通用可擴展固件接口(Universal Extensible Firmware Interface, UEFI)軟件,關於該軟件已經編寫了很多內容。谷歌的Coreboot小組已經啓動了一個雄心勃勃的非可擴展簡化固件(NERF)項目,其目標不僅是替換UEFI,而且還替換早期的Linux用戶空間組件,如systemd。在我們等待這些新努力的結果時,Linux用戶現在可以從Purism、System76或禁用IME的Dell購買筆記本電腦,另外我們還可以希望購買ARM 64位處理器的筆記本電腦。

BootLoaders

除了啓動有bug的間諜軟件,早期引導固件還有什麼功能?引導裝載程序的工作是爲新支持的處理器提供運行Linux等通用操作系統所需的資源。在開機時,不僅沒有虛擬內存,而且在控制器啓動之前也沒有DRAM。然後,引導裝載程序打開電源並掃描總線和接口,以定位內核映像和根文件系統。流行的引導加載程序,如U-Boot和GRUB,支持熟悉的接口,如USB、PCI和NFS,以及更嵌入式的特定設備,如NOR和NAND-flash。引導加載程序還與硬件安全設備(如受信任平臺模塊(Trusted Platform Modules, TPMs))交互,從最早的引導開始建立信任鏈。

image

在構建主機的沙箱中運行U-boot引導加載程序。

這個開源的、廣泛使用的U-Boot引導加載程序支持各種系統,從樹莓派到任天堂設備,從汽車板到chromebook。沒有syslog,當事情偏離正題時,通常甚至沒有任何控制檯輸出。爲了方便調試,U-Boot團隊提供了一個沙箱,可以在構建主機上測試補丁,甚至可以在夜間持續集成系統中測試補丁。在安裝了Git和GNU Compiler Collection (GCC)等常用開發工具的系統上,使用U-Boot沙箱相對簡單:

$# git clone git://git.denx.de/u-boot; cd u-boot
$# make ARCH=sandbox defconfig
$# make; ./u-boot
=> printenv
=> help

就是這樣:您在x86_64上運行U-Boot,可以測試一些複雜的特性,如模擬存儲設備(mock storage device)重新分區、基於tpm的密鑰操作和USB設備的熱插拔。U-Boot沙箱甚至可以在GDB調試器下單步執行。使用沙箱進行開發要比將引導加載程序重新加載到一塊板上進行測試快10倍,並且可以使用Ctrl+C恢復“bricked”沙箱。

啓動內核

提供一個引導內核

在完成任務後,BootLoader將執行一個跳轉到已加載到主內存中的內核代碼的操作,並開始執行,傳遞用戶指定的任何命令行選項。內核是什麼樣的程序?文件/boot/vmlinuz表明它是一個bzImage,意思是一個大的壓縮的bzImage。Linux源樹包含一個 [extract-vmlinux tool]https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux)工具,可用於解壓文件:

$# scripts/extract-vmlinux /boot/vmlinuz-$(uname -r) > vmlinux
$# file vmlinux 
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically 
linked, stripped

內核是一種Executable and Linking Format(ELF)二進制文件,就像Linux用戶空間程序一樣。這意味着我們可以使用來自binutils包(如readelf)的命令來檢查它。例如,比較…的輸出:

$# readelf -S /bin/date
$# readelf -S vmlinux

二進制文件中的節列表基本相同。

所以內核必須啓動類似於其他Linux ELF二進制文件的東西……但是用戶空間程序實際上是如何啓動的呢?在main()函數中,對吧?並不完全準確。

在main()函數運行之前,程序需要一個執行上下文,其中包括堆和堆棧內存以及stdio、stdout和stderr的文件描述符。用戶空間程序從標準庫獲得這些資源,標準庫是大多數Linux系統上的glibc。例如:

$# file /bin/date 
/bin/date: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically 
linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, 
BuildID[sha1]=14e8563676febeb06d701dbee35d225c5a8e565a,
stripped

ELF二進制文件有一個解釋器,就像Bash和Python腳本一樣,但是解釋器不需要用#!在腳本中,ELF是Linux的本機格式。ELF解釋器通過調用_start()提供一個包含所需資源的二進制文件,_start()是glibc源包中提供的一個函數,可以通過GDB進行檢查。內核顯然沒有解釋器,必須自己提供,但是如何提供呢?

用GDB檢查內核的啓動會給出答案。首先安裝包含未剝離的vmlinux版本的內核的調試包,例如apt-get install linux-image-amd64-dbg,或者根據Debian Kernel Handbook中的說明,從源代碼編譯和安裝自己的內核。執行gdb vmlinux,然後執行info files顯示init.text分區信息。在init.text中列出程序執行的開始,帶有l *(address)的文本,其中address是init.text的十六進制開頭。GDB將指示x86_64內核在內核文件arch/x86/kernel/head_64.S中啓動,在這裏,我們找到了彙編函數start_cpu0(),以及在調用x86_64 start_kernel()函數之前顯式創建堆棧並解壓縮zImage的代碼。ARM 32位內核具有類似的arch/ARM /kernel/head.S。start_kernel()不是特定於體系結構的,因此位於內核[的init/main.c]函數(https://github.com/torvalds/linux/blob/master/init/main.c)中的start_kernel()可以說是Linux真正的main()函數。

從start_kernel()到PID 1

內核的硬件清單:設備樹和ACPI表

在引導時,內核需要有關已編譯的處理器類型之外的硬件的信息。代碼中的指令由單獨存儲的配置數據進行擴充。存儲這些數據有兩種主要方法:設備樹和ACPI表。內核通過讀取這些文件來了解每次引導時必須運行哪些硬件。

對於嵌入式設備,設備樹是已安裝硬件的清單。設備樹只是一個與內核源代碼同時編譯的文件,通常與vmlinux一起位於/boot中。要查看ARM設備上的二進制設備樹中的內容,只需對名稱匹配/boot/*的文件使用binutils包中的strings命令即可。dtb是指一個設備樹二進制文件。顯然,可以通過編輯組成設備樹的類似json的文件並重新運行內核源代碼提供的特殊dtc編譯器來修改設備樹。雖然設備樹是一個靜態文件,其文件路徑通常由命令行上的引導加載程序傳遞給內核,但是近年來添加了一個設備樹覆蓋工具,內核可以在引導後動態加載額外的片段以響應熱插拔事件。

x86-family和許多企業級ARM64設備使用了替代的高級配置和電源接口(ACPI)機制。與設備樹不同,ACPI信息存儲在/sys/firmware/ ACPI /tables虛擬文件系統中,該文件系統是內核通過訪問板載ROM在引導時創建的。讀取ACPI表的簡單方法是使用acpica-tools包中的acpidump命令。這裏有一個例子:

聯想筆記本電腦上的ACPI表都是爲Windows 2001設置的

image

是的,如果您願意安裝的話,您的Linux系統已經爲Windows 2001做好了準備。ACPI同時具有方法和數據,這與設備樹不同,後者更像是一種硬件描述語言。ACPI方法在引導後仍然是活動的。例如,啓動命令acpi_listen(來自包apcid)並打開和關閉筆記本電腦的蓋子將顯示ACPI功能一直在運行。雖然臨時和動態地覆蓋ACPI表是可能的,但是永久地更改它們涉及到在引導時與BIOS菜單進行交互或對ROM進行反流。如果您遇到這麼多麻煩,也許您應該安裝coreboot,這是一種開源固件替代品。

從start_kernel()到用戶空間

init/main.c中的代碼可讀性驚人,而且有趣的是,它仍然保留着Linus Torvalds 1991-1992年的原始版權。在新引導系統上的dmesg |head中發現的行主要來自這個源文件。第一個CPU註冊到系統中,初始化全局數據結構,調度程序、中斷處理程序(IRQs)、計時器和控制檯按嚴格的順序逐一聯機。在函數timekeeping_init()運行之前,所有時間戳都爲零。內核初始化的這一部分是同步的,這意味着執行只在一個線程中進行,在最後一個線程完成並返回之前不會執行任何函數。因此,即使在兩個系統之間,只要它們具有相同的設備樹或ACPI表,dmesg輸出也是完全可複製的。Linux的行爲類似於運行在MCUs上的RTOS(實時操作系統),例如QNX或VxWorks。這種情況將持久化到函數rest_init()中,該函數在終止時由start_kernel()調用。

早期內核引導過程的總結
image

名稱很不起眼的rest_init()生成一個運行kernel_init()的新線程,該線程調用do_initcalls()。用戶可以通過在內核命令行中添加initcall_debug來監視initcall的運行,從而在每次運行initcall函數時生成dmesg條目。initcall通過七個順序級別:early、core、postcore、arch、subsys、fs、device和late。initcalls中用戶最可見的部分是探測和設置所有處理器的外圍設備:buses, network, storage, displays等等,並加載它們的內核模塊。rest_init()還在引導處理器上生成第二個線程,該線程在等待調度程序分配工作時首先運行cpu_idle()。kernel_init()還設置了對稱多處理(SMP)。對於最新的內核,可以在demsg的輸出中查找"“Bringing up secondary CPUs…”"SMP通過“hotpluging”cpu進行,這意味着它使用一種狀態機來管理它們的生命週期,這種狀態機在理論上類似於熱插拔USB等設備的生命週期。內核的電源管理系統經常將單個內核脫機,然後根據需要喚醒它們,以便在不繁忙的機器上反覆調用相同的CPU熱插拔代碼。使用名爲offcputime.py的BCC工具觀察電源管理系統對CPU熱插拔的調用。

注意,當smp_init()運行時,init/main.c中的代碼幾乎已經完成執行:引導處理器已經完成了大多數其他內核不需要重複的一次性初始化。儘管如此,必須爲每個內核派生每個cpu線程,以管理每個內核上的中斷(irq)、工作隊列、計時器和電源事件。例如,可以通過ps -o psr命令查看爲軟中斷和工作隊列提供服務的每個cpu線程。

$\# ps -o pid,psr,comm $(pgrep ksoftirqd)  
 PID PSR COMMAND 
   7   0 ksoftirqd/0 
  16   1 ksoftirqd/1 
  22   2 ksoftirqd/2 
  28   3 ksoftirqd/3 

$\# ps -o pid,psr,comm $(pgrep kworker)
PID  PSR COMMAND 
   4   0 kworker/0:0H 
  18   1 kworker/1:0H 
  24   2 kworker/2:0H 
  30   3 kworker/3:0H
[ . .  . ]

其中PSR字段代表“處理器”。每個核心還必須承載自己的計時器和cpuhp熱插拔處理程序。

用戶空間是如何開始的呢?在它的末尾,kernel_init()尋找一個可以代表它執行init進程的initrd。如果沒有找到,內核直接執行init本身。那麼,爲什麼可能需要一個initrd呢?

早期用戶空間:誰訂購了initrd?

除了設備樹之外,在引導時可選地提供給內核的另一個文件路徑是initrd。initrd通常與x86上的bzImage文件vmlinuz一起位於/boot中,或者對於ARM來說其與uImage和設備樹在一起。使用lsinitramfs工具(initramfs-tools-core包的一部分)列出initrd的內容。發行版initrd方案包含最小/bin、/sbin和/etc目錄以及內核模塊,以及/scripts中的一些文件。所有這些看起來都很熟悉,因爲initrd在很大程度上只是一個最小的Linux根文件系統。這種明顯的相似性有點欺騙性,因爲ramdisk中/bin和/sbin中的幾乎所有可執行文件都是BusyBox binary件的符號鏈接,導致/bin和/sbin目錄比glibc目錄小10倍。

如果initrd所做的只是加載一些模塊,然後在常規的根文件系統上啓動init,那麼爲什麼還要創建initrd呢?考慮加密的根文件系統。解密可能依賴於加載存儲在根文件系統/lib/modules中的內核模塊……不出所料,在initrd中也是如此。可以將crypto模塊靜態編譯到內核中,而不是從文件中加載,但是有很多原因不希望這樣做。例如,使用模塊靜態編譯內核可能會使內核太大而無法裝入可用的存儲,或者靜態編譯可能違反軟件許可證的條款。不出所料,存儲、網絡和人工輸入設備(human input device, HID)驅動程序也可能出現在initrd中——基本上是掛載根文件系統所需的內核之外的任何代碼。initrd也是用戶可以存放自己的自定義ACPI表代碼的地方。

image

initrd對於測試文件系統和數據存儲設備本身也非常有用。將這些測試工具保存在initrd中,並從內存中而不是從被測試的對象中運行測試。

最後,當init運行時,系統就啓動了!由於二級處理器現在正在運行,機器已經變成了異步的、可搶佔的、不可預測的、高性能的生物,我們知道並喜愛它。實際上,ps -o pid、psr、comm - p1很容易顯示用戶空間的init進程不再在引導處理器上運行。

總結

考慮到即使在簡單的嵌入式設備上也有許多不同的軟件參與,Linux引導過程聽起來令人生畏。從另一個角度看,引導過程相當簡單,因爲在引導中不存在搶佔、RCU和競態條件等特性所導致的令人困惑的複雜性。只關注內核和PID 1忽略了引導加載程序和輔助處理器在爲內核運行準備平臺時可能要做的大量工作。雖然內核在Linux程序中當然是獨一無二的,但是可以通過將一些用於檢查其他ELF二進制文件的相同工具應用到內核中來了解其結構。學習內核的正常啓動過程可以爲系統出現故障時做準備。

要了解更多信息,請參加Alison Chaiken的演講:Linux: The first second

感謝Akkana Peck最初提出這個主題並進行了許多修正。

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