3、initrd与initramfs机制:从软盘到cpio的进化之路

说到根文件系统的早期加载,就绕不开 initrd 和 initramfs 这两个老伙计。我刚开始接触嵌入式 Linux 时,用的还是 2.4 内核,那时候 initrd 是标配。后来 2.6 内核引入了 initramfs,很多人觉得只是换了个打包格式,其实背后的设计思想完全不同。

这一节,咱们就把这两兄弟的来龙去脉、工作原理、以及那个看似简单实则暗藏玄机的 cpio 格式,一次讲透。

3.1 initrd 的起源:软盘时代的无奈之举

initrd,全称是 Initial RAM Disk。它的诞生,说白了就是为了解决一个鸡生蛋蛋生鸡的问题:内核启动时需要加载驱动,但驱动文件又放在根文件系统里,而根文件系统所在的设备(比如 SCSI 硬盘)又需要驱动才能访问。

你想想看,这怎么破?

早期解决方案很粗暴——把所有可能用到的驱动都编译进内核。但内核体积越来越大,软盘都装不下了。于是就有了 initrd:

  • 内核自身只包含最基本的驱动,能识别 RAM 和简单的块设备就行
  • initrd 是一个小型的根文件系统镜像,里面放着各种驱动模块和加载脚本
  • 内核先挂载 initrd,运行里面的脚本,加载真正的根设备驱动
  • 最后 pivot_root 切换到真正的根文件系统

核心流程: bootloader → 内核 → initrd(加载驱动)→ pivot_root → 真正的根文件系统

我记得早期做嵌入式开发时,为了把 initrd 塞进 1.44MB 的软盘,得反复裁剪驱动,那叫一个痛苦。后来有了 Flash 和网络启动,情况才好转。

3.2 initrd 的工作原理:块设备模拟

initrd 本质上是一个块设备镜像。内核把它当作一个 RAM disk(/dev/ram0),用类似硬盘的方式读写。

具体流程是这样的:

  1. bootloader 把 initrd 镜像加载到内存,告诉内核它的起始地址和大小
  2. 内核解压 initrd(通常是 gzip 压缩的 ext2 文件系统镜像)到 RAM disk 设备
  3. 内核挂载这个 RAM disk 为根文件系统
  4. 执行 /linuxrc 或 /init,加载必要的内核模块
  5. 加载完成后,执行 pivot_root,把根切换到真正的文件系统
  6. 卸载 initrd,释放内存

注意: pivot_root 和 chroot 不同。pivot_root 会把当前根挂载点移到另一个目录,然后挂载新根。chroot 只是改变进程的根目录视角,不会动内核的挂载点。我见过有人用 chroot 代替 pivot_root,结果卸载 initrd 时出问题,因为内核还占着那个挂载点。

这里有个关键点——initrd 是块设备,需要文件系统驱动(比如 ext2)来解析。这就意味着内核必须把 ext2 驱动编译进去,否则连 initrd 都读不了。嗯,这里要注意,这其实是个循环依赖的隐患。

3.3 initramfs 的设计哲学:文件缓存代替块设备

initramfs 的出现,就是为了解决 initrd 的那些痛点。它由内核开发者 Al Viro 在 2001 年提出,从 2.6 内核开始成为主流。

设计哲学很简单:不再用块设备模拟,而是直接利用内核的文件缓存(page cache)机制。

对比项 initrd initramfs
本质 块设备镜像(RAM disk) 内存中的 tmpfs 文件系统
存储格式 ext2 等文件系统镜像 cpio 归档
挂载方式 需要块设备驱动 + 文件系统驱动 内核直接解析 cpio,无需额外驱动
内存占用 固定大小,预分配 按需分配,用多少占多少
切换根 pivot_root 直接删除 initramfs 内容或切换
灵活性 差,修改需要重新制作镜像 好,可以动态增减文件

说白了,initramfs 就是把一堆文件用 cpio 打包,然后直接链接进内核或者由 bootloader 加载。内核启动时,把这些文件解压到内存中的 tmpfs 里,作为初始根文件系统。

我个人习惯: 现在做新项目,除非有特殊兼容性要求,否则一律用 initramfs。它比 initrd 省心太多——不需要关心块设备驱动,不需要预分配内存大小,修改起来也方便。我曾经在一个资源受限的 MIPS 平台上,因为 initrd 预分配了 4MB 内存导致系统启动失败,换成 initramfs 后问题迎刃而解。

3.4 cpio 归档格式解析:简单到极致

initramfs 使用 cpio 格式,而不是 tar 或 ext2。为什么?因为 cpio 足够简单。

cpio 的全称是 "copy in/out",最早出现在 Unix V7 中。它的设计目标就是轻量、顺序读写、易于内核解析。

一个 cpio 归档的结构是这样的:

