歡迎光臨
每天分享高質量文章

Linux 啟動過程分析 | Linux 中國

理解運轉良好的系統對於處理不可避免的故障是最好的準備。
— Alison Chaiken


致謝
編譯自 | https://opensource.com/article/18/1/analyzing-linux-boot-process 
 作者 | Alison Chaiken
 譯者 | jessie-pang ? ? 共計翻譯:8 篇 貢獻時間:46 天

理解運轉良好的系統對於處理不可避免的故障是最好的準備。

關於開源軟體最古老的笑話是:“程式碼是自具檔案化的self-documenting”。經驗表明,閱讀原始碼就像聽天氣預報一樣:明智的人依然出門會看看室外的天氣。本文講述瞭如何運用除錯工具來觀察和分析 Linux 系統的啟動。分析一個功能正常的系統啟動過程,有助於使用者和開發人員應對不可避免的故障。

從某些方面看,啟動過程非常簡單。核心在單核上以單執行緒和同步狀態啟動,似乎可以理解。但核心本身是如何啟動的呢?initrd(initial ramdisk)[1] 和引導程式bootloader具有哪些功能?還有,為什麼乙太網埠上的 LED 燈是常亮的呢?

請繼續閱讀尋找答案。在 GitHub 上也提供了 介紹演示和練習的程式碼[2]

啟動的開始:OFF 狀態

區域網喚醒Wake-on-LAN

OFF 狀態表示系統沒有上電,沒錯吧?錶面簡單,其實不然。例如,如果系統啟用了局域網喚醒機制(WOL),乙太網指示燈將亮起。透過以下命令來檢查是否是這種情況:

  1. # sudo ethtool <interface name>

其中  是網路介面的名字,比如 eth0。(ethtool 可以在同名的 Linux 軟體包中找到。)如果輸出中的 Wake-on 顯示 g,則遠端主機可以透過傳送 魔法資料包MagicPacket[3] 來啟動系統。如果您無意遠端喚醒系統,也不希望其他人這樣做,請在系統 BIOS 選單中將 WOL 關閉,或者用以下方式:

  1. # sudo ethtool -s <interface name> wol d

響應魔法資料包的處理器可能是網路介面的一部分,也可能是 底板管理控制器Baseboard Management Controller[4](BMC)。

英特爾管理引擎、平臺控制器單元和 Minix

BMC 不是唯一的在系統關閉時仍在監聽的微控制器(MCU)。x86_64 系統還包含了用於遠端管理系統的英特爾管理引擎(IME)軟體套件。從伺服器到膝上型電腦,各種各樣的裝置都包含了這項技術,它開啟瞭如 KVM 遠端控制和英特爾功能許可服務等 功能[5]。根據 Intel 自己的檢測工具[6]IME 存在尚未修補的漏洞[7]。壞訊息是,要禁用 IME 很難。Trammell Hudson 發起了一個 me_cleaner 專案[8],它可以清除一些相對惡劣的 IME 元件,比如嵌入式 Web 伺服器,但也可能會影響執行它的系統。

IME 韌體和系統管理樣式System Management Mode(SMM)軟體是 基於 Minix 作業系統[9] 的,並執行在單獨的平臺控制器單元Platform Controller Hub上(LCTT 譯註:即南橋晶片),而不是主 CPU 上。然後,SMM 啟動位於主處理器上的通用可擴充套件韌體介面Universal Extensible Firmware Interface(UEFI)軟體,相關內容 已被提及多次[10]。Google 的 Coreboot 小組已經啟動了一個雄心勃勃的 非擴充套件性縮減版韌體Non-Extensible Reduced Firmware[11](NERF)專案,其目的不僅是要取代 UEFI,還要取代早期的 Linux 使用者空間元件,如 systemd。在我們等待這些新成果的同時,Linux 使用者現在就可以從 Purism、System76 或 Dell 等處購買 禁用了 IME[12] 的膝上型電腦,另外 帶有 ARM 64 位處理器膝上型電腦[13] 還是值得期待的。

引導程式

除了啟動那些問題不斷的間諜軟體外,早期引導韌體還有什麼功能呢?引導程式的作用是為新上電的處理器提供通用作業系統(如 Linux)所需的資源。在開機時,不但沒有虛擬記憶體,在控制器啟動之前連 DRAM 也沒有。然後,引導程式開啟電源,並掃描匯流排和介面,以定位核心映象和根檔案系統的位置。U-Boot 和 GRUB 等常見的引導程式支援 USB、PCI 和 NFS 等介面,以及更多的嵌入式專用裝置,如 NOR 快閃記憶體和 NAND 快閃記憶體。引導程式還與 可信平臺模組Trusted Platform Module[14](TPM)等硬體安全裝置進行互動,在啟動最開始建立信任鏈。

