为什么需要initramfs
在Linux内核被加载到内存并运行后,内核进程最终需要切换到用户的进程来使用计算机,而用户进程又存在于外存储设备上,比如systemd进程,通常systemd进程所在的存储设备也是Linux真正的根文件系统所在的位置,我们知道内核源码是没有包含驱动程序的,驱动程序在外存储设备上,那么问题来了,要切换到systemd进程(system deamon),就需要外存储的驱动,但是没有驱动又没办法访问外存储。
为了解决上述问题,内核开发者们设计了initramfs机制。initramfs是一个临时的文件系统,其中包含了必要的设备如硬盘、网卡、文件系统等的驱动以及加载驱动的工具及其运行环境,比如基本的C库,动态库的链接加载器等等。同时,那些处理根文件系统在RAID、网络设备上的程序也存放在initramfs中。由第三方程序(如Bootloader)负责将initramfs从硬盘装载进内存。
以驱动硬盘为例,内核就不必再从硬盘,而是从已经加载到内存的initramfs中获取硬盘控制器等相关驱动了,继而可以驱动硬盘,访问硬盘上的根文件系统,从而解决了前面提到的鸡和蛋的矛盾。在初始化的最后,内核运行initramfs中的init程序,该程序将探测硬件设备、加载驱动,挂载真正的文件系统,执行文件系统上的/sbin/init,进而切换到真正的用户空间。真正的文件系统挂载后,initramfs即完成了使命,其占用的内存也会被释放。
在initramfs之前,也就是在2.4以及更早版本的内核中,内核使用的是initrd。initrd是基于ramdisk技术的,而ramdisk就是一个基于内存的块设备,因此initrd也具有块设备的一切属性。比如initrd容量是固定的,一旦创建initrd时设定了一个大小,就不能再进行动态调整。而且,如同块设备一样,initrd 需要按照一定的文件系统格式进行组织,因此制作initrd时需要使用如mke2fs这样的工具“格式化”initrd,访问initrd时需要通过文件系统驱动。更重要的是,虽然initrd是一个伪块设备,但是从内核的角度看,其与真实的块设备并无区别,因此,内核访问initrd也需使用缓存机制,显然这是多此一举的,因为本身initrd就在内存中。
鉴于ramdisk机制的种种限制,Linus Torvalds提出了一个想法:能否将cache当作一个文件系统直接挂载使用?基于这个想法,Linus Torvalds基于已有的缓存机制实现了ramfs。ramfs与ramdisk有着本质的区别,ramdisk本质上是基于内存的一个块设备,而ramfs是基于缓存的一个文件系统。因此,ramfs去除了前述块设备的一些限制。比如,ramfs根据其中包含的文件大小可自由伸缩;增加文件时,自动分配内存;删除文件时,自动释放内存。更重要的是,ramfs是基于已有的缓存机制,因此不必再像ramdisk那样需要和缓存之间进行多余的复制一环。
伴随着ramfs的出现,从2.6开始,内核开发人员基于ramfs开发了initramfs替代initrd。当2.6版本的内核引导时,在挂载真正的根文件系统之前,首先将挂载一个名为rootfs的文件系统,并将rootfs的根作为虚拟文件系统目录树的总根。那么为什么要使用rootfs这么一个中间过程呢?原因之一还是为了解决鸡和蛋的问题。内核需要根文件系统上的驱动以及程序来驱动和挂载根文件系统,但是这些驱动和程序有可能没有编译进内核,而在根文件系统上。如果不借助第三方,内核是没有办法挂载真正的根文件系统的。而rootfs虽然名称为rootfs,但是并不是什么新的文件系统,事实上,rootfs就是一个ramfs,只不过换了一个名称。换句话说,rootfs是在内存中的,内核不需要特殊的驱动就可以挂载rootfs,所以内核使用rootfs作为一个过渡的桥梁。在挂载了rootfs后,内核将Bootloader加载到内存中的initramfs中打包的文件解压到rootfs中,而这些文件中包含了驱动以及挂载真正的根文件系统的工具,内核通过加载这些驱动、使用这些工具,实现了挂载真正的根文件系统。
Ramfs 是什么
Ramfs 是一个非常简单的文件系统,它将 Linux 的磁盘缓存机制(页缓存和目录项缓存)作为动态可调整大小的基于 RAM 的文件系统来导出。通常,所有文件都由 Linux 缓存在内存中。从后备存储(通常是文件系统挂载的块设备)读取的数据页面会保留在内存中,以防再次需要,但被标记为干净(可释放)以防虚拟内存系统需要内存用于其他目的。类似地,写入文件的数据在写入后备存储后被标记为干净,但出于缓存目的而保留,直到虚拟内存重新分配内存。一个类似的机制(目录项缓存)极大地加速了对目录的访问。
使用 ramfs 时,没有后备存储。写入 ramfs 的文件通常会分配目录项和页缓存,但没有地方可以写入。这意味着页面永远不会被标记为干净,因此当虚拟内存在寻找回收内存时,它们不能被释放。
实现 ramfs 所需的代码量非常小, ramfs的代码位于/fs/ramfs/目录下,因为所有工作都由现有的 Linux 缓存基础设施完成。基本上,你是将磁盘缓存作为文件系统挂载。因此,ramfs 不是一个可选组件,不能通过 menuconfig 移除,因为空间节省可以忽略不计。
Ramfs 和 ramdisk:
谈到ramfs就不得不提一下ramdisk,ramdisk作为旧的 "ram 盘" 机制通过将 RAM 区域创建为合成块设备,并将其作为文件系统的后备存储。这个块设备的大小是固定的,因此挂载在其上的文件系统的大小也是固定的。使用 ram 盘还需要不必要地将内存从假块设备复制到页缓存(并将更改复制回),以及创建和销毁目录项。它还需要一个文件系统驱动程序(如 ext2)来格式化和解释这些数据。
在使用 ramdisk 时,数据会被复制到一个模拟的块设备中,然后再从这个块设备复制到操作系统的页缓存中。这个过程不仅增加了内存的使用量,还增加了 CPU 的工作量,因为需要执行额外的数据复制操作。此外,频繁的数据复制操作还可能干扰 CPU 缓存的正常工作,导致缓存效率降低。
具体来说,当操作系统访问文件系统中的数据时,数据通常会被加载到内存中的页缓存里,以便于快速访问。如果使用 ramdisk,数据首先被存储在一个模拟的块设备上,这个块设备实际上是内存中的一块区域。当需要访问这些数据时,操作系统会先将数据从 ramdisk 的块设备复制到页缓存,然后再从页缓存提供给应用程序或其他系统组件。这个过程涉及到两次数据复制:一次是从 ramdisk 到页缓存,另一次可能是从页缓存回写到 ramdisk,尤其是在数据被修改后需要持久化时。
这种数据复制不仅消耗了额外的内存带宽,还可能因为频繁的复制操作导致 CPU 缓存(CPU Cache)中的其他数据被替换,这些被替换的数据可能更频繁地被访问,因此 ramdisk 机制会降低缓存的效率,这就是所谓的“污染缓存”。相比之下,ramfs 和 tmpfs 直接使用现有的页缓存和目录项缓存,避免了这种额外的数据复制和缓存干扰。
与 ramfs 相比,上述的这些不必要的数据拷贝会浪费内存(和内存总线带宽),浪费CPU资源去做不必要的工作,并且会污染 CPU 缓存。更重要的是,ramfs 所做的所有工作都必须发生,因为所有文件访问都通过页和目录项缓存。但是ramdisk的很多工作是不必要的;另外ramfs 在内部实现也要简单得多。
什么是 initramfs?
所有 2.6 Linux 内核都包含一个 gzip 压缩的 "cpio" 格式归档文件,当内核启动时,将其解压到 rootfs 中。解压后,内核会检查 rootfs 是否包含 "init" 文件,如果是,它将作为 PID 1 执行它。如果找到了,这个 init 进程负责将系统启动到其余部分,包括定位和挂载真实的根设备(如果有)。如果解压后的内置 cpio 归档文件后 rootfs 不包含 init 程序,内核将回退到旧代码来定位和挂载根分区,然后在其中执行某种变体的 /sbin/init。initramfs与 initrd 在几个方面有所不同:
1)initrd 总是一个单独的文件,而 initramfs 归档文件链接到 Linux 内核映像中。
- initrd 文件是一个 gzip 压缩的文件系统映像(在某些文件格式中,例如 ext2,需要内核内置的驱动程序),而新的 initramfs 归档是一个 gzip 压缩的 cpio 归档(像 tar 但更简单)。内核的 cpio 提取代码不仅非常小,而且还是 __init 文本和数据,在启动过程中可以丢弃。
- initrd 运行的程序(称为 /initrd,而不是 /init)会做一些设置,然后返回到内核,而 initramfs 中的 init 程序不期望返回到内核。(如果 /init 需要移交控制权,它可以覆盖 / 与一个新的根设备,并执行另一个 init 程序)。
3)当切换另一个根设备时,initrd 会 pivot_root 然后卸载 ramdisk。但是 initramfs 是 rootfs:你既不能 pivot_root rootfs,也不能卸载它。相反,删除 rootfs 中的所有内容以释放空间,用新根覆盖 rootfs,将标准输入/输出/错误附加到新的 /dev/console,并执行新的 init。
制作 initramfs:
2.6 内核构建过程总是创建一个 gzip 压缩的 cpio 格式 initramfs 归档,并将其链接到生成的内核二进制文件中。默认情况下,这个归档是空的(在 x86 上占用 134 字节)。
配置选项 CONFIG_INITRAMFS_SOURCE(在 menuconfig 的常规设置中,位于 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 访问权限就可以在新归档中设置权限或创建设备节点。
内核不依赖外部 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
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