4. 最小 Rootfs 构建:从零搭起文件系统骨架

好,咱们进入实战环节。前面几章讲了裁剪思路和工具链,这一章我们亲手搭一个最小的 rootfs。说白了,就是给内核准备一个能跑起来的「家」——有目录、有工具、有初始化进程。

我记得刚入行那会儿,总觉得 rootfs 很神秘,以为得用发行版那一套。后来被一个老工程师点醒:嵌入式系统里,rootfs 可以小到只有几个目录和一个 busybox。嗯,今天我们就来干这件事。

4.1 基础目录结构:五脏俱全

Linux 文件系统有约定俗成的目录布局,叫 FHS(Filesystem Hierarchy Standard)。但嵌入式场景下,我们不需要完全遵守,只保留最核心的即可。

我个人习惯的最小目录集是这样的:

rootfs/
├── bin       # 用户命令(ls, cp, sh 等)
├── sbin      # 系统管理命令(init, mount, ifconfig 等)
├── etc       # 配置文件
├── dev       # 设备节点
├── proc      # 进程文件系统挂载点
├── sys       # sysfs 挂载点
├── tmp       # 临时文件
├── usr
│   ├── bin
│   └── sbin
└── lib       # 动态库(如果 busybox 是动态链接)
我的经验: 别急着把 /var、/run、/home 都建上。最小系统跑起来后,缺什么再补什么。我见过有人一开始建了十几个空目录,结果调试时反而搞不清哪些是必要的。

创建命令很简单:

mkdir -p rootfs/{bin,sbin,etc,dev,proc,sys,tmp,usr/{bin,sbin},lib}

这里有个细节:/usr/bin 和 /usr/sbin 在最小系统中其实可以省略,因为 busybox 默认装在 /bin 和 /sbin。但我建议保留,方便后续扩展。

4.2 拷贝 Busybox 二进制:一个二进制搞定一切

Busybox 被称为「嵌入式瑞士军刀」。它把上百个 Linux 命令打包成一个二进制文件,通过符号链接来区分行为。

编译好 busybox 后(上一章讲过),你会得到一个 busybox 可执行文件。把它拷贝到 rootfs 的 /bin 下:

cp busybox rootfs/bin/
chmod 755 rootfs/bin/busybox

然后创建符号链接。比如 ls 命令,其实就是指向 busybox 的软链接:

ln -s /bin/busybox rootfs/bin/ls
ln -s /bin/busybox rootfs/bin/cp
ln -s /bin/busybox rootfs/bin/mv
# ... 其他命令同理

手动一个个创建太麻烦。Busybox 提供了自动安装功能:

cd rootfs
/bin/busybox --install -s

这条命令会在当前目录的 /bin、/sbin、/usr/bin、/usr/sbin 下自动创建所有符号链接。

注意: 如果 busybox 是动态链接的,你还需要把依赖的库文件拷贝到 /lib 下。用 ldd busybox 查看依赖,比如 libc.so、ld-linux.so 等。我曾经踩过这个坑——系统启动后 busybox 报 "No such file or directory",其实就是缺库。

检查依赖的命令:

ldd rootfs/bin/busybox

输出类似:

linux-vdso.so.1 (0x00007fff...)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f...)

把这些库拷贝到 rootfs 的 /lib 下:

cp /lib/x86_64-linux-gnu/libc.so.6 rootfs/lib/
cp /lib64/ld-linux-x86-64.so.2 rootfs/lib/

4.3 配置 init 进程:系统启动的起点

内核启动完成后,会尝试执行第一个用户空间程序——init。默认路径是 /sbin/init,如果找不到,会依次尝试 /bin/init、/bin/sh。

Busybox 本身就提供了 init 功能。我们只需要创建符号链接:

ln -s /bin/busybox rootfs/sbin/init

然后编写 /etc/inittab 文件,告诉 init 进程该做什么。一个最简的 inittab 如下:

::sysinit:/etc/init.d/rcS
::askfirst:-/bin/sh

解释一下:

  • ::sysinit:系统初始化时执行 /etc/init.d/rcS 脚本
  • ::askfirst:在控制台启动一个 shell,启动前询问用户

接着创建 /etc/init.d/rcS 脚本:

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
echo "Welcome to Minimal Rootfs!"

别忘了加执行权限:

chmod 755 rootfs/etc/init.d/rcS
核心要点: init 进程是用户空间的第一个进程,PID 为 1。如果它挂了,整个系统就 panic 了。所以 inittab 和 rcS 脚本一定要反复检查,我一般会在 rcS 里加一句 exec /bin/sh 兜底,确保 shell 不会意外退出。

4.4 设备节点:让内核与用户空间对话

/dev 目录下需要最基本的设备节点。最精简的情况下,你只需要:

  • console:控制台设备,内核输出信息用
  • null:空设备,丢弃不需要的输出

创建方式:

mknod rootfs/dev/console c 5 1
mknod rootfs/dev/null c 1 3

参数说明:c 表示字符设备,后面的数字是主设备号和次设备号。

如果你后续要挂载 devtmpfs(内核自动管理设备节点),那 /dev 甚至可以只留一个 console。但为了保险,我习惯把 console 和 null 都手动创建好。

4.5 整体流程梳理

下面这张图展示了最小 rootfs 的构建流程和启动逻辑:

最小 Rootfs 构建与启动流程 构建阶段 创建目录结构 拷贝 Busybox 创建符号链接 拷贝库文件 配置阶段 配置 /etc/inittab 编写 rcS 脚本 创建设备节点 启动阶段 内核启动 执行 /sbin/init 解析 inittab 执行 rcS → Shell 关键:init 是 PID 1,必须稳定运行;rcS 脚本建议加 exec /bin/sh 兜底

4.6 验证与测试

构建完成后,可以用 QEMU 快速验证:

qemu-system-x86_64 -kernel bzImage -initrd rootfs.cpio.gz -append "console=ttyS0" -nographic

如果一切正常,你会看到内核启动日志,然后进入 shell 提示符。输入 ls /bin,应该能看到 busybox 和一堆符号链接。

避坑指南: 我曾经在 rcS 脚本里忘了挂载 /proc,结果 ps 命令报错。还有一次把 inittab 的格式写错了——每行必须用 :: 分隔,少一个冒号都不行。启动后直接 panic,查了半天才发现。

4.7 本章小结

最小 rootfs 的构建,本质上就是三件事:

  1. 目录结构:/bin、/sbin、/etc、/dev、/proc、/sys、/tmp、/lib
  2. Busybox:一个二进制 + 符号链接 + 动态库(如果需要)
  3. init 配置:/sbin/init → /etc/inittab → /etc/init.d/rcS