Ramfs、rootfs 和 initramfs

2005 年 10 月 17 日

作者:

Rob Landley <rob@landley.net>

什麼是 ramfs?

Ramfs 是一個非常簡單的檔案系統,它將 Linux 的磁碟快取機制(頁面快取和 dentry 快取)匯出為一個可動態調整大小的基於 RAM 的檔案系統。

通常,所有檔案都由 Linux 快取在記憶體中。 從後備儲存(通常是檔案系統掛載的塊裝置)讀取的資料頁面會被保留以備再次需要,但標記為乾淨(可釋放),以防虛擬記憶體系統需要該記憶體用於其他目的。 類似地,寫入檔案的資料一旦寫入後備儲存就被標記為乾淨,但為了快取目的而保留,直到 VM 重新分配記憶體。 類似的機制(dentry 快取)大大加快了對目錄的訪問。

使用 ramfs,沒有後備儲存。 寫入 ramfs 的檔案像往常一樣分配目錄項和頁面快取,但沒有地方可以寫入它們。 這意味著頁面永遠不會被標記為乾淨,因此當 VM 尋找回收記憶體時,它們無法被 VM 釋放。

實現 ramfs 所需的程式碼量很小,因為所有工作都由現有的 Linux 快取基礎架構完成。 基本上,您正在將磁碟快取掛載為檔案系統。 因此,ramfs 不是可以透過 menuconfig 刪除的可選元件,因為節省的空間可以忽略不計。

ramfs 和 ramdisk:

較舊的“ram disk”機制從 RAM 區域建立一個合成塊裝置,並將其用作檔案系統的後備儲存。 這個塊裝置的大小是固定的,因此掛載在其上的檔案系統的大小也是固定的。 使用 ram disk 還需要不必要地將記憶體從假塊裝置複製到頁面快取(並將更改複製回來),以及建立和銷燬目錄項。 此外,它還需要一個檔案系統驅動程式(例如 ext2)來格式化和解釋此資料。

與 ramfs 相比,這會浪費記憶體(和記憶體匯流排頻寬),為 CPU 創造不必要的工作,並汙染 CPU 快取。 (有一些技巧可以透過操作頁表來避免這種複製,但它們非常複雜,而且最終與複製一樣昂貴。) 更重要的是,ramfs 所做的所有工作都必須_無論如何_發生,因為所有檔案訪問都透過頁面和目錄項快取。 RAM 磁碟根本沒有必要; ramfs 在內部要簡單得多。

ramdisk 變得半過時的另一個原因是 loopback 裝置的引入提供了一種更靈活和方便的方式來建立合成塊裝置,現在是從檔案而不是從記憶體塊。 有關詳細資訊,請參閱 losetup (8)。

ramfs 和 tmpfs:

ramfs 的一個缺點是,您可以一直向其中寫入資料,直到填滿所有記憶體,並且 VM 無法釋放它,因為 VM 認為檔案應該寫入後備儲存(而不是交換空間),但 ramfs 沒有任何後備儲存。 因此,只有 root(或受信任的使用者)才應被允許對 ramfs 掛載點進行寫訪問。

建立了一個名為 tmpfs 的 ramfs 派生版本,以新增大小限制以及將資料寫入交換空間的能力。 普通使用者可以被允許對 tmpfs 掛載點進行寫訪問。 有關更多資訊,請參見 Tmpfs

什麼是 rootfs?

Rootfs 是 ramfs(或 tmpfs,如果已啟用)的一個特殊例項,它始終存在於 2.6 系統中。 您無法解除安裝 rootfs,原因與您無法終止 init 程序的原因大致相同; 與其使用特殊程式碼來檢查和處理空列表,核心更小更簡單,只需確保某些列表不會變空。

大多數系統只是在 rootfs 上掛載另一個檔案系統並忽略它。 空 ramfs 例項佔用的空間很小。

如果啟用了 CONFIG_TMPFS,則 rootfs 預設將使用 tmpfs 而不是 ramfs。 要強制使用 ramfs,請將“rootfstype=ramfs”新增到核心命令列。

