Ramfs、rootfs和initramfs
作者 Rob Landley rob@landley.net
什么是ramfs?
Ramfs是一个非常简单的文件系统,它将Linux的磁盘缓存机制(页面缓存和目录项缓存)作为一个动态可调整大小的基于RAM的文件系统进行导出。
通常情况下,Linux会将所有文件缓存在内存中。从后备存储(通常是文件系统所挂载的块设备)读取的数据页会被保留,以防再次需要,但标记为干净(可释放),以便虚拟内存系统在需要内存进行其他用途时可以释放。类似地,写入文件的数据一旦被写入后备存储,就会被标记为干净,但会被保留以进行缓存,直到虚拟内存重新分配内存。类似的机制(目录项缓存)极大地加快了对目录的访问速度。
使用ramfs时,没有后备存储。写入ramfs的文件会像通常一样分配目录项和页面缓存,但没有地方可以写入。这意味着这些页面永远不会被标记为干净,因此在虚拟内存寻求回收内存时无法释放。
实现ramfs所需的代码量非常小,因为所有工作都是由现有的Linux缓存基础设施完成的。基本上,你是将磁盘缓存挂载为文件系统。因此,ramfs不是通过menuconfig可选移除的组件,因为这样做几乎不会节省空间。
ramfs和ramdisk:
旧的“ram磁盘”机制创建了一个合成的块设备,将一块RAM区域用作文件系统的后备存储。这个块设备的大小是固定的,因此挂载在其上的文件系统也是固定大小的。使用ram磁盘还需要不必要地将内存从虚假的块设备复制到页面缓存(并将更改再次复制出来),以及创建和销毁目录项。此外,它需要一个文件系统驱动程序(如ext2)来格式化和解释这些数据。
与ramfs相比,这浪费了内存(和内存总线带宽),为CPU创建了不必要的工作,并污染了CPU缓存。 (虽然有一些技巧可以通过操作页表来避免这种复制,但它们非常复杂,并且结果与复制一样昂贵。)更重要的是,ramfs正在进行的所有工作无论如何都必须发生,因为所有文件访问都通过页面和目录项缓存进行。RAM磁盘是多余的;ramfs在内部要简单得多。
另一个ram磁盘半过时的原因是,回环设备的引入提供了一种更灵活和方便的方法来创建合成的块设备,现在是从文件而不是从内存块中创建。有关详细信息,请参阅losetup(8)。
ramfs和tmpfs:
ramfs的一个缺点是你可以不断地向其中写入数据,直到填满所有内存,而虚拟内存无法释放它,因为虚拟内存认为文件应该被写入后备存储(而不是交换空间),但ramfs没有后备存储。因此,只有root(或受信任的用户)应该被允许对ramfs挂载进行写访问。
为了解决这个问题,创建了一个ramfs的衍生版本叫做tmpfs,它增加了大小限制,并且可以将数据写入交换空间。普通用户可以被允许对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进程负责将系统的其余部分带起来,包括定位和挂载真正的根设备(如果有的话)。如果rootfs在嵌入的cpio存档被解压到其中后不包含init程序,内核将继续执行旧代码来定位和挂载根分区,然后在其中执行/sbin/init的某个变体。
所有这些与旧的initrd有几点不同:
- 旧的initrd总是一个单独的文件,而initramfs存档被链接到Linux内核映像中(在构建过程中,linux-*/usr目录用于生成此存档)。
- 旧的initrd文件是一个经过gzip压缩的文件系统映像(以某种文件格式,如ext2,需要内核内置的驱动程序),而新的initramfs存档是一个经过gzip压缩的cpio存档(类似于tar,只是更简单,请参阅cpio(1)和initramfs缓冲区格式)。内核的cpio解压缩代码不仅非常小,而且也是__init文本和数据,在引导过程中可以丢弃。
- 旧的initrd运行的程序(称为/init,而不是/init)进行了一些设置,然后返回到内核,而initramfs中的init程序不应该返回到内核。(如果/init需要交出控制权,它可以用新的根设备覆盖/mount,并在其中执行另一个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"条目期望在名为"initramfs"的目录下找到名为"init.sh"和"busybox"的文件。有关更多详细信息,请参阅Early userspace support。)
内核不依赖外部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 <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的man页面包含了一些错误的建议,如果你遵循它,将会破坏你的initramfs存档。它说:“生成文件名列表的典型方法是使用find命令;你应该给find加上-depth选项,以最小化对不可写或不可搜索的目录权限的问题。”在创建initramfs.cpio.gz映像时不要这样做,它不会起作用。Linux内核的cpio提取器不会在不存在的目录中创建文件,因此目录条目必须在其中的文件之前。上面的脚本按正确的顺序获取它们。
外部initramfs映像:
如果内核启用了initrd支持,也可以将一个外部cpio.gz存档传递到2.6内核中,以取代initrd。在这种情况下,内核将自动检测类型(initramfs,而不是initrd),并在尝试运行/init之前将外部cpio存档解压到rootfs中。
这具有initramfs的内存效率优势(没有ramdisk块设备),但initrd的单独打包(如果您有非GPL代码希望从initramfs中运行,而不与GPL许可的Linux内核二进制混合在一起,这很好)。
它还可以用于补充内核内置的initramfs映像。外部存档中的文件将覆盖内置initramfs存档中的任何冲突文件。一些发行版还喜欢使用一个特定于任务的initramfs映像自定义单个内核映像,而不重新编译。
initramfs的内容:
initramfs档案是Linux的完整自包含根文件系统。如果你还不了解需要哪些共享库、设备和路径来启动一个最小的根文件系统,以下是一些参考资料:
"klibc"软件包(https://www.kernel.org/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也会使用dlopen加载libnss进行名称查找。)
一个很好的第一步是让initramfs运行一个静态链接的“hello world”程序作为init,并在像qemu(www.qemu.org)或User Mode 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 # 使用initrd加载机制测试外部initramfs。 qemu -kernel /boot/vmlinuz -initrd test.cpio.gz /dev/zero
在调试普通根文件系统时,可以通过“init=/bin/sh”选项引导。对应的initramfs选项是“rdinit=/bin/sh”,同样有用。
为什么选择cpio而不是tar?
这个决定是在2001年12月做出的。讨论始于这里:
并产生了第二个讨论线程(具体涉及tar和cpio的比较),始于这里:
简单粗暴的摘要版本(这并不能替代阅读上述讨论)是:
- cpio是一个标准。它有几十年的历史(来自AT&T时代),在Linux上已经被广泛使用(在RPM、Red Hat的设备驱动盘中)。1996年的Linux Journal有一篇关于它的文章:
- 它不像tar那样流行,因为传统的cpio命令行工具需要非常糟糕的命令行参数。但这并不能说明归档格式的好坏,而且还有一些替代工具,比如:
- 内核选择的cpio归档格式更简单、更清晰(因此更容易创建和解析),比起(成千上万种)各种tar归档格式。完整的initramfs归档格式在buffer-format.txt中有解释,在usr/gen_init_cpio.c中创建,在init/initramfs.c中提取。这三者加起来不到26k的人类可读文本。
- GNU项目选择tar作为标准,大致相当于Windows选择zip。Linux不属于其中任何一个,可以自行做出技术决定。
- 由于这是一个内核内部格式,它本来可以是全新的。内核提供了自己的工具来创建和提取这种格式。使用现有的标准是可取的,但不是必需的。
- 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中,由内核构建自动创建和使用。