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://linuxkernel.org.cn/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 中,由内核构建自动创建和使用。