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
cpio 归档格式结构示意 文件1 元数据头 文件1 数据块 文件2 元数据头 文件2 数据块 文件3 元数据头 文件3 数据块 ... TRAILER!!! 结束标记 每个文件头固定 110 字节 + 文件名长度,数据块按 4 字节对齐填充 内核只识别 newc 格式(魔数 070701),不支持旧版 odc 或 binary 格式

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
常见误区: 虽然现代发行版仍将 initramfs 镜像文件命名为 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。

内核解压并挂载 initramfs 完整流程 1. Bootloader 加载内核 + initramfs 到物理内存指定位置 2. 内核自解压(vmlinuz 解压缩) initramfs 仍保持压缩状态在内存中 3. populate_rootfs() 调用 定位 initramfs 内存地址与大小 4. unpack_to_rootfs() 解压 支持 gzip/xz/lzma/bzip2/lzo/lz4 5. cpio 解包到 tmpfs 根目录 创建文件、目录、设备节点、硬链接 6. 执行 /init 进程 切换到 tmpfs 根,运行用户空间初始化 setup_header ramdisk_image/size init/main.c initramfs 初始化入口 tmpfs 动态分配 按需使用内存页

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 内部执行流程
关键理解: initramfs 的设计哲学是"最小化内核,最大化用户空间灵活性"。内核只负责将 cpio 归档解压到 tmpfs 并执行 /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