在構建主機上的沙盒中執行 U-boot 引導程式。

包括樹莓派、任天堂裝置、汽車主機板和 Chromebook 在內的系統都支援廣泛使用的開源引導程式 U-Boot[15]。它沒有系統日誌,當發生問題時,甚至沒有任何控制檯輸出。為了便於除錯,U-Boot 團隊提供了一個沙盒,可以在構建主機甚至是夜間的持續整合(CI)系統上測試補丁程式。如果系統上安裝了 Git 和 GNU Compiler Collection(GCC)等通用的開發工具,使用 U-Boot 沙盒會相對簡單:

  1. # git clone git://git.denx.de/u-boot; cd u-boot

  2. # make ARCH=sandbox defconfig

  3. # make; ./u-boot

  4. => printenv

  5. => help

在 x86_64 上執行 U-Boot,可以測試一些棘手的功能,如 模擬儲存裝置[2] 的重新分割槽、基於 TPM 的金鑰操作以及 USB 裝置熱插拔等。U-Boot 沙盒甚至可以在 GDB 除錯器下單步執行。使用沙盒進行開發的速度比將引導程式掃清到電路板上的測試快 10 倍,並且可以使用 Ctrl + C 恢復一個“變磚”的沙盒。

啟動核心

配置引導核心

引導程式完成任務後將跳轉到已載入到主記憶體中的核心程式碼,並開始執行,傳遞使用者指定的任何命令列選項。內核是什麼樣的程式呢?用命令 file /boot/vmlinuz 可以看到它是一個 “bzImage”,意思是一個大的壓縮的映象。Linux 原始碼樹包含了一個可以解壓縮這個檔案的工具—— extract-vmlinux[16]

  1. # scripts/extract-vmlinux /boot/vmlinuz-$(uname -r) > vmlinux

  2. # file vmlinux

  3. vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically

  4. linked, stripped

內核是一個 可執行與可連結格式Executable and Linking Format[17](ELF)的二進位制檔案,就像 Linux 的使用者空間程式一樣。這意味著我們可以使用 binutils 包中的命令,如 readelf 來檢查它。比較一下輸出,例如:

  1. # readelf -S /bin/date

  2. # readelf -S vmlinux

這兩個二進位制檔案中的段內容大致相同。

所以核心必須像其他的 Linux ELF 檔案一樣啟動,但使用者空間程式是如何啟動的呢?在 main() 函式中?並不確切。

在 main() 函式執行之前,程式需要一個執行背景關係,包括堆疊記憶體以及 stdiostdout 和 stderr 的檔案描述符。使用者空間程式從標準庫(多數 Linux 系統在用 “glibc”)中獲取這些資源。參照以下輸出:

  1. # file /bin/date

  2. /bin/date: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically

  3. linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32,

  4. BuildID[sha1]=14e8563676febeb06d701dbee35d225c5a8e565a,

  5. stripped

ELF 二進位制檔案有一個直譯器,就像 Bash 和 Python 指令碼一樣,但是直譯器不需要像指令碼那樣用 #! 指定,因為 ELF 是 Linux 的原生格式。ELF 直譯器透過呼叫 _start() 函式來用所需資源 配置一個二進位制檔案[18],這個函式可以從 glibc 原始碼包中找到,可以 用 GDB 檢視[19]。核心顯然沒有直譯器,必須自我配置,這是怎麼做到的呢?

用 GDB 檢查內核的啟動給出了答案。首先安裝內核的除錯軟體包,核心中包含一個未剝離的unstrippedvmlinux,例如 apt-get install linux-image-amd64-dbg,或者從原始碼編譯和安裝你自己的核心,可以參照 Debian Kernel Handbook[20] 中的指令。gdb vmlinux後加 info files 可顯示 ELF 段 init.text。在 init.text 中用 l *(address) 列出程式執行的開頭,其中 address 是 init.text 的十六進位制開頭。用 GDB 可以看到 x86_64 核心從核心檔案 arch/x86/kernel/head_64.S[21] 開始啟動,在這個檔案中我們找到了彙編函式 start_cpu0(),以及一段明確的程式碼顯示在呼叫 x86_64 start_kernel() 函式之前建立了堆疊並解壓了 zImage。ARM 32 位核心也有類似的檔案 arch/arm/kernel/head.S[22]start_kernel() 不針對特定的體系結構,所以這個函式駐留在內核的 init/main.c[23] 中。start_kernel() 可以說是 Linux 真正的 main() 函式。

從 start_kernel() 到 PID 1

內核的硬體清單:裝置樹和 ACPI 表

