initrd
initrd
Linux初始RAM磁碟(initrd)是在系統引導過程中掛載的一個臨時根文件系統,用來支持兩階段的引導過程。initrd文件中包含了各種可執行程序和驅動程序,它們可以用來掛載實際的根文件系統,然後再將這個 initrd RAM磁碟卸載,並釋放內存。在很多嵌入式Linux系統中,initrd 就是最終的根文件系統。
什麼是初始 RAM 磁碟
初始RAM磁碟(initrd)是在實際根文件系統可用之前掛載到系統中的一個初始根文件系統。initrd與內核綁定在一起,並作為內核引導過程的一部分進行載入。內核然後會將這個 initrd文件作為其兩階段引導過程的一部分來載入模塊,這樣才能稍後使用真正的文件系統,並掛載實際的根文件系統。
initrd 中包含了實現這個目標所需要的目錄和可執行程序的 最小集合,例如將內核模塊載入到內核中所使用的 insmod 工具。
在桌面或伺服器Linux 系統中,initrd 是一個 臨時的文件系統。其生存周期很短,只會用作到真實文件系統的一個橋樑。在沒有存儲設備的嵌入式系統中,initrd 是 永久的根文件系統。本文將對這兩種情況進行探索。
initrd 剖析
initrd 映像中包含了支持 Linux系統兩階段引導過程所需要的必要可執行程序和系統文件。
根據我們運行的 Linux 的版本不同,創建初始 RAM磁碟的方法也可能會有所不同。在 Fedora Core 3 之前,initrd 是使用loop設備 來構建的。loop設備是一個設備驅動程序,利用它可以將文件作為一個塊設備掛載到系統中,然後就可以查看這個文件系統中的內容了。在您的內核中可能並沒有loop設備,不過這可以通過內核配置工具(make menuconfig)選擇 Device Drivers > Block Devices> Loopback Device Support 來啟用。我們可以按照下面的方法來查看loop設備的內容(initrd文件的名字可能會稍有不同):
清單 1. 查看 initrd 的內容(適用於 FC3 之前的版本)
mkdir temp ; cd temp
cp /boot/initrd.img.gz .
gunzip initrd.img.gz
mount -t ext -oloopinitrd.img /mnt/initrd
ls -la /mnt/initrd
這樣我們就可以查看 /mnt/initrd 子目錄中的內容了,這就代表了 initrd文件的內容。注意,即使您的 initrd映像文件不是以 .gz 結尾,它也可能是一個壓縮文件,您可以給這個文件添加上 .gz後綴,然後再使用 gunzip 對其進行解壓。
從 Fedora Core 3 開始,默認的 initrd 映像變成了一個經過壓縮的cpio 歸檔文件。我們不用再使用loop設備來將 initrd 作為壓縮映像進行掛載,而是可以將其作為 cpio 歸檔文件來使用。要查看cpio 歸檔文件的內容,可以使用下面的命令:
清單 2. 查看 initrd 的內容(適用於 FC3 及其以後的版本)
mkdir temp ; cd temp
cp /boot/initrd-2.6.14.2.img initrd-2.6.14.2.img.gz
gunzip initrd-2.6.14.2.img.gz
cpio -i -d < initrd-2.6.14.2.img
結果會生成一個很小的根文件系統,如清單 3 所示。在 ./bin 目錄中有一組很少但卻非常必要的應用程序,包括 nash(即 not a shell,是一個腳本解釋器)、insmod(用來載入內核模塊)和 lvm(邏輯卷管理工具)。
清單 3. 默認的 Linux initrd目錄結構
ls -la
drwxr-xr-x 10 root root 4096 May 7 02:48 .
drwxr-x--- 15 root root 4096 May 7 00:54 ..
drwxr-xr-x 2 root root 4096 May 7 02:48 bin
drwxr-xr-x 2 root root 4096 May 7 02:48 dev
drwxr-xr-x 4 root root 4096 May 7 02:48 etc
-rwxr-xr-x 1 root root 812 May 7 02:48 init
-rw-r--r-- 1 root root 1723392 May 7 02:45 initrd-2.6.14.2.img
drwxr-xr-x 2 root root 4096 May 7 02:48 lib
drwxr-xr-x 2 root root 4096 May 7 02:48loopfs
drwxr-xr-x 2 root root 4096 May 7 02:48 proc
lrwxrwxrwx 1 root root 3 May 7 02:48 sbin -> bin
drwxr-xr-x 2 root root 4096 May 7 02:48 sys
drwxr-xr-x 2 root root 4096 May 7 02:48 sysroot
#
清單 3 中比較有趣的是 init 文件就在根目錄中。與傳統的 Linux 引導過程類似,這個文件也是在將 initrd 映像解壓到 RAM磁碟中時被調用的。在本文稍後我們將來探索這個問題。
cpio 命令
使用 cpio 命令,我們可以對 cpio 文件進行操作。cpio是一種文件格式,它簡單地使用文件頭將一組文件串接在一起。cpio 文件格式可以使用 ASCII 和二進位文件。為了保證可移植性,我們可以使用ASCII 格式。為了減小文件大小,我們可以使用二進位的版本。
下面讓我們回到最開始,來看一下 initrd 映像最初是如何構建的。對於傳統的Linux系統來說,initrd 映像是在 Linux 構建過程中創建的。有很多工具,例如mkinitrd,都可以用來使用必要的庫和模塊自動構建 initrd,從而用作與真實的根文件系統之間的橋樑。mkinitrd工具實際上就是一個 shell腳本,因此我們可以看到它究竟是如何來實現這個結果的。還有一個 YAIRD(即 Yet AnotherMkinitrd)工具,可以對 initrd 構建過程的各個方面進行定製。
手工構建定製的初始 RAM 磁碟
由於在很多基於 Linux 的嵌入式系統上沒有硬碟,因此 initrd也會作為這種系統上的永久根文件系統使用。清單 4 顯示了如何創建一個 initrd 映像文件。我使用了一個標準的 Linux桌面,這樣您即使沒有嵌入式平台,也可以按照下面的步驟來執行了。除了交叉編譯,其他概念(也適用於 initrd 的構建)對於嵌入式平台都是相同的。
清單 4. 創建定製 initrd 的工具(mkird)
#!/bin/bash
# Housekeeping...
rm -f /tmp/ramdisk.img
rm -f /tmp/ramdisk.img.gz
# Ramdisk Constants
RDSIZE=4000
BLKSIZE=1024
# Create an emptyramdisk image
dd if=/dev/zero of=/tmp/ramdisk.img bs=$BLKSIZE count=$RDSIZE
# Make it an ext2 mountable file system
/sbin/mke2fs -F -m 0 -b $BLKSIZE /tmp/ramdisk.img $RDSIZE
# Mount it so that we can populate
mount /tmp/ramdisk.img /mnt/initrd -t ext2 -oloop=/dev/loop0
# Populate the filesystem (subdirectories)
mkdir /mnt/initrd/bin
mkdir /mnt/initrd/sys
mkdir /mnt/initrd/dev
mkdir /mnt/initrd/proc
# Grabbusyboxand create the symbolic links
pushd /mnt/initrd/bin
cp /usr/local/src/busybox-1.1.1/busybox.
ln -sbusyboxash
ln -sbusyboxmount
ln -sbusyboxecho
ln -sbusyboxls
ln -sbusyboxcat
ln -sbusyboxps
ln -sbusyboxdmesg
ln -sbusyboxsysctl
popd
# Grab the necessary dev files
cp -a /dev/console /mnt/initrd/dev
cp -a /dev/ramdisk /mnt/initrd/dev
cp -a /dev/ram0 /mnt/initrd/dev
cp -a /dev/null /mnt/initrd/dev
cp -a /dev/tty1 /mnt/initrd/dev
cp -a /dev/tty2 /mnt/initrd/dev
# Equate sbin with bin
pushd /mnt/initrd
ln -s bin sbin
popd
# Create the init file
cat >> /mnt/initrd/linuxrc << EOF
#!/bin/ash
echo
echo "Simple initrd is active"
echo
mount -t proc /proc /proc
mount -t sysfs none /sys
/bin/ash --login
EOF
chmod +x /mnt/initrd/linuxrc
# Finish up...
umount /mnt/initrd
gzip -9 /tmp/ramdisk.img
cp /tmp/ramdisk.img.gz /boot/ramdisk.img.gz
initrd Linux 發行版
Minimax 是一個開放源碼項目,其設計目標是成為一個全部封裝在 initrd 中的Linux 發行版。它的大小是 32MB,為了盡量小,它使用了 BusyBox 和 uClibc。除了非常小之外,它還使用了 2.6 版本的Linux內核,並提供了很多有用的工具。
為了創建 initrd,我們最開始創建了一個空文件,這使用了/dev/zero(一個由零組成的碼流)作為輸入,並將其寫入到ramdisk.img 文件中。所生成的文件大小是 4MB(4000 個 1K大小的塊)。然後使用 mke2fs 命令在這個空文件上創建了一個 ext2(即 secondextended)文件系統。現在這個文件變成了一個 ext2 格式的文件系統,我們使用loop設備將這個文件掛載到 /mnt/initrd上了。在這個掛載點上,我們現在就有了一個目錄,它以 ext2文件系統的形式呈現出來,我們可以對自己的 initrd文件進行拼裝了。接下來的腳本提供了這種功能。
下一個步驟是創建構成根文件系統所需要的子目錄:/bin、/sys、/dev 和 /proc。這裡只列出了所需要的目錄(例如沒有庫),但是其中包含了很多功能。
ext2文件系統的替代品
儘管 ext2 是一種通用的 Linux文件系統格式,但是還有一些替代品可以減小initrd映像文件以及所掛載上來的文件系統的大小。這種文件系統的例子有 romfs(ROM文件系統)、cramfs(壓縮 ROM文件系統)和 squashfs(高度壓縮只讀文件系統)。如果我們需要暫時將數據寫入文件系統中,ext2可以很好地實現這種功能。最後,e2compr 是 ext2文件系統驅動程序的一個擴展,可以支持在線壓縮。
為了可以使用根文件系統,我們使用了BusyBox。這個工具是一個單一映像,其中包含了很多在 Linux系統上通常可以找到的工具(例如 ash、awk、sed、insmod等)。BusyBox的優點是它將很多工具打包成一個文件,同時還可以共享它們的通用元素,這樣可以極大地減少映像文件的大小。這對於嵌入式系統來說非常理想。將BusyBox 映像從自己的源目錄中拷貝到自己根目錄下的 /bin 目錄中。然後創建了很多符號鏈接,它們都指向 BusyBox工具。BusyBox 會判斷所調用的是哪個工具,並執行這個工具的功能。我們在這個目錄中創建了幾個鏈接來支持 init腳本(每個命令都是一個指向 BusyBox 的鏈接。)
下一個步驟是創建幾個特殊的設備文件。我從自己當前的 /dev 子目錄中直接拷貝了這些文件,這使用了 -a 選項(歸檔)來保留它們的屬性。
倒數第二個步驟是生成linuxrc 文件。在內核掛載 RAM磁碟之後,它會查找init 文件來執行。如果沒有找到 init 文件,內核就會調用linuxrc文件作為自己的啟動腳本。我們在這個文件中實現對環境的基本設置,例如掛載 /proc文件系統。除了 /proc 之外,我還掛載了 /sys文件系統,並向終端列印一條消息。最後,我們調用了 ash(一個 Bourne Shell的克隆),這樣就可以與根文件系統進行交互了。linuxrc 文件然後使用 chmod 命令修改成可執行的。
最後,我們的根文件系統就完成了。我們將其卸載掉,然後使用 gzip 對其進行壓縮。所生成的文件(ramdisk.img.gz)被拷貝到 /boot 子目錄中,這樣就可以通過 GNU GRUB 對其進行載入了。
要構建初始 RAM磁碟,我們可以簡單地調用 mkird,這樣就會自動創建這個映像文件,並將其拷貝到 /boot 目錄中。
測試定製的初始 RAM 磁碟
Linux內核中對 initrd 的支持
對於 Linux內核來說,要支持初始 RAM磁碟,內核必須要使用 CONFIG_BLK_DEV_RAM 和 CONFIG_BLK_DEV_INITRD 選項進行編譯。
新的 initrd 映像現在已經在 /boot目錄中了,因此下一個步驟是使用默認的內核來對其進行測試。現在我們可以重新啟動 Linux 系統了。在出現 GRUB 界面時,按 C 鍵啟動GRUB 中的命令行工具。我們現在可以與 GRUB 進行交互,從而定義要載入哪個內核和 initrd映像文件。kernel命令讓我們可以指定內核文件,initrd 命令可以用來指定 initrd 映像文件。在定義好這些參數之後,就可以使用 boot命令來引導內核了,如清單 5 所示。
清單 5. 使用 GRUB 手工引導內核和 initrd
GNU GRUB version 0.95 (638K lower / 97216K upper memory)
[ Minimal BASH-like line editing is supported. For the first word, TAB
lists possible command completions. Anywhere else TAB lists the possible
completions of a device/filename. ESC at any time exits.]
grub> kernel /bzImage-2.6.1
[Linux-bzImage, setup=0x1400, size=0x29672e]
grub> initrd /ramdisk.img.gz
[Linux-initrd @ 0x5f2a000, 0xb5108 bytes]
grub> boot
Uncompressing Linux... OK, booting the kernel.
在內核啟動之後,它會檢查是否有 initrd映像文件可用(稍後會更詳細介紹),然後將其載入,並將其掛載成根文件系統。在清單 6 中我們可以看到這個 Linux啟動過程最後的樣子。在啟動之後,ash shell 就可以用來輸入命令了。在這個例子中,我們將瀏覽一下根文件系統的內容,並查看一下虛擬proc 文件系統中的內容。我們還展示了如何通過 touch 命令在文件系統中創建文件。注意所創建的第一個進程是linuxrc(通常都是init)。
清單 6. 使用簡單的 initrd 引導 Linux 內核
...
md: Autodetecting RAID arrays
md: autorun
md: ... autorun DONE.
RAMDISK: Compressed image found at block 0
VFS: Mounted root (ext2 file system).
Freeing unused kernel memory: 208k freed
/ $ ls
bin etclinuxrc proc sys
dev lib lost+found sbin
/ $ cat /proc/1/cmdline
/bin/ash/linuxrc
/ $ cd bin
/bin $ ls
ash cat echo mount sysctl
busybox dmesg ls ps
/bin $ touch zfile
/bin $ ls
ash cat echo mount sysctl
busybox dmesg ls ps zfile
使用初始 RAM 磁碟來引導系統
現在我們已經了解了如何構建並使用定製的初始 RAM磁碟,本節將探索內核是如何識別 initrd 並將其作為根文件系統進行掛載的。我們將介紹啟動鏈中的幾個主要函數,並解釋一下到底在進行什麼操作。
引導載入程序,例如 GRUB,定義了要載入的內核,並將這個內核映像以及相關的 initrd 拷貝到內存中。我們可以在 Linux內核源代碼目錄中的 ./init 子目錄中找到很多這種功能。
在內核和 initrd映像被解壓並拷貝到內存中之後,內核就會被調用了。它會執行不同的初始化操作,最終您會發現自己到了init/main.c:init()(subdir/file:function)函數中。這個函數執行了大量的子系統初始化操作。此處會執行一個對init/do_mounts.c:prepare_namespace() 的調用,這個函數用來準備名稱空間(掛載 dev文件系統、RAID或 md、設備以及最後的 initrd)。載入 initrd 是通過調用init/do_mounts_initrd.c:initrd_load() 實現的。
initrd_load()函數調用了init/do_mounts_rd.c:rd_load_image(),它通過調用init/do_mounts_rd.c:identify_ramdisk_image() 來確定要載入哪個 RAM磁碟。這個函數會檢查映像文件的 magic 號來確定它是 minux、etc2、romfs、cramfs 或 gzip 格式。在返回到initrd_load_image 之前,它還會調用 init/do_mounts_rd:crd_load()。這個函數負責為 RAM磁碟分配空間,並計算循環冗餘校驗碼(CRC),然後對 RAM磁碟映像進行解壓,並將其載入到內存中。現在,我們在一個適合掛載的塊設備中就有了這個 initrd 映像。
現在使用一個 init/do_mounts.c:mount_root()調用將這個塊設備掛載到根文件系統上。它會創建根設備,並調用 init/do_mounts.c:mount_block_root()。在這裡調用init/do_mounts.c:do_mount_root(),後者又會調用 fs/namespace.c:sys_mount()來真正掛載根文件系統,然後 chdir 到這個文件系統中。這就是我們在清單 6 中所看到的熟悉消息 VFS: Mounted root(ext2 file system). 的地方。
最後,返回到 init 函數中,並調用init/main.c:run_init_process。這會導致調用 execve 來啟動 init 進程(在本例中是/linuxrc)。linuxrc 可以是一個可執行程序,也可以是一個腳本(條件是它有腳本解釋器可用)。
這些函數的調用層次結構如清單 7 所示。儘管此處並沒有列出拷貝和掛載初始 RAM磁碟所涉及的所有函數,但是這足以為我們提供一個整體流程的粗略框架。
清單 7. initrd 載入和掛載過程中所使用的主要函數的層次結構
init/main.c:init
init/do_mounts.c:prepare_namespace
init/do_mounts_initrd.c:initrd_load
init/do_mounts_rd.c:rd_load_image
init/do_mounts_rd.c:identify_ramdisk_image
init/do_mounts_rd.c:crd_load
lib/inflate.c:gunzip
init/do_mounts.c:mount_root
init/do_mounts.c:mount_block_root
init/do_mounts.c:do_mount_root
fs/namespace.c:sys_mount
init/main.c:run_init_process
execve
無盤引導
與嵌入式引導的情況類似,本地磁碟(軟盤或 CD-ROM)對於引導內核和 ramdisk根文件系統來說都不是必需的。DHCP(Dynamic Host Configuration Protocol)可以用來確定網路參數,例如 IP地址和子網掩碼。TFTP(Trivial File Transfer Protocol)可以用來將內核映像和初始ramdisk映像傳輸到本地設備上。傳輸完成之後,就可以引導 Linux內核並掛載 initrd 了,這與本地映像引導的過程類似。
壓縮 initrd
在構建嵌入式系統時,我們可能希望將 initrd映像文件做得儘可能小,這其中有一些技巧需要考慮。首先是使用 BusyBox(本文中已經展示過了)。BusyBox 可以將數 MB 的工具壓縮成幾百 KB。
在這個例子中,BusyBox映像是靜態鏈接的,因此它不需要其他庫。然而,如果我們需要標準的 C 庫(我們自己定製的二進位可能需要這個庫),除了巨大的 glibc之外,我們還有其他選擇。第一個較小的庫是 uClibc,這是為對空間要求非常嚴格的系統準備的一個標準 C 庫。另外一個適合空間緊張的環境的庫是dietlib。要記住我們需要使用這些庫來重新編譯想在嵌入式系統中重新編譯的二進位文件,因此這需要額外再做一些工作(但是這是非常值得的)。
結束語
初始 RAM磁碟最初是設計用來通過一個臨時根文件系統來作為內核到最終的根文件系統之間的橋樑。initrd 對於在嵌入式系統中載入到 RAM磁碟里的非持久性根文件系統來說也非常有用