內核長篇分享

rootfs initramfs kexec 與 Linux 啟動過程

作為 Debian 用戶,在使用 apt 更新系統時偶爾會發現某次安裝更新的時間特別長,這往往出現在較大版本的更新中,仔細觀察後就會發現,這個耗時極長的操作並不是安裝某個軟體,而是對一個名為 initrd.img 的文件進行解壓、修改再重新壓縮,那麼為什麼我們會需要在更新系統時修改這個文件呢?這還要從 Linux啟動流程說起。

在遠古時代

回到 Linux 的遠古時代,那時 Linux 支持的外設和功能還沒有像今天這樣豐富,因此啟動也相對較為簡單,只需要一個 Bootloader 啟動內核(例如 LILO 或 GRUB 等),並在啟動時向內核傳遞 rootfs 所在的設備即可,之後 Linux 即可尋找到 rootfs 並正常啟動。

以筆者這張龍芯 2K1000LA 嵌入式開發板搭載的 PMON 5.0.2 引導程序為例,要讓內核知道 rootfs 的位置,只需要在/boot/boot.cfg文件中內核的啟動參數上加上這樣一條:

root=/dev/sda

值得一提的是,這裡的/dev/sda通常指的是硬碟,例如在我的機器上就是安裝在主板上的 nvme 硬碟,那麼問題就來了,如果在保持內核不變的同時,想要從 U 盤上的 rootfs 啟動怎麼辦呢?你可能會以為只需要這樣修改就好了:

root=/dev/sdb

當初的筆者也是這麼想的,然而事情並沒有這麼簡單,修改完畢開機之後,內核很快拋出了找不到 /dev/sdb 的 Panic,於是在 Linux Kernel 手冊里翻找一番之後,我找到了這樣的一個參數:

rootwait        [KNL] Wait (indefinitely) for root device to show up.
                      Useful for devices that are detected asynchronously
                      (e.g. USB and MMC devices).

在參數的簡介中特別說明了適用 USB 等非同步檢測的設備,在啟動參數中加上它之後,內核終於成功啟動到了 U 盤中的 rootfs,一個簡陋的 Live USB 系統也就這樣做好了,整體體驗屬實是相當復古了。

不過從嚴格意義上來說,這並不能算是「返古」,畢竟對於嵌入式系統來說,接入的設備和應用場景都相對固定,因此沒有必要為系統啟動引入額外的層級。但對於桌面工作站、甚至是伺服器等運行 Linux 的設備來說,他們需要處理遠比這複雜的場景,rootfs 可能會存儲在 RAID 磁碟陣列中,可能需要實現 XFS 等複雜的文件系統才能讀取,甚至這台設備可能就沒有硬碟,需要從網路載入 rootfs 等等...隨著需求越來越複雜,人們意識到不能無限制地向 Kernel 中塞入代碼,於是在 Linux 引入了一個全新的機制——早期用戶空間。

早期用戶空間

眾所周知,在計算機領域解決問題的最好方式之一就是:引入一層新的抽象,於是早期用戶空間(Early Userspace)橫空出世。它正是為了解決 Linux 的啟動越來越複雜,需要內核支持的功能越來越多這一問題出現的。它主要由三個部分組成:

  • gen_init_cpio,這個程序會生成包含根文件系統的 cpio 格式鏡像,這個文件是壓縮過的,並且可以直接包含在內核中。
  • initramfs,它的實現代碼會在內核啟動的過程中解壓縮並載入 cpio 格式的鏡像
  • klibc,這是一個用戶態的微型 libc 庫,專門為早期用戶空間設計,體積非常小。

這三個部分共同組合構成了早期用戶空間,可以把它理解成一個專門為了初始化各種設備優化的根文件系統,它的體積很小,可以直接塞進 Linux 內核鏡像中,不過大部分發行版還是會選擇將其作為一個單獨的文件,放在/boot目錄里,並在啟動時使用如下的命令載入它:

initrd = <position of initrd image>

載入完成並啟動到早期用戶空間之後,首先會執行該系統根目錄下的/init文件,它通常會是一個/bin/busybox的軟鏈接,busybox 筆者在過去也介紹過,集成了一系列實用工具,例如 sh、ls、mount 等等使用這些工具和腳本的組合,便可以準備好載入真正的根目錄所需的各種環境,之後再使用switch_root切換過去即可。

不過現如今,在大部分 Linux 發行版啟動時,能看到的輸出大部分都是由 systemd 輸出的了——這也是沒辦法的事,相比於遵守 KISS 哲學的 SysVinit,大而全的管理工具在如今更得人心,乃至於大部分的發行版中的 initramfs 中包含的 init 也都是 systemd 而不是 busybox 了。