在引導時,核心需要硬體資訊,不僅僅是已編譯過的處理器型別。程式碼中的指令透過單獨儲存的配置資料進行擴充。有兩種主要的資料儲存方法:裝置樹device-tree[24] 和 高階配置和電源介面(ACPI)表[25]。核心透過讀取這些檔案瞭解每次啟動時需要執行的硬體。

對於嵌入式裝置,裝置樹是已安裝硬體的清單。裝置樹只是一個與核心原始碼同時編譯的檔案,通常與 vmlinux 一樣位於 /boot 目錄中。要檢視 ARM 裝置上的裝置樹的內容,只需對名稱與 /boot/*.dtb 匹配的檔案執行 binutils 包中的 strings 命令即可,這裡 dtb 是指裝置樹二進位制檔案device-tree binary。顯然,只需編輯構成它的類 JSON 的檔案並重新執行隨核心原始碼提供的特殊 dtc 編譯器即可修改裝置樹。雖然裝置樹是一個靜態檔案,其檔案路徑通常由命令列引導程式傳遞給核心,但近年來增加了一個 裝置樹改寫[26] 的功能,核心在啟動後可以動態載入熱插拔的附加裝置。

x86 系列和許多企業級的 ARM64 裝置使用 ACPI[25] 機制。與裝置樹不同的是,ACPI 資訊儲存在核心在啟動時透過訪問板載 ROM 而建立的 /sys/firmware/acpi/tables 虛擬檔案系統中。讀取 ACPI 表的簡單方法是使用 acpica-tools 包中的 acpidump 命令。例如:

聯想膝上型電腦的 ACPI 表都是為 Windows 2001 設定的。

是的,你的 Linux 系統已經準備好用於 Windows 2001 了,你要考慮安裝嗎?與裝置樹不同,ACPI 具有方法和資料,而裝置樹更多地是一種硬體描述語言。ACPI 方法在啟動後仍處於活動狀態。例如,執行 acpi_listen 命令(在 apcid 包中),然後開啟和關閉筆記本機蓋會發現 ACPI 功能一直在執行。暫時地和動態地 改寫 ACPI 表[27] 是可能的,而永久地改變它需要在引導時與 BIOS 選單互動或掃清 ROM。如果你遇到那麼多麻煩,也許你應該 安裝 coreboot[28],這是開源韌體的替代品。

從 start_kernel() 到使用者空間

init/main.c[23] 中的程式碼竟然是可讀的,而且有趣的是,它仍然在使用 1991 – 1992 年的 Linus Torvalds 的原始版權。在一個剛啟動的系統上執行 dmesg | head,其輸出主要來源於此檔案。第一個 CPU 註冊到系統中,全域性資料結構被初始化,並且排程程式、中斷處理程式(IRQ)、定時器和控制檯按照嚴格的順序逐一啟動。在 timekeeping_init() 函式執行之前,所有的時間戳都是零。核心初始化的這部分是同步的,也就是說執行只發生在一個執行緒中,在最後一個完成並傳回之前,沒有任何函式會被執行。因此,即使在兩個系統之間,dmesg 的輸出也是完全可重覆的,只要它們具有相同的裝置樹或 ACPI 表。Linux 的行為就像在 MCU 上執行的 RTOS(實時作業系統)一樣,如 QNX 或 VxWorks。這種情況持續存在於函式 rest_init() 中,該函式在終止時由 start_kernel() 呼叫。

早期的核心啟動流程。

函式 rest_init() 產生了一個新行程以執行 kernel_init(),並呼叫了 do_initcalls()。使用者可以透過將 initcall_debug 附加到核心命令列來監控 initcalls,這樣每執行一次 initcall 函式就會產生 一個 dmesg 條目。initcalls 會歷經七個連續的級別:early、core、postcore、arch、subsys、fs、device 和 late。initcalls 最為使用者可見的部分是所有處理器外圍裝置的探測和設定:匯流排、網路、儲存和顯示器等等,同時載入其核心模組。rest_init() 也會在引導處理器上產生第二個執行緒,它首先執行 cpu_idle(),然後等待排程器分配工作。

kernel_init() 也可以 設定對稱多處理(SMP)結構[29]。在較新的核心中,如果 dmesg 的輸出中出現 “Bringing up secondary CPUs…” 等字樣,系統便使用了 SMP。SMP 透過“熱插拔” CPU 來進行,這意味著它用狀態機來管理其生命週期,這種狀態機在概念上類似於熱插拔的 U 盤一樣。內核的電源管理系統經常會使某個core離線,然後根據需要將其喚醒,以便在不忙的機器上反覆呼叫同一段的 CPU 熱插拔程式碼。觀察電源管理系統呼叫 CPU 熱插拔程式碼的 BCC 工具[30] 稱為 offcputime.py

請註意,init/main.c 中的程式碼在 smp_init() 執行時幾乎已執行完畢:引導處理器已經完成了大部分一次性初始化操作,其它核無需重覆。儘管如此,跨 CPU 的執行緒仍然要在每個核上生成,以管理每個核的中斷(IRQ)、工作佇列、定時器和電源事件。例如,透過 ps -o psr 命令可以檢視服務每個 CPU 上的執行緒的 softirqs 和 workqueues。

  1. # ps -o pid,psr,comm $(pgrep ksoftirqd)  

  2. PID PSR COMMAND

  3.   7   0 ksoftirqd/0

  4.  16   1 ksoftirqd/1

  5.  22   2 ksoftirqd/2

  6.  28   3 ksoftirqd/3

  7. # ps -o pid,psr,comm $(pgrep kworker)

  8. PID  PSR COMMAND

  9.   4   0 kworker/0:0H

  10.  18   1 kworker/1:0H

  11.  24   2 kworker/2:0H

  12.  30   3 kworker/3:0H

  13. [ . . . ]

其中,PSR 欄位代表“處理器processor”。每個核還必須擁有自己的定時器和 cpuhp 熱插拔處理程式。

那麼使用者空間是如何啟動的呢?在最後,kernel_init() 尋找可以代表它執行 init 行程的 initrd。如果沒有找到,核心直接執行 init 本身。那麼為什麼需要 initrd呢?

早期的使用者空間:誰規定要用 initrd?

除了裝置樹之外,在啟動時可以提供給內核的另一個檔案路徑是 initrd 的路徑。initrd通常位於 /boot 目錄中,與 x86 系統中的 bzImage 檔案 vmlinuz 一樣,或是與 ARM 系統中的 uImage 和裝置樹相同。用 initramfs-tools-core 軟體包中的 lsinitramfs 工具可以列出 initrd 的內容。發行版的 initrd 方案包含了最小化的 /bin/sbin 和 /etc 目錄以及核心模組,還有 /scripts 中的一些檔案。所有這些看起來都很熟悉,因為 initrd 大致上是一個簡單的最小化 Linux 根檔案系統。看似相似,其實不然,因為位於虛擬記憶體盤中的 /bin 和 /sbin 目錄下的所有可執行檔案幾乎都是指向 BusyBox 二進位制檔案[31] 的符號連結,由此導致 /bin 和 /sbin 目錄比 glibc 的小 10 倍。

如果要做的只是載入一些模組,然後在普通的根檔案系統上啟動 init,為什麼還要建立一個 initrd 呢?想想一個加密的根檔案系統,解密可能依賴於載入一個位於根檔案系統 /lib/modules 的核心模組,當然還有 initrd 中的。加密模組可能被靜態地編譯到核心中,而不是從檔案載入,但有多種原因不希望這樣做。例如,用模組靜態編譯內核可能會使其太大而不能適應儲存空間,或者靜態編譯可能會違反軟體許可條款。不出所料,儲存、網路和人類輸入裝置(HID)驅動程式也可能存在於 initrd 中。initrd 基本上包含了任何掛載根檔案系統所必需的非核心程式碼。initrd 也是使用者存放 自定義ACPI[31] 表程式碼的地方。

救援樣式的 shell 和自定義的 initrd 還是很有意思的。

initrd 對測試檔案系統和資料儲存裝置也很有用。將這些測試工具存放在 initrd 中,並從記憶體中執行測試,而不是從被測物件中執行。

最後,當 init 開始執行時,系統就啟動啦!由於第二個處理器現在在執行,機器已經成為我們所熟知和喜愛的非同步、可搶佔、不可預測和高效能的生物。的確,ps -o pid,psr,comm -p 1 很容易顯示使用者空間的 init 行程已不在引導處理器上運行了。

總結

Linux 引導過程聽起來或許令人生畏,即使是簡單嵌入式裝置上的軟體數量也是如此。但換個角度來看,啟動過程相當簡單,因為啟動中沒有搶佔、RCU 和競爭條件等撲朔迷離的複雜功能。只關註內核和 PID 1 會忽略了引導程式和輔助處理器為執行核心執行的大量準備工作。雖然核心在 Linux 程式中是獨一無二的,但透過一些檢查 ELF 檔案的工具也可以瞭解其結構。學習一個正常的啟動過程,可以幫助運維人員處理啟動的故障。

要瞭解更多資訊,請參閱 Alison Chaiken 的演講——Linux: The first second[32],已於 1 月 22 日至 26 日在悉尼舉行。參見 linux.conf.au[33]

感謝 Akkana Peck[34] 的提議和指正。


via: https://opensource.com/article/18/1/analyzing-linux-boot-process

作者:Alison Chaiken[36] 譯者:jessie-pang 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

贊(0)

分享創造快樂