什麼是 initramfs?

所有 2.6 Linux 核心都包含一個 gzipped “cpio”格式的存檔,該存檔在核心啟動時被提取到 rootfs 中。 提取後,核心會檢查 rootfs 是否包含檔案“init”,如果包含,則將其作為 PID 1 執行。 如果找到,此 init 程序負責啟動系統的其餘部分,包括定位和掛載實際的根裝置(如果有)。 如果在將嵌入式 cpio 存檔提取到 rootfs 後,rootfs 不包含 init 程式,則核心將回退到舊程式碼以定位和掛載根分割槽,然後從該分割槽中執行 /sbin/init 的某個變體。

所有這些都與舊的 initrd 有幾個不同之處

  • 舊的 initrd 始終是一個單獨的檔案,而 initramfs 存檔連結到 Linux 核心映像中。 (linux-*/usr 目錄專門用於在構建期間生成此存檔。)

  • 舊的 initrd 檔案是一個 gzipped 檔案系統映像(採用某種檔案格式,例如 ext2,需要將驅動程式構建到核心中),而新的 initramfs 存檔是一個 gzipped cpio 存檔(類似於 tar,但更簡單,請參見 cpio(1) 和 initramfs 緩衝區格式)。 核心的 cpio 提取程式碼不僅非常小,而且是 __init 文字和資料,可以在啟動過程中丟棄。

  • 舊 initrd 執行的程式(稱為 /initrd,而不是 /init)進行了一些設定,然後返回到核心,而 initramfs 中的 init 程式不希望返回到核心。 (如果 /init 需要移交控制權,它可以將 / 與新的根裝置一起覆蓋掛載,然後執行另一個 init 程式。請參見下面的 switch_root 實用程式。)

  • 切換另一個根裝置時,initrd 將 pivot_root,然後解除安裝 ramdisk。 但是 initramfs 是 rootfs:您既不能 pivot_root rootfs,也不能解除安裝它。 而是從 rootfs 中刪除所有內容以釋放空間(find -xdev / -exec rm ‘{}’ ‘;’),使用新的根覆蓋掛載 rootfs(cd /newmount; mount --move . /; chroot .),將 stdin/stdout/stderr 連線到新的 /dev/console,然後執行新的 init。

    由於這是一個非常挑剔的過程(並且涉及在您可以執行命令之前刪除它們),因此 klibc 軟體包引入了一個輔助程式(utils/run_init.c)來為您完成所有這些操作。 大多數其他軟體包(例如 busybox)已將此命令命名為“switch_root”。

填充 initramfs:

2.6 核心構建過程始終建立一個 gzipped cpio 格式的 initramfs 存檔,並將其連結到生成的核心二進位制檔案中。 預設情況下,此存檔是空的(在 x86 上消耗 134 位元組)。

config 選項 CONFIG_INITRAMFS_SOURCE(位於 menuconfig 的 General Setup 中,並且位於 usr/Kconfig 中)可用於指定 initramfs 存檔的源,該源將自動合併到生成的二進位制檔案中。 此選項可以指向現有的 gzipped cpio 存檔、包含要存檔的檔案的目錄或文字檔案規範,例如以下示例

dir /dev 755 0 0
nod /dev/console 644 0 0 c 5 1
nod /dev/loop0 644 0 0 b 7 0
dir /bin 755 1000 1000
slink /bin/sh busybox 777 0 0
file /bin/busybox initramfs/busybox 755 0 0
dir /proc 755 0 0
dir /sys 755 0 0
dir /mnt 755 0 0
file /init initramfs/init.sh 755 0 0

執行“usr/gen_init_cpio”(在核心構建之後)以獲取記錄上述檔案格式的用法訊息。

配置檔案的優點之一是,不需要 root 訪問許可權即可在新存檔中設定許可權或建立裝置節點。 (請注意,這兩個示例“file”條目希望在 linux-2.6.* 目錄下的一個名為“initramfs”的目錄中找到名為“init.sh”和“busybox”的檔案。 有關更多詳細資訊,請參見 Early userspace support。)