實際上,在 Linux 2.6 之後,並不需要上面的參數也能載入早期用戶空間了,該版本的 Linux 引入了一種新的格式:initramfs,相較於使用鏡像文件格式的傳統 initrd,initramfs 是一個使用 gzip 壓縮後的 cpio 文件,它不僅能單獨存儲,還能集成在內核文件當中,並且相較於鏡像文件依賴於某種特定的文件系統(如 ext2),initramfs 是基於 ramfs 的全新實現,相對載入效率更高,並且速度更快。

在編譯 Linux 時即可使用 menuconfig 設置是否啟用 initramfs/initrd,以及是否把 initramfs 集成進內核里,默認前者是開啟的狀態,而後者在大多數 Linux 發行版中則是關閉的狀態,畢竟它會顯著增加 Linux 內核鏡像文件的體積,往往是得不償失的。

除了在編譯內核時,也可以使用工具直接製作 initramfs 文件,這裡用一個最簡單的 C 程序和 qemu 為例子來演示,下面是init.c文件的內容。

#include <stdio.h>

void main()
{
    printf("Hello World!");
    fflush(stdout);
    // 避免 init 程序結束導致 Kernel Panic
    while(true);
}

使用 gcc 靜態編譯該程序,防止因為缺少動態庫導致無法運行。

gcc -static init.c -o init

再使用 cpio 歸檔程序創建 initramfs 文件。

echo init | cpio -o --format=newc > initramfs

最後使用 qemu 指定內核和 initrd 文件啟動,測試它是否能正常啟動到初期用戶空間並運行 init 程序:

qemu-system-x86_64 -m 1024M -kernel zImage -initrd initramfs 

qemu 輸出Hello World並自旋,證明成功啟動到了 initramfs 中的根文件系統。

鏈式啟動

關於 Linux 的啟動過程,其實還有非常多有趣的部分,例如筆者在開頭一筆帶過的 Bootloader,像是 Linuxboot 這樣的項目宣稱能在伺服器上做到比傳統 UEFI 快得多的啟動速度,實際上它的底層實現與早期用戶空間的概念也有異曲同工之妙——先進入一個小型系統,進行設備的探測和初始化,最後用kexec載入內核,可惜的是由於kexec只支持 Linux,因此無法使用 Linuxboot 啟動 Windows 和其他 BSD 系統...不過事情真的如此嗎?

顯然,有人不這麼認為。來自 Trammell Hudson's Projects 的工程師們就整了個大活——用 Linux 鏈式啟動 Windows:Booting Windows with Linux,並且他們還給出了這麼做的理由:為了安全!是的,即便 Windows 11 帶來了 TPM(受信任的平台模塊)支持也並不足夠,因為在實際的生產環境中,伺服器可能需要載入存放在網路上的系統,而這就是 TPM 力所不能及的了,因此他們決定引入 Linux 載入安全模塊,再用它啟動 Windows。

當然,他們並沒有使用 kexec 直接啟動 Windows 內核——這需要對 Windows 內核進行相當程度的逆向,顯然不可能用於實際生產環境;也沒有選擇使用 Linux 載入 EFI 文件——雖然這些文檔都是開源的,實現一個這樣的載入器並不困難,但難點在於 kexec 默認並不支持 Windows 可執行文件(即 PE32)格式的 EFI 文件,還會涉及到關於 Linux 與 Windows 之間共享庫的兼容問題等等,因此他們也沒有選擇這個方案。最終,他們走了第三條路——鏈式引導。

這篇文章的內容非常豐富,以上的介紹只是一個簡單的引文,關於他們是如何實現鏈式引導的更多內容我會在之後的文章中向各位分享,或許會是對原文的簡單翻譯,也或許會有我個人的一些理解和補充,敬請期待!

參考文章
kernel parameters
early userpace support
ramfs, rootfs, initramfs

對這篇文章感覺如何?

太棒了
3
不錯
1
愛死了
1
不太好
0
感覺很糟
0

You may also like

Leave a reply

您的電子郵箱地址不會被公開。 必填項已用 * 標註

此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

More in:內核

內核

龍芯開始發布針對3A6000系列CPU的Linux補丁

儘管龍芯3A6000處理器尚未正式推出,但自去年以來的傳言將其定於在今年上半年推出,並有人聲稱這種性能提升可以與AMD Zen 3或英特爾Tiger Lake的性能水平相媲美。在3A6000系列推出之 […]
內核

Linux 共享庫的 soname 命名機制

Linux 有一套規則來命名系統中的每一個共享庫,它規定共享庫的文件命名規則如下:libname.so.x.y.z,即前綴"lib"+庫名稱+後綴".so"+三個數字組成的版本號,其中,x 表示主版本號,y 表示次版本號,z 表示發布版本號。SO-NAME 命名機制,就是把共享庫的文件名去掉次版本號和發布版本號,只保留主版本號。在 Linux 系統中,系統會為每個共享庫在它所在的目錄創建一個跟它的 」SO-NAME」 一樣的軟鏈接指向它。