最近在着手给Sparrow设计一个简单的ram文件系统,想借鉴一下Linux的思路。发现对initrd和initramfs两个概念搞不太清楚,恍惚觉得它们也都是内存文件系统,但又说不清楚其间的区别。今天特意查了一下,做个总结。
的确,它们的相同之处在于,都是用于内核启动阶段的临时内存文件系统。比较一下,主要有这样几点区别:
1. Initrd本身是一个ram disk,是一个虚拟盘的概念,被挂载到设备目录(dev/ram0);initramfs本身就是一种文件系统,直接被作为临时根目录挂载。
2. Initrd由于是设备的概念,所以需要另外的文件系统来管理文件,它依赖于ext2文件系统;而initramfs有自己的文件系统格式CPIO。
3. 挂载好之后,内核首先执行的文件不同。对于Initrd,内核先执行/linuxrc;对于initramfs,行执行/init。
总的来说,initramfs比initrd先进一些,前者是后者的替代品。
这里有一篇比较好的介绍两者区别的文章(http://blog.linux.org.tw/~jserv/archives/001954.html),不过好像这个网站不太稳定,所以把主要的内容节选出来,作为记录:
自 1991 年 Linux 問世以來,資訊技術的應用有了極大的轉變,筆者之前的文章 [探索 Linux bootloader 的佳作] 與 [kboot 初探與模擬驗證] 約略提及光是開機本身的設計來說,就有多種衝擊與需求,隨著 Linux 走出個人電腦領域,在嵌入式系統應用上,更是五花八門。本文所探討的 initramfs,衍生自 Linux kernel 的 initrd,理解其設計需求是先行的準備。initrd 字面上的意思就是 "boot loader initialized RAM disk",換言之,這是一塊特殊的 RAM disk,在載入 Linux kernel 前,由 boot loader 予以初始化,具體動作就是從特定的儲存裝置中載入 initrd 到 RAM 中 (由啟動參數 "initrd=" 指定 image 的實體或邏輯位置),隨後 linux kernel 被載入並執行時,會優先處理置放 initrd 的記憶體空間,而這個空間基本上也有檔案系統,通常會包含 init 等程式,故可用以掛入某些特別的驅動程式,比方說 SCSI,完成階段性目標後,kernel 會將真正的 root file system 掛載,並執行 /sbin/init 程式。
話說回來,我們為何需要此等迂迴的開機途徑呢?原因是,root file system (由啟動參數 "root=" 所指定,以下簡稱 rootfs) 所在的儲存裝置很可能極難尋找,比方說 SCSI 裝置就需要複雜且耗時的程序,若用 RAID 系統更是需要看配置情況而定,同樣的問題也發生在 USB storage 上,因為 kernel 得花上更長的等待與配置時間,或說遠端掛載 rootfs,不僅得處理網路裝置的問題,甚至還得考慮相關的伺服器認證、通訊往返時間等議題。更重要的是,我們可在 initrd 放置某些特別的程式,一來作為掛載 rootfs 作準備,比方說硬體初始化、解密、解壓縮等等,二來提示使用者或系統管理員目前的狀態,這對於消費性電子產品來說,有很大的意義。整體來說,如果能增加開機的彈性 (比方說配合簡單的 shell script 即可達成 USB/SCSI 初始化動作,若透過 kernel code 實做,恐怕上百千行是免不掉的),又能適度降低 kernel image 本身的設計複雜度與空間使用量,採取 initrd 是很不錯的方式,所以幾乎各大 Linux distribution 都有提供 initrd,以解決在不同硬體、不同裝置上開機的技術議題,也能確保一片 CD-ROM/DVD 可裝入多種個人電腦系統,也可支援 [bootsplash] 一類顯示開機動畫的程式。
具體來說,initrd 提供了「兩階段開機」程序。首先,一切都還是在 kernel mode,由 kernel 完成與硬體相關的初始化工作,接著,在適當的時機點,當 kernel 讀取並掛載 initrd 所在記憶體空間的檔案系統後,kernel 首次從 kernel space 切入 user space,以執行存放於 RAM disk 中的 init 程式,當然,這需要完整的執行環境 (比方說 C runtime 或必要的 program loader 等),另外,也得確定 rootfs 可被 kernel 所找到並正確掛載。待第一階段的 initrd 步入尾聲後,再回到 kernel mode,initrd 所在的記憶體空間也會適度被釋放 (依據組態而定),這才進入第二階段,也就是執行真正的 rootfs 中的 init 程式。在 Linux kernel 2.4 中,initrd 大致的處理流程如下:(方括號表示主要的執行單元)
- [boot loader] Boot loader 依據預先設定的條件,將 kernel 與 initrd 這兩個 image 載入到 RAM
- [boot loader -> kernel] 完成必要的動作後,準備將執行權交給 Linux kernel
- [kernel] 進行一系列初始化動作,initrd 所在的記憶體被 kernel 對應為 /dev/initrd 裝置設備,透過 kernel 內部的 decompressor (gzip 解壓縮) 解開該內容並複製到 /dev/ram0 裝置設備上
- [kernel] Linux 以 R/W (可讀寫) 模式將 /dev/ram0 掛載為暫時性的 rootfs
- [kernel-space -> user-space] kernel 準備執行 /dev/ram0 上的 /linuxrc 程式,並切換執行流程
- [user space] /linuxrc 與相關的程式處理特定的操作,比方說準備掛載 rootfs 等
- [user-space -> kernel-space] /linuxrc 執行即將完畢,執行權轉交給 kernel
- [kernel] Linux 掛載真正的 rootfs 並執行 /sbin/init 程式
- [user space] 依據 Linux distribution 規範的流程,執行各式系統與應用程式
值得一提的是,以上「兩階段開機」是 initrd 提出的彈性開機流程,在真實的應用中,也可能從未需要掛載真正的 rootfs,換言之,只是把系統當作都在 RAM disk 上運作,或者永遠都在 initrd 所引導執行的 /linuxrc 程序中執行 (注意:kernel 永遠保留 PID=1 作為 init process 識別,而 /linuxrc 執行的 PID 必非為 1),在許多裝置如智慧型手機,都是行之有年的,不過這不影響我們後續的探討。
Linux Kernel 的發展文化就是願意捨棄既有實做,大膽採用新的途徑 (在符合國際規格的前提下),Linux 2.6 的 initramfs 之所以提出,就是要修正 initrd 的種種技術問題。問題在哪呢?首先,回顧剛剛探討的流程,initrd RAM disk 對 kernel 來說,本身是個真實的 block device,為了建構存放其中的檔案 (最起碼要有 /linuxrc),通常我們需要 ext2 一類的檔案系統 (建議)。所以,就建構如此的 initrd image 來看,通常會透過 mkfs.ext2 與 losetup (功能:"set up and control loop devices") 等工具建立 loopback device 並編修,所以自然需面對以下問題:
- initrd 必須綁定某個檔案系統實做,如 ext2,可是多數的情況下,我們根本不需要在此階段擁有完整的實做
- /dev/initrd block device 建構時即有空間限制,維護繁瑣
- 運作於 initrd 階段,檔案操作實際上是不斷將 /dev/initrd (對應於某段記憶體) 對應到可存取檔案系統的記憶位址,做了不必要的資源消耗
Kernel 文件 ( Documentation/filesystems/ramfs-rootfs-initramfs.txt ) 更指出:
基於上述資源使用與效能考量,原本 ramdisk 途徑就被標示為「老舊」,而 initramfs 的提出,則是基於更簡單有效率的 ramfs 與新的處理方式。
回到 initrd ramdisk,事實上,原本的設計甚至更加浪費記憶體,因為 Linux 在設計上就會盡可能將讀入/寫入自 block device 的檔案或目錄予以 cache,所以,Linux 會自 ramdisk 中複製資料到 page cache 與 dentry cache,如此往返,徒增資源使用的浪費,這一切問題的根源就是將 initrd 以 block device 來操作的本質使然。Linus Torvalds 為此提出一個想法:
基於這些想法,Linus Torvalds 實做了 ramfs,隨後在其他核心開發者的改進下,成為 tmpfs,支援寫入 swap 空間與限制記憶體使用量等特徵。而,initramfs 就是建構於 tmpfs 的基礎上。採取此途徑的效益就是,檔案系統可自行調整空間使用量,以符合所需資料儲存的空間,同時,也不再會有重複的 block device 與 cache 資料,因為跟本不需要,更重要的是,這樣的檔案系統實做,其實就只是 cache 機制的延伸,沒有太多新的程式碼,所以系統可保持簡單明暸。以下是對 initrd 與 initramfs 的概念性比較:
| INITRD | INITRAMFS |
---|---|---|
Image | 壓縮過的檔案系統 (如 ext2 gzip) | 封裝過的檔案 (cpio gzip) |
實做途徑 | block device (RAM disk) | tmpfs |
首先執行的程式 | /linuxrc | /init |
掛載 rootfs 方式 | 將欲載入的 rootfs 掛載於某個目錄,再 pivot_root 切換 rootfs | 使用 switch_root |
前面的段落已說明這兩者對於記憶體存取與檔案操作的落差,同時也提及實做途徑,接下來的重點是這兩者如何看待真正的 rootfs。如同前述所及,Linux kernel 2.4 中,initrd 可被視為起始參數 "root=" 的先前處理機制,透過一系列的程序,協助 kernel 找到最終的 rootfs,並一舉掛載進系統,不過,過去的設計其實做了一個假設:「真正的 rootfs 所在的裝置是 block device,同時 initrd 絕非是真正的 rootfs」,這也是為何要讓 kernel 在第一次準備切入 user-space 時,是執行 /linuxrc,而非 /init 或 /sbin/init,因為後者的 PID 恆為 1 且不可被 kill (終止),但前者因為只是過度的存在,隨時仍可被 kill。
而在 Linux 2.6 引入 initramfs 的設計後,上述彆扭的假設與處理方式就不復存在,不再區隔「真正」的 rootfs 是如何「存在」,也就是一開機,kernel 就執行位於 initramfs 中的 /init,作為 PID=1 的 init process,僅以 switch_root 作 rootfs 的重新定位罷了 (選擇性)。正因為這樣的特性,核心開發者也將 initramfs 的行為稱為 [Early User Space],Jeff Garzik 於 2002 年十一月發表於 lkml 的文章 [initramfs merge, part 1 of N] 提到他的願景:
核心開發者很喜歡彼此取笑,這裡提到的 [Al Viro] 是位知名的 kernel hacker,常常為了捍衛核心設計的一致性與許多開發者對立。這意思就是說,藉由 Early userspace 整合到核心設計後,原本很不容易處理的開機模式,比方說 LVM (Linux Volume Manager)、網路開機、特別儲存裝置的開機等,都可交由 user-space 的應用程式專門處理,相對來說,kernel 就不必過度涉入,長遠來說,對於發展的分工、降低系統複雜度,以及提高可性賴度,均有很大的助益。
基於 initramfs / Early userspace 的想法,核心開發者又思考為何不將過去難以有效維護但又非得存在不可的程式碼,比方說 do_mount 這一類用以實做掛載特定裝置或邏輯儲存設備的功能,全面轉交給 user-space 的程式去執行呢?這樣 kernel 可專心提昇功能或者效能的改進。為此,以 H. Peter Anvin 為首的核心開發者引入 [klibc] 與 kinit,前者 (至少目標上) 是最小的 C library 實做,用來支持後者所需 (定位與 [dietlibc] 或 [uclibc] 一類精巧但通用性的 libc 實做不同),而 kinit 就是將前述原本在核心實做的程式 (很難偵錯且分析的 kernel code) 拉出到 user-space 中,他於 2006 年六月提交的 patch [kinit: replacement for in-kernel do_mount, ipconfig, nfsroot] 就展現了將不同的檔案系統 (cramfs, ext2, ext3, jfs, lvm2, minixfs, reiserfs, romfs, xfs, ...) 予以掛載 (即 user-space 的 do_mount)、ipconfig (bootp, dhcp)、nfsmount 等等,整合到 kinit 程式中一併處理,kernel image 可因此大幅縮減。