4、构建最小根文件系统:使用BusyBox构建最小initramfs、编译静态链接的init程序、生成cpio归档文件

根文件系统是Linux系统启动后挂载的第一个文件系统,它包含了系统运行所需的核心目录结构、设备节点、配置文件和初始化程序。在QEMU系统级模拟中,initramfs(initial RAM filesystem)是一种将根文件系统打包到内存中的机制,无需磁盘即可完成系统引导。本章将使用BusyBox构建一个极简的initramfs,并编译一个静态链接的init程序作为系统的第一个用户态进程。

为什么选择initramfs? initramfs直接嵌入内核映像或作为独立cpio归档由引导加载程序加载,无需块设备驱动支持,非常适合QEMU快速验证环境。其大小通常控制在1~4MB以内,启动速度极快。

4.1 整体架构与构建流程

构建最小根文件系统的流程分为三个核心阶段:编译BusyBox(提供常用Shell命令)、编写并编译静态init程序(系统第一个进程)、打包为cpio归档(initramfs标准格式)。下图展示了从源码到QEMU启动的完整链路。

最小initramfs构建与启动流程 ① 编译BusyBox make defconfig && make ② 编写静态init aarch64-linux-gnu-gcc -static ③ 生成cpio归档 find . | cpio -o -H newc ④ 内核集成 CONFIG_INITRAMFS_SOURCE ⑤ QEMU加载 -initrd initramfs.cpio.gz ⑥ 系统启动 init进程 → Shell 关键路径: BusyBox提供/bin/sh和基础命令 → init程序挂载sysfs/procfs并执行sh → 用户获得交互式Shell

4.2 编译BusyBox

BusyBox是一个集成了数百个精简版Unix命令的单一可执行文件,是嵌入式最小根文件系统的核心组件。我们将其编译为静态链接版本,避免依赖外部动态库。

4.2.1 下载与配置

# 下载BusyBox源码(以1.36.1为例)
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xf busybox-1.36.1.tar.bz2
cd busybox-1.36.1/

# 使用默认配置,然后调整为静态编译
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig

在menuconfig界面中,必须修改以下选项:

配置项 路径 设置值 说明
静态链接 Settings → Build static binary (no shared libs) [*] 避免依赖动态库,确保在initramfs中独立运行
目标架构 Settings → Target architecture arm64 指定ARM64(AArch64)交叉编译
交叉编译器前缀 Settings → Cross compiler prefix aarch64-linux-gnu- 与工具链前缀一致
安装路径 Settings → Destination path for 'make install' ./_install 输出到本地目录,便于后续打包

4.2.2 编译与安装

# 编译(使用-jN加速,N为CPU核心数)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)

# 安装到_install目录
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- install

# 查看安装结果
ls -la _install/
# 输出应包含 bin/ sbin/ usr/ 等目录及busybox可执行文件
验证静态链接: 使用 file _install/bin/busybox 命令,输出应包含 "statically linked" 字样。如果显示 "dynamically linked",则需要返回menuconfig确认静态编译选项已开启。

4.3 编写并编译静态init程序

init是内核启动后执行的第一个用户态进程(PID=1)。在最小系统中,init程序负责挂载虚拟文件系统、启动Shell。我们使用C语言编写一个极简的静态链接init。

4.3.1 init源码

// init.c - 最小init程序
#include <stdio.h>
#include <unistd.h>
#include <sys/mount.h>
#include <sys/reboot.h>

int main(void) {
    // 挂载procfs,提供进程信息
    mkdir("/proc", 0755);
    mount("proc", "/proc", "proc", 0, NULL);

    // 挂载sysfs,提供设备与驱动信息
    mkdir("/sys", 0755);
    mount("sysfs", "/sys", "sysfs", 0, NULL);

    // 挂载devtmpfs,自动创建设备节点
    mkdir("/dev", 0755);
    mount("devtmpfs", "/dev", "devtmpfs", 0, NULL);

    // 打印启动信息
    printf("Minimal initramfs started. PID=1\n");
    printf("Mounting virtual filesystems... OK\n");

    // 执行交互式Shell
    printf("Launching /bin/sh...\n");
    execl("/bin/sh", "/bin/sh", NULL);

    // 如果execl失败,进入紧急循环
    printf("ERROR: Failed to start shell!\n");
    while(1) pause();
    return 0;
}

4.3.2 静态编译

# 使用aarch64交叉编译器静态编译
aarch64-linux-gnu-gcc -static -o init init.c

# 验证文件类型
file init
# 输出示例:init: ELF 64-bit LSB executable, ARM aarch64, statically linked
常见错误: 如果编译时提示 fatal error: sys/mount.h: No such file,说明交叉工具链缺少内核头文件。安装 linux-libc-dev-arm64-cross 包或手动将内核头文件复制到工具链的sysroot目录。

4.4 构建根文件系统目录结构

在打包之前,需要将BusyBox安装输出、init程序以及必要的设备节点和配置文件组织成标准的根文件系统目录树。

# 创建根文件系统目录
mkdir -p rootfs
cd rootfs

