4、最小化rootfs构建:BusyBox的裁剪与配置,只保留核心工具

上一章我们聊了rootfs的整体结构,这一章咱们来点实际的——怎么用BusyBox搭出一个最小可用的rootfs。说白了,就是只留那些真正干活儿的工具,把花里胡哨的东西全砍掉。

我刚开始做嵌入式的时候,也犯过傻。直接拿BusyBox默认配置编译,一烧进去,rootfs占了8MB多。板子Flash总共才16MB,内核和dtb一放,剩下的空间连个日志文件都存不下。后来才明白,BusyBox的裁剪,才是rootfs优化的第一道坎。

4.1 BusyBox是什么?为什么选它?

BusyBox,圈里人叫它「嵌入式瑞士军刀」。一个二进制文件,集成了几百个Linux常用命令。ls、cp、mv、sh、init……全塞在一个程序里。你想想看,一个完整的coreutils包编译出来可能几MB,BusyBox把这些命令的公共代码合并,最终镜像能压到几百KB。

为什么会这么小?因为BusyBox用了「多调用二进制」技术。所有命令共享一份C库、一份内存管理、一份文件I/O。每个命令只是一个符号链接,指向同一个busybox二进制,运行时通过argv[0]区分行为。

对比项 GNU coreutils BusyBox
二进制大小(ARM) ~4-8 MB ~300-800 KB
依赖库 glibc(~1.5 MB) uClibc/musl(~400 KB)
命令数量 ~100+ 独立二进制 ~300+ 集成在一个二进制
启动速度 较慢(多次fork+exec) 快(符号链接直接调用)

我个人习惯,只要Flash小于32MB,首选BusyBox。如果Flash只有4MB、8MB,那更是非它不可。

4.2 裁剪前的准备工作

别急着动手改配置。先搞清楚三件事:

  • 目标硬件:CPU架构(ARM/MIPS/RISC-V)、Flash大小、RAM大小
  • 启动方式:initramfs还是独立rootfs分区?这决定了你需不需要mount命令
  • 业务需求:你的应用到底需要哪些命令?只需要一个shell和网络?还是需要完整的文件系统工具?

我曾经接手过一个项目,前任工程师把BusyBox里所有网络工具都编译进去了——tftp、ftp、telnet、httpd……结果产品上线后,安全审计发现一堆漏洞。其实产品只需要一个wget用来下载固件。嗯,这就是典型的「用不上但留着」的浪费。

4.3 核心裁剪策略

BusyBox的配置在 .config 文件里,或者用 make menuconfig 图形界面改。我一般分四步走:

4.3.1 第一步:选择C库

BusyBox默认用glibc,但glibc太大了。对于最小化rootfs,我建议用musl或uClibc-ng。

# 在BusyBox配置中
Settings  --->
    [*] Build with musl libc (if using musl toolchain)
    # 或者
    [*] Build with uClibc (if using uClibc toolchain)

musl的优点是代码干净、静态链接后体积小。uClibc-ng兼容性好,很多老项目在用。我个人倾向musl,尤其是新项目。

4.3.2 第二步:砍掉不需要的Applet

BusyBox把命令分成几十个类别。我列一个「保命清单」和「可砍清单」:

类别 必须保留 可以砍掉
Shell ash, sh hush, msh, lash
文件管理 ls, cp, mv, rm, cat, mkdir, mount, umount cpio, tar, gzip(如果不用压缩包)
文本处理 cat, echo, printf, grep, sed awk, diff, patch, vi
网络 ifconfig, ping, wget(按需) telnet, ftp, httpd, tftp, nslookup
进程管理 ps, kill, init, reboot, poweroff top, pgrep, pkill
系统工具 dmesg, sysctl, uname, hostname hwclock, rdate, crond
其他 mdev(设备管理) login, passwd, adduser(单用户系统不需要)
我的经验: 先按「最小启动」配置——只保留init、ash、mount、mdev、ls、cat、echo、reboot。启动成功后,缺什么再加什么。这样不会漏掉关键依赖。

4.3.3 第三步:调整编译选项

在 Settings → General Configuration 里,有几个关键开关:

# 去掉不必要的功能
[*] Don't use /usr (减少目录层级)
[ ] Support for --long-options (去掉长选项支持,省空间)
[ ] Support for -a (去掉-a选项,省空间)
[ ] Enable locale support (去掉本地化,省空间)

# 优化大小
Settings  --->
    [*] Build BusyBox as a static binary (no shared libs)
    (0)  Force NOMMU build (如果CPU没有MMU)
    [*] Optimize for size (-Os)

静态链接是必须的。动态链接虽然省一点RAM,但你要额外带一个ld-linux和libc,算下来总大小反而更大。而且静态链接的busybox不怕库版本冲突,部署起来省心。

4.3.4 第四步:配置init和inittab

BusyBox的init是启动入口。它的配置文件是 /etc/inittab。最小化配置长这样:

::sysinit:/etc/init.d/rcS
::askfirst:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/umount -a -r