+------------------+
|  文件条目 1       |
|  - 头部 (110 字节)|
|  - 文件名         |
|  - 文件内容       |
+------------------+
|  文件条目 2       |
|  - 头部           |
|  - 文件名         |
|  - 文件内容       |
+------------------+
|  ...              |
+------------------+
|  尾部 (TRAILER!!!)|
+------------------+

每个文件条目的头部是固定的 110 字节,包含以下关键字段:

字段 偏移 长度 说明
c_magic 0 6 字节 魔数,固定为 "070701"(新格式)
c_ino 6 8 字节 inode 号
c_mode 14 8 字节 文件类型和权限
c_uid 22 8 字节 用户 ID
c_gid 30 8 字节 组 ID
c_nlink 38 8 字节 硬链接数
c_mtime 46 8 字节 修改时间
c_filesize 54 8 字节 文件大小
c_maj 62 8 字节 主设备号
c_min 70 8 字节 次设备号
c_rmaj 78 8 字节 设备文件主设备号
c_rmin 86 8 字节 设备文件次设备号
c_namesize 94 8 字节 文件名长度(含结尾 \0)
c_chksum 102 8 字节 校验和

所有字段都是 ASCII 表示的十六进制数。对,你没看错——纯文本格式的二进制头部。内核解析时直接用 sscanf 就能读出来,不需要复杂的二进制解析逻辑。

关键设计: cpio 头部字段用 ASCII 十六进制表示,虽然浪费了一点空间(每个 8 字节字段实际只用了 4-6 字节),但换来了极简的解析代码。内核中处理 cpio 的代码只有几百行,而处理 ext2 的代码有几万行。这就是设计上的取舍。

文件内容后面会按 4 字节对齐填充。整个归档以名为 "TRAILER!!!" 的尾部条目结束。

来看一个实际的例子。假设我们有一个简单的 initramfs,包含一个 /init 脚本:

# 创建目录结构
mkdir -p rootfs/bin rootfs/dev rootfs/etc rootfs/proc rootfs/sys

# 创建 init 脚本
cat > rootfs/init << 'EOF'
#!/bin/busybox sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mdev -s
exec /bin/sh
EOF

chmod +x rootfs/init

# 用 cpio 打包
cd rootfs
find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz

这条命令生成的 initramfs.cpio.gz 就是内核能直接使用的 initramfs 镜像。内核启动时,如果检测到内核中嵌入了 cpio 归档,或者 bootloader 传入了 initramfs 的地址,就会自动解析并挂载。

避坑指南: 我曾经在调试一个启动失败的问题时,发现 initramfs 里的设备节点权限不对。原因是 cpio 打包时,find 命令默认不包含设备文件。如果你需要创建设备节点(比如 /dev/console),记得用 mknod 创建后再打包,或者让 init 脚本在运行时用 mdev 或 udev 动态创建。

3.5 内核如何解析 initramfs:从入口到根文件系统

内核解析 initramfs 的代码主要在 init/initramfs.c 中。流程大致如下:

  1. populate_rootfs() 被调用,这是入口函数
  2. 检查是否有内嵌的 cpio 归档(通过 __initramfs_start 和 __initramfs_size 符号)
  3. 如果有,调用 unpack_to_rootfs() 解压到根目录
  4. 如果 bootloader 传入了外部 initramfs,也一并解压
  5. 解压完成后,根目录下就有了完整的文件系统
  6. 内核尝试执行 /init,如果不存在则尝试 /sbin/init 等

这里有个细节:内嵌 initramfs 和外部 initramfs 可以共存。内核会先解压内嵌的,再解压外部的,后者会覆盖前者。我在项目中就用这个特性——把基础工具链编进内核,调试时通过 bootloader 传入额外的调试工具,非常灵活。

initramfs 启动流程 initramfs 启动流程 Bootloader 加载内核 + initramfs 内核解压 initramfs.cpio.gz unpack_to_rootfs() 解析 cpio 条目 挂载 tmpfs 作为根文件系统 执行 /init 脚本 switch_root 到真正的根文件系统 关键点: • 内核直接解析 cpio,无需块设备驱动 • 文件缓存按需分配,不浪费内存 • /init 是用户空间的第一个进程 与 initrd 的区别: • initrd: 块设备 → 挂载 → pivot_root • initramfs: cpio → tmpfs → switch_root • initramfs 更简单、更高效

3.6 实际项目中的选择建议

说了这么多,到底该用 initrd 还是 initramfs?我的建议很简单:

  • 新项目一律用 initramfs,除非你要兼容 2.4 内核
  • 如果 bootloader 不支持加载 initramfs,可以把 cpio 归档链接进内核镜像(CONFIG_INITRAMFS_SOURCE)
  • 调试阶段用外部 initramfs,方便修改;发布阶段内嵌进内核,减少启动环节

注意: 有些老旧的 bootloader 只支持 initrd 格式。如果你遇到这种情况,可以在内核配置中启用 CONFIG_BLK_DEV_INITRD,内核会兼容处理。但说实话,这种场景越来越少了,我上次遇到还是五年前在一个古董级的 PowerPC 平台上。