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(单用户系统不需要) |
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。
::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分区的选择,那是另一个维度的优化。