这里 ::sysinit 表示系统初始化时执行rcS脚本。::askfirst 表示在控制台等待用户按回车进入shell。如果你想要完全自动启动,改成 ::respawn:-/bin/sh。

注意: 如果inittab写错了,系统启动后直接panic。我曾经犯过这个错——把 ::sysinit 写成了 sysinit,少了前面的冒号,结果内核启动完就卡死了。排查了半天才发现是格式问题。

4.4 构建最小rootfs的完整步骤

光说不练假把式。我直接给出一套可用的构建脚本,你照着跑一遍就能得到一个最小rootfs:

#!/bin/bash
# 最小rootfs构建脚本(ARM架构示例)

# 1. 设置变量
BUSYBOX_VERSION=1.36.1
CROSS_COMPILE=arm-linux-musleabi-
ROOTFS_DIR=./rootfs_minimal

# 2. 下载并解压BusyBox
wget https://busybox.net/downloads/busybox-${BUSYBOX_VERSION}.tar.bz2
tar xf busybox-${BUSYBOX_VERSION}.tar.bz2
cd busybox-${BUSYBOX_VERSION}

# 3. 使用最小配置
make ARCH=arm CROSS_COMPILE=${CROSS_COMPILE} defconfig
# 手动裁剪:去掉不需要的applet(这里用sed快速修改)
sed -i 's/CONFIG_FEATURE_TOP=y/# CONFIG_FEATURE_TOP is not set/' .config
sed -i 's/CONFIG_VI=y/# CONFIG_VI is not set/' .config
sed -i 's/CONFIG_TELNET=y/# CONFIG_TELNET is not set/' .config
# 更多裁剪项...

# 4. 编译
make ARCH=arm CROSS_COMPILE=${CROSS_COMPILE} -j$(nproc)

# 5. 安装到rootfs目录
make ARCH=arm CROSS_COMPILE=${CROSS_COMPILE} CONFIG_PREFIX=${ROOTFS_DIR} install

# 6. 创建必要目录和文件
cd ${ROOTFS_DIR}
mkdir -p proc sys dev etc/init.d
cat > etc/inittab << 'EOF'
::sysinit:/etc/init.d/rcS
::askfirst:-/bin/sh
EOF

cat > etc/init.d/rcS << 'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
echo "Minimal rootfs started"
EOF
chmod +x etc/init.d/rcS

# 7. 创建设备节点(如果不用devtmpfs)
# mknod dev/console c 5 1
# mknod dev/null c 1 3

echo "Done! Rootfs size:"
du -sh ${ROOTFS_DIR}

跑完这个脚本,你得到的rootfs大概在500KB到1MB之间。具体大小取决于你裁剪了多少applet。

4.5 裁剪后的验证

构建完别急着烧录。先在QEMU里跑一下,验证能不能正常启动:

# 用QEMU测试最小rootfs
qemu-system-arm -M versatilepb \
    -kernel vmlinuz \
    -initrd rootfs_minimal.cpio.gz \
    -append "console=ttyAMA0" \
    -nographic

启动后,你应该能看到BusyBox的init打印信息,然后进入shell。试试几个基本命令:

# ls /bin
# cat /proc/cpuinfo
# mount
# reboot

如果都能正常工作,恭喜你,最小rootfs构建成功了。

核心原则: 裁剪不是越少越好,而是「刚好够用」。多一个命令就多一份攻击面,少一个命令就可能让业务跑不起来。平衡点在哪里?——启动后能完成所有业务功能,且没有冗余。

4.6 我踩过的坑

最后分享几个实战中遇到的坑,你遇到了可以少走弯路:

  • 坑1:砍掉了mount —— 结果init脚本里挂载proc/sysfs失败,系统启动后一堆设备找不到。mount是必须的,除非你用initramfs且不需要动态挂载。
  • 坑2:忘了mdev —— 没有mdev,/dev目录下只有静态节点。插个USB设备,内核报错说找不到设备节点。mdev是BusyBox自带的udev替代品,一定要带上。
  • 坑3:静态链接选了glibc —— glibc的静态链接体积巨大,一个busybox能到2MB。换成musl后直接降到600KB。选C库的时候想清楚。
  • 坑4:inittab格式错误 —— 前面说过了,少个冒号就panic。写完后用 busybox init -q 检查一下语法。

嗯,这一章的内容就这些。核心就一句话:BusyBox裁剪,砍掉的是冗余,留下的是稳定。下一章我们聊聊initramfs和独立rootfs分区的选择,那是另一个维度的优化。

BusyBox最小化rootfs构建流程 1. 选择C库 musl / uClibc-ng 2. 裁剪Applet 保留核心命令 3. 编译选项 静态链接 -Os 4. 配置init inittab + rcS 5. 构建rootfs make install 6. QEMU验证 启动测试 关键指标: • 最终rootfs大小:300KB - 1MB(取决于裁剪粒度) • 启动后RAM占用:< 2MB(含内核) 裁剪原则:先最小启动,再按需添加。不要一开始就追求极致。 构建阶段(步骤1-6)