Ramfs、rootfs 和 initramfs

2005 年 10 月 17 日

作者:

Rob Landley <rob@landley.net>

什么是 ramfs?

Ramfs 是一个非常简单的文件系统,它将 Linux 的磁盘缓存机制(页面缓存和目录项缓存)作为基于 RAM 的动态可调整大小的文件系统导出。

通常,所有文件都由 Linux 缓存在内存中。从后备存储(通常是文件系统挂载的块设备)读取的数据页面会被保留,以防再次需要,但如果虚拟内存系统需要内存用于其他用途,则标记为干净(可释放)。类似地,写入文件的数据在写入后备存储后会立即标记为干净,但为了缓存目的而保留,直到 VM 重新分配内存。类似的机制(目录项缓存)大大加快了对目录的访问。

对于 ramfs,没有后备存储。写入 ramfs 的文件会像往常一样分配目录项和页面缓存,但没有地方可以写入它们。这意味着页面永远不会被标记为干净,因此当 VM 寻找回收内存时,它们无法被 VM 释放。

实现 ramfs 所需的代码量很小,因为所有工作都由现有的 Linux 缓存基础设施完成。基本上,您是将磁盘缓存作为文件系统挂载。因此,ramfs 不是可以通过 menuconfig 移除的可选组件,因为空间节省微不足道。

ramfs 和 ramdisk:

较旧的“ram 磁盘”机制从 RAM 区域创建了一个合成块设备,并将其用作文件系统的后备存储。此块设备的大小是固定的,因此挂载在其上的文件系统的大小也是固定的。使用 ram 磁盘还需要不必要地将内存从伪块设备复制到页面缓存(并将更改复制回去),以及创建和销毁目录项。此外,它还需要一个文件系统驱动程序(例如 ext2)来格式化和解释此数据。

与 ramfs 相比,这会浪费内存(和内存总线带宽),为 CPU 创建不必要的工作,并污染 CPU 缓存。(有一些技巧可以通过操作页表来避免这种复制,但它们非常复杂,而且事实证明它们与复制的开销差不多。)更重要的是,ramfs 所做的所有工作都必须 _无论如何_ 发生,因为所有文件访问都通过页面和目录项缓存进行。RAM 磁盘是完全不必要的;ramfs 在内部要简单得多。

ramdisk 半过时的另一个原因是,回环设备的引入提供了一种更灵活和方便的方式来创建合成块设备,现在可以从文件而不是从内存块创建。有关详细信息,请参阅 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 内核都包含一个 gzip 压缩的“cpio”格式的存档,该存档在内核启动时解压缩到 rootfs 中。解压缩后,内核会检查 rootfs 是否包含文件“init”,如果包含,则将其作为 PID 1 执行。如果找到,则此 init 进程负责将系统引导到剩余部分,包括定位和挂载实际的根设备(如果有)。如果在将嵌入式 cpio 存档解压缩到 rootfs 后,rootfs 不包含 init 程序,则内核将退回到旧代码以定位和挂载根分区,然后从该分区执行 /sbin/init 的某些变体。

所有这些与旧的 initrd 有几个不同之处

  • 旧的 initrd 始终是一个单独的文件,而 initramfs 存档链接到 linux 内核映像中。(目录 linux-*/usr 专门用于在构建过程中生成此存档。)

  • 旧的 initrd 文件是一个 gzip 压缩的文件系统映像(采用某种文件格式,例如 ext2,需要在内核中构建一个驱动程序),而新的 initramfs 存档是一个 gzip 压缩的 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 内核构建过程始终创建一个 gzip 压缩的 cpio 格式的 initramfs 存档,并将其链接到生成的内核二进制文件中。默认情况下,此存档为空(在 x86 上占用 134 字节)。

配置选项 CONFIG_INITRAMFS_SOURCE(在 menuconfig 的 General Setup 中,位于 usr/Kconfig 中)可用于指定 initramfs 存档的源,该源将自动合并到生成的二进制文件中。此选项可以指向现有的 gzip 压缩的 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”的文件。有关更多详细信息,请参阅 早期用户空间支持。)

内核不依赖于外部 cpio 工具。如果您指定一个目录而不是配置文件,则内核的构建基础结构会从该目录创建一个配置文件(usr/Makefile 调用 usr/gen_initramfs.sh),并使用该配置文件打包该目录(通过将其馈送到 usr/gen_init_cpio,该文件由 usr/gen_init_cpio.c 创建)。内核的构建时 cpio 创建代码是完全自包含的,内核的启动时提取器也是(显然)自包含的。

您可能需要安装外部 cpio 实用程序的唯一原因是创建或提取您自己的预先准备好的 cpio 文件以馈送给内核构建(而不是配置文件或目录)。

以下命令行可以将 cpio 映像(由上述脚本或内核构建)提取回其组件文件

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

以下 shell 脚本可以创建一个预构建的 cpio 存档,您可以使用该存档代替上述配置文件

#!/bin/sh

# Copyright 2006 Rob Landley <[email protected]> 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许可。(一个独立的 initramfs 软件包计划在 busybox 1.3 版本中发布。)

理论上你可以使用 glibc,但它不太适合像这样的小型嵌入式用途。(一个静态链接 glibc 的“hello world”程序超过 400k。使用 uClibc 则为 7k。另请注意,glibc 会 dlopen 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 的设备驱动程序磁盘中)。这是 1996 年 Linux Journal 上关于它的一篇文章:

    它不像 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 实现才能解决。它们可以被压缩、加密、写时复制、环回挂载、奇怪地分区等等。

这种复杂性(不可避免地包括策略)应该在用户空间中正确处理。klibc 和 busybox/uClibc 都在开发简单的 initramfs 软件包,以便放入内核构建中。

klibc 软件包现在已被 Andrew Morton 的 2.6.17-mm 树接受。内核当前的早期引导代码(分区检测等)可能会迁移到默认的 initramfs 中,由内核构建自动创建和使用。