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),用类似硬盘的方式读写。
具体流程是这样的:
- bootloader 把 initrd 镜像加载到内存,告诉内核它的起始地址和大小
- 内核解压 initrd(通常是 gzip 压缩的 ext2 文件系统镜像)到 RAM disk 设备
- 内核挂载这个 RAM disk 为根文件系统
- 执行 /linuxrc 或 /init,加载必要的内核模块
- 加载完成后,执行 pivot_root,把根切换到真正的文件系统
- 卸载 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 中。流程大致如下:
- populate_rootfs() 被调用,这是入口函数
- 检查是否有内嵌的 cpio 归档(通过
__initramfs_start和__initramfs_size符号) - 如果有,调用
unpack_to_rootfs()解压到根目录 - 如果 bootloader 传入了外部 initramfs,也一并解压
- 解压完成后,根目录下就有了完整的文件系统
- 内核尝试执行
/init,如果不存在则尝试/sbin/init等
这里有个细节:内嵌 initramfs 和外部 initramfs 可以共存。内核会先解压内嵌的,再解压外部的,后者会覆盖前者。我在项目中就用这个特性——把基础工具链编进内核,调试时通过 bootloader 传入额外的调试工具,非常灵活。
3.6 实际项目中的选择建议
说了这么多,到底该用 initrd 还是 initramfs?我的建议很简单:
- 新项目一律用 initramfs,除非你要兼容 2.4 内核
- 如果 bootloader 不支持加载 initramfs,可以把 cpio 归档链接进内核镜像(CONFIG_INITRAMFS_SOURCE)
- 调试阶段用外部 initramfs,方便修改;发布阶段内嵌进内核,减少启动环节
注意: 有些老旧的 bootloader 只支持 initrd 格式。如果你遇到这种情况,可以在内核配置中启用 CONFIG_BLK_DEV_INITRD,内核会兼容处理。但说实话,这种场景越来越少了,我上次遇到还是五年前在一个古董级的 PowerPC 平台上。