核心不依賴於外部 cpio 工具。 如果您指定一個目錄而不是配置檔案,則核心的構建基礎架構會從該目錄建立一個配置檔案(usr/Makefile 呼叫 usr/gen_initramfs.sh),並繼續使用該配置檔案打包該目錄(透過將其饋送到從 usr/gen_init_cpio.c 建立的 usr/gen_init_cpio)。 核心的構建時 cpio 建立程式碼是完全獨立的,並且核心的啟動時提取器也是(顯然)獨立的。

您可能需要安裝外部 cpio 實用程式來建立或提取您自己的預先準備好的 cpio 檔案以饋送到核心構建(而不是配置檔案或目錄)。

以下命令列可以將 cpio 映像(透過上述指令碼或核心構建)提取回其元件檔案

cpio -i -d -H newc -F initramfs_data.cpio --no-absolute-filenames

以下 shell 指令碼可以建立一個預構建的 cpio 存檔,您可以使用它來代替上述配置檔案

#!/bin/sh

# Copyright 2006 Rob Landley <rob@landley.net> and TimeSys Corporation.
# Licensed under GPL version 2

if [ $# -ne 2 ]
then
  echo "usage: mkinitramfs directory imagename.cpio.gz"
  exit 1
fi

if [ -d "$1" ]
then
  echo "creating $2 from $1"
  (cd "$1"; find . | cpio -o -H newc | gzip) > "$2"
else
  echo "First argument must be a directory"
  exit 1
fi

注意

cpio 手冊頁包含一些錯誤的建議,如果您遵循這些建議,將會破壞您的 initramfs 存檔。 它說“生成檔名列表的典型方法是使用 find 命令; 您應該為 find 提供 -depth 選項,以最大限度地減少對不可寫或不可搜尋的目錄的許可權問題。” 建立 initramfs.cpio.gz 映像時不要這樣做,它將不起作用。 Linux 核心 cpio 提取器不會在不存在的目錄中建立檔案,因此目錄條目必須在位於這些目錄中的檔案之前。 上面的指令碼會以正確的順序獲取它們。

外部 initramfs 映像:

如果核心啟用了 initrd 支援,則可以將外部 cpio.gz 存檔傳遞到 2.6 核心中,以代替 initrd。 在這種情況下,核心將自動檢測型別(initramfs,而不是 initrd)並在嘗試執行 /init 之前將外部 cpio 存檔提取到 rootfs 中。

這具有 initramfs 的記憶體效率優勢(沒有 ramdisk 塊裝置),但具有 initrd 的單獨打包優勢(如果您希望從 initramfs 執行非 GPL 程式碼,而不會將其與 GPL 許可的 Linux 核心二進位制檔案混淆,這很好)。

它也可以用於補充核心的內建 initramfs 映像。 外部存檔中的檔案將覆蓋內建 initramfs 存檔中的任何衝突檔案。 一些分發者也更喜歡使用特定於任務的 initramfs 映像自定義單個核心映像,而無需重新編譯。

initramfs 的內容:

initramfs 存檔是一個完整的獨立 Linux 根檔案系統。 如果您還不瞭解啟動和執行最小根檔案系統所需的共享庫、裝置和路徑,請參閱以下參考資料

“klibc”軟體包 (https://kernel.linux.club.tw/pub/linux/libs/klibc) 旨在成為一個微小的 C 庫,用於靜態連結早期使用者空間程式碼,以及一些相關的實用程式。 它已獲得 BSD 許可。

我自己使用 uClibc (https://www.uclibc.org) 和 busybox (https://www.busybox.net)。 它們分別獲得 LGPL 和 GPL 許可。 (busybox 1.3 版本計劃釋出一個獨立的 initramfs 軟體包。)

理論上您可以使用 glibc,但它不太適合像這樣的小型嵌入式用途。 (一個靜態連結到 glibc 的“hello world”程式超過 400k。使用 uClibc,它是 7k。另請注意,glibc dlopens libnss 執行名稱查詢,即使以其他方式靜態連結。)

一個好的第一步是讓 initramfs 執行一個靜態連結的“hello world”程式作為 init,並在 qemu (www.qemu.org) 或使用者模式 Linux 等模擬器下對其進行測試,如下所示

cat > hello.c << EOF
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
  printf("Hello world!\n");
  sleep(999999999);
}
EOF
gcc -static hello.c -o init
echo init | cpio -o -H newc | gzip > test.cpio.gz
# Testing external initramfs using the initrd loading mechanism.
qemu -kernel /boot/vmlinuz -initrd test.cpio.gz /dev/zero

除錯普通根檔案系統時,能夠使用“init=/bin/sh”啟動是很不錯的。 initramfs 等效項是“rdinit=/bin/sh”,它同樣有用。

為什麼選擇 cpio 而不是 tar?

此決定是在 2001 年 12 月做出的。討論從這裡開始

併產生了第二個執行緒(專門討論 tar 與 cpio),從這裡開始

快速而骯髒的摘要版本(不能代替閱讀上述執行緒)是

  1. cpio 是一種標準。 它有數十年的歷史(來自 AT&T 時代),並且已在 Linux 上廣泛使用(在 RPM 中,Red Hat 的裝置驅動程式磁碟)。 這是 Linux Journal 1996 年關於它的文章

    它不像 tar 那樣受歡迎,因為傳統的 cpio 命令列工具需要_真正_可怕_的命令列引數。 但這並沒有說明存檔格式的任何一種方式,並且有替代工具,例如

  2. 核心選擇的 cpio 存檔格式比任何(實際上是幾十種)各種 tar 存檔格式都更簡單、更乾淨(因此更容易建立和解析)。 完整的 initramfs 存檔格式在 buffer-format.rst 中進行了解釋,該檔案在 usr/gen_init_cpio.c 中建立,並在 init/initramfs.c 中提取。 三者加起來總共不到 26k 的人類可讀文字。

  3. GNU 專案標準化 tar 大致相當於 Windows 標準化 zip。 Linux 不是其中任何一個的一部分,並且可以自由做出自己的技術決策。

  4. 由於這是一個核心內部格式,因此很容易成為全新的東西。 核心提供了自己的工具來建立和提取此格式。 使用現有標準是首選,但不是必需的。

  5. Al Viro 做出決定(引用:“tar 醜陋至極,並且不會在核心端支援”)

    解釋了他的理由

    並且,最重要的是,設計並實現了 initramfs 程式碼。

未來方向:

今天 (2.6.16),initramfs 始終被編譯,但並非總是使用。 核心會回退到傳統啟動程式碼,只有在 initramfs 不包含 /init 程式時才會訪問該程式碼。 回退是舊程式碼,旨在確保平穩過渡,並允許早期啟動功能逐漸遷移到“早期使用者空間”(即 initramfs)。

遷移到早期使用者空間是必要的,因為查詢和掛載實際的根裝置很複雜。 根分割槽可以跨越多個裝置(raid 或單獨的日誌)。 它們可以在網路上(需要 dhcp、設定特定的 MAC 地址、登入到伺服器等)。 它們可以位於可移動媒體上,具有動態分配的主/次裝置號和持久命名問題,需要完整的 udev 實現才能解決。 它們可以被壓縮、加密、寫時複製、loopback 掛載、奇怪地分割槽等等。

這種複雜性(不可避免地包括策略)應該在使用者空間中處理。 klibc 和 busybox/uClibc 都在開發簡單的 initramfs 軟體包,以放入核心構建中。

klibc 軟體包現在已被 Andrew Morton 的 2.6.17-mm 樹接受。 核心當前早期啟動程式碼(分割槽檢測等)可能會遷移到預設的 initramfs 中,由核心構建自動建立和使用。