3. initramfs 原理:cpio 归档格式、initramfs 与 initrd 的区别、内核如何解压并挂载 initramfs
initramfs(initial RAM filesystem)是现代 Linux 内核在挂载真正根文件系统之前,加载到内存中的一个微型根文件系统。它承担着硬件初始化、驱动加载、块设备发现以及最终切换到真实根文件系统的关键任务。理解 initramfs 的工作原理,是掌握内核参数传递与启动调试的基础。
3.1 cpio 归档格式:initramfs 的载体
initramfs 本质上是一个使用 cpio(Copy In/Out)格式打包的内存盘镜像。cpio 是一种古老的 Unix 归档格式,其设计简单、紧凑,非常适合内核在早期启动阶段快速解压并加载到 tmpfs 中。
cpio 格式结构
一个标准的 cpio 归档由一系列文件的元数据头和数据块组成,最后以一个特殊的 TRAILER!!! 记录结束。内核只支持 newc(SVR4 portable format)格式,其头部结构如下:
| 字段 | 偏移量(字节) | 长度(字节) | 说明 |
|---|---|---|---|
| c_magic | 0 | 6 | 固定为 "070701"(newc 格式魔数) |
| c_ino | 6 | 8 | inode 号(十六进制 ASCII) |
| c_mode | 14 | 8 | 文件类型与权限(十六进制 ASCII) |
| c_uid | 22 | 8 | 用户 ID(十六进制 ASCII) |
| c_gid | 30 | 8 | 组 ID(十六进制 ASCII) |
| c_nlink | 38 | 8 | 硬链接数(十六进制 ASCII) |
| c_mtime | 46 | 8 | 修改时间戳(十六进制 ASCII) |
| c_filesize | 54 | 8 | 文件数据大小(十六进制 ASCII) |
| c_maj | 62 | 8 | 设备主号(十六进制 ASCII) |
| c_min | 70 | 8 | 设备次号(十六进制 ASCII) |
| c_rmaj | 78 | 8 | 特殊设备主号(十六进制 ASCII) |
| c_rmin | 86 | 8 | 特殊设备次号(十六进制 ASCII) |
| c_namesize | 94 | 8 | 文件名长度(包含结尾 null,十六进制 ASCII) |
| c_chksum | 102 | 8 | 校验和(十六进制 ASCII,未使用时为 0) |
| c_name | 110 | c_namesize | 文件名(以 null 结尾) |
| c_data | 对齐到 4 字节边界 | c_filesize | 文件数据(填充到 4 字节对齐) |
cpio -t < initramfs.cpio 可以列出归档内容;使用 lsinitramfs /boot/initrd.img-$(uname -r) 可直接查看当前系统的 initramfs 文件列表。
构建与解包示例
# 创建 initramfs 目录结构
mkdir -p my_initramfs/{bin,dev,etc,lib,lib64,mnt,proc,root,sbin,sys,usr}
# 使用 cpio 打包
cd my_initramfs
find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz
# 解包现有的 initramfs
zcat /boot/initrd.img-$(uname -r) | cpio -idmv
3.2 initramfs 与 initrd 的区别
initrd(initial RAM disk)是早期内核使用的机制,而 initramfs 是 2.6 内核引入的替代方案。两者虽然功能相似,但底层实现有本质区别。
| 对比维度 | initrd(传统 RAM disk) | initramfs(现代 RAM fs) |
|---|---|---|
| 底层实现 | 基于块设备模拟(ramdisk 驱动),需要格式化文件系统(如 ext2) | 基于 tmpfs(内存文件系统),直接使用页缓存 |
| 镜像格式 | 文件系统镜像(如 ext2 镜像),通常需要 gzip 压缩 | cpio 归档(newc 格式),可 gzip 或 xz 压缩 |
| 内存占用 | 固定大小,即使未使用也会占用全部预分配内存 | 动态增长,按需分配,更节省内存 |
| 加载方式 | 内核将镜像解压到预分配的 RAM 块设备(/dev/ram0) | 内核将 cpio 归档解压到 tmpfs 实例中 |
| 启动速度 | 较慢,需要创建块设备、格式化文件系统、挂载 | 较快,直接解压到内存页缓存,无需块设备层 |
| 内核版本 | Linux 2.0 ~ 2.4(现已废弃) | Linux 2.6 至今(默认方案) |
| 内核配置选项 | CONFIG_BLK_DEV_INITRD |
CONFIG_BLK_DEV_INITRD + CONFIG_RD_CPIO |
initrd.img-xxx,但这只是历史遗留命名习惯,其内容格式已经是 cpio 归档,而非传统的块设备镜像。不要被文件名误导。
3.3 内核解压并挂载 initramfs 的完整流程
从 bootloader 加载内核到 initramfs 中的 /init 进程执行,内核内部经历了一系列精心设计的步骤。以下是完整的流程分解:
步骤 1:bootloader 加载内核与 initramfs 到内存
GRUB 等 bootloader 根据配置文件,将内核镜像(vmlinuz)和 initramfs 镜像加载到物理内存的特定位置。对于 x86 架构,initramfs 通常被加载到 0x1000000(16MB)附近的内存区域。bootloader 通过内核的 setup header 中的 ramdisk_image 和 ramdisk_size 字段告知内核 initramfs 的位置和大小。
步骤 2:内核早期解压缩
内核入口点(startup_32 或 startup_64)首先进行自解压,将压缩的内核代码解压到高端内存。此时 initramfs 镜像仍以压缩形式保留在 bootloader 指定的内存位置。
步骤 3:populate_rootfs 函数调用
在内核初始化早期,init/main.c 中的 populate_rootfs() 函数被调用。该函数负责定位、解压并挂载 initramfs。其核心逻辑如下:
// 内核源码 init/initramfs.c(简化逻辑)
static int __init populate_rootfs(void)
{
char *err;
// 1. 从 bootloader 指定的内存位置获取压缩的 initramfs 数据
char *data = (char *)initrd_start;
unsigned long size = initrd_end - initrd_start;
// 2. 尝试解压(支持 gzip、xz、lzma、bzip2、lzo、lz4)
err = unpack_to_rootfs(data, size);
if (err) {
// 解压失败,尝试作为未压缩的 cpio 处理
printk(KERN_WARNING "initramfs unpacking failed: %s\n", err);
return 0;
}
// 3. 解压成功,cpio 归档内容已被展开到根目录的 tmpfs 中
return 0;
}
步骤 4:cpio 归档解包到 tmpfs
unpack_to_rootfs() 函数遍历 cpio 归档中的每个文件记录,在内存中的 tmpfs 根目录(/)下创建对应的目录、设备节点和普通文件。关键处理逻辑:
- 识别
TRAILER!!!标记结束解包 - 处理硬链接(通过
c_nlink和c_ino匹配) - 创建设备节点(通过
c_mode中的 S_IFBLK/S_IFCHR 标志) - 设置文件权限、所有者和时间戳
步骤 5:切换到根文件系统并执行 /init
解包完成后,内核将当前进程的根目录切换到 tmpfs 实例,并尝试执行 /init 程序。如果 /init 不存在,内核会依次尝试 /sbin/init、/etc/init、/bin/init 和 /bin/sh。
3.4 内核参数对 initramfs 行为的影响
内核启动参数可以显著改变 initramfs 的加载行为,以下是几个关键参数:
| 内核参数 | 作用 | 调试场景 |
|---|---|---|
initrd= |
手动指定 initramfs 镜像路径(通常由 bootloader 自动设置) | 测试自定义 initramfs 镜像 |
noinitrd |
跳过 initramfs 加载,直接尝试挂载真实根文件系统 | 排查 initramfs 导致的启动失败 |
rdinit= |
指定 initramfs 中执行的第一个用户空间程序(默认 /init) |
使用自定义 shell 脚本替代默认 init |
init= |
指定真实根文件系统的 init 程序(initramfs 完成切换后执行) | 覆盖默认的 /sbin/init |
root= |
指定真实根文件系统的设备(如 /dev/sda1 或 UUID=xxx) |
调试根设备识别问题 |
rd.debug |
启用 initramfs 阶段的详细调试输出(dracut 系统) | 跟踪 initramfs 内部执行流程 |
/init,所有硬件探测、驱动加载、文件系统挂载等复杂逻辑都交给用户空间的 /init 脚本处理。这种解耦使得 Linux 启动流程高度可定制。
3.5 调试 initramfs 加载问题的方法
当 initramfs 加载失败时,可以通过以下方法进行诊断:
- 内核日志分析: 使用
dmesg或journalctl -k查看populate_rootfs和unpack_to_rootfs的输出信息。 - earlyprintk: 添加内核参数
earlyprintk=serial,ttyS0,115200可以在非常早期的阶段输出调试信息。 - initramfs 内容检查: 使用
lsinitramfs或cpio -t验证镜像内容是否完整。 - 手动触发解压: 在 GRUB 命令行中,使用
initrd命令手动加载不同的 initramfs 镜像进行测试。
# 在 GRUB 命令行中手动加载 initramfs 并启动
grub> linux /vmlinuz-5.15.0-generic root=/dev/sda1 ro
grub> initrd /initrd.img-5.15.0-generic
grub> boot
# 使用 QEMU 模拟启动并调试 initramfs
qemu-system-x86_64 -kernel /boot/vmlinuz-5.15.0-generic \
-initrd /boot/initrd.img-5.15.0-generic \
-append "root=/dev/sda1 console=ttyS0" \
-nographic