# 复制BusyBox安装内容
cp -a ../busybox-1.36.1/_install/* .

# 复制静态init程序
cp ../init .

# 创建必要的目录
mkdir -p proc sys dev etc

# 创建etc/inittab(可选,BusyBox init需要)
cat > etc/inittab << 'EOF'
::sysinit:/bin/mount -t proc proc /proc
::sysinit:/bin/mount -t sysfs sysfs /sys
::sysinit:/bin/mount -t devtmpfs devtmpfs /dev
::respawn:-/bin/sh
EOF

# 设置init为可执行
chmod +x init

# 查看最终目录结构
find . | head -30

最终目录结构应包含以下核心元素:

路径 类型 说明
init 可执行文件 静态编译的init程序,PID=1
bin/busybox 可执行文件 BusyBox多调用二进制,提供sh、ls、mount等命令
bin/sh 符号链接 指向/bin/busybox,提供交互式Shell
proc/ 目录 procfs挂载点
sys/ 目录 sysfs挂载点
dev/ 目录 devtmpfs挂载点,自动生成设备节点
etc/inittab 配置文件 BusyBox init的启动脚本(可选)

4.5 生成cpio归档文件

cpio是initramfs的标准打包格式。内核在启动时从cpio归档中解压出根文件系统到内存中。我们先生成未压缩的cpio,再使用gzip压缩以减小体积。

4.5.1 创建cpio归档

# 进入rootfs目录
cd rootfs

# 生成cpio归档(newc格式,保证设备节点权限)
find . | cpio -o -H newc -R root:root | gzip > ../initramfs.cpio.gz

# 查看归档大小
ls -lh ../initramfs.cpio.gz
# 典型大小:1.5MB ~ 3MB
关键参数说明:
-o:创建归档(输出模式)
-H newc:使用newc格式(SVR4 portable format with CRC),Linux内核要求使用此格式
-R root:root:将归档内所有文件的所有者设为root,避免权限问题
gzip:压缩归档,内核支持自动解压gzip格式的initramfs

4.5.2 验证cpio归档内容

# 解压验证(不实际解压到文件系统)
zcat ../initramfs.cpio.gz | cpio -t -H newc | head -20

# 输出示例:
# .
# ./init
# ./bin
# ./bin/busybox
# ./bin/sh
# ./proc
# ./sys
# ./dev
# ./etc
# ./etc/inittab

4.6 将initramfs集成到内核或QEMU命令行

有两种方式使用生成的initramfs:嵌入内核(编译时集成)或作为独立文件(QEMU运行时加载)。对于课程实验,推荐使用QEMU命令行加载方式,便于快速迭代。

4.6.1 方式一:QEMU命令行加载(推荐)

qemu-system-aarch64 \
    -machine virt \
    -cpu cortex-a57 \
    -kernel Image \
    -initrd initramfs.cpio.gz \
    -append "console=ttyAMA0" \
    -nographic

4.6.2 方式二:嵌入内核

将cpio归档路径配置到内核的CONFIG_INITRAMFS_SOURCE选项:

# 在内核源码目录执行
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig

# 进入 General setup → Initial RAM filesystem and RAM disk (initramfs/initrd) support
# 设置 Initramfs source file(s) 为 initramfs.cpio.gz 的绝对路径

# 重新编译内核
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) Image

4.7 启动验证与常见问题

使用QEMU启动后,如果一切正常,你将看到内核启动信息,随后进入交互式Shell:

# QEMU启动输出(节选)
...
[    0.000000] Linux version 6.1.0 ...
...
[    0.500000] Freeing unused kernel memory: 1024K
[    0.510000] Run /init as init process
Minimal initramfs started. PID=1
Mounting virtual filesystems... OK
Launching /bin/sh...
/ # _

在Shell中,可以执行ls /proc、mount等命令验证系统状态。

问题现象 可能原因 解决方案
Kernel panic - not syncing: No working init found init程序路径错误或不可执行 确认rootfs根目录下有init文件,且为静态链接ARM64可执行文件
init: must be run as PID 1 init程序未静态链接,依赖动态库 使用 file init 检查,重新编译为静态链接
sh: can't access tty; job control turned off 缺少/dev/console或/dev/tty设备 确保init中挂载了devtmpfs,或在cpio归档中包含设备节点
cpio: premature end of file cpio归档损坏或格式错误 使用 cpio -t 验证归档完整性,确认使用-H newc格式
调试技巧: 如果系统启动失败,可以在QEMU命令行中添加 -append "console=ttyAMA0 initcall_debug" 查看内核初始化详细日志,定位initramfs加载阶段的具体错误。

4.8 本章小结

通过本章的实践,我们完成了以下关键任务:

  • 使用BusyBox构建了包含常用命令的最小根文件系统
  • 编写并静态编译了自定义init程序,负责挂载虚拟文件系统和启动Shell
  • 生成了符合内核要求的cpio归档格式initramfs
  • 通过QEMU验证了initramfs的启动过程

这个最小根文件系统虽然只有2~3MB,但已经具备了完整的Linux用户态环境,可以作为后续课程中设备树验证、驱动开发和系统调试的基础平台。