4. rootfs 目录结构优化:精简 /bin、/sbin、/lib、/usr 等目录
嵌入式系统的存储空间,说白了就是寸土寸金。Flash 容量就那么大,你多放一个没用的库,用户就少放一个功能。很多工程师拿到 BSP 后,直接把 buildroot 或 Yocto 生成的 rootfs 打包烧进去,结果发现 128MB 的 Flash 用了 90MB,其中一半还是根本用不上的东西。
我个人习惯,拿到 rootfs 的第一件事就是「瘦身」。不是盲目删文件,而是有策略地清理。这一章,我们就从目录结构入手,把 /bin、/sbin、/lib、/usr 这些大头彻底过一遍。
4.1 为什么要动 rootfs 目录结构?
你想想看,一个标准的 Linux 发行版,/bin 下面有上百个命令。但你的嵌入式设备可能只跑一个业务进程,连 shell 都不需要。那些 ls、cat、grep 真的有必要吗?
我在项目中遇到过一件事:某款工业网关,Flash 只有 64MB,rootfs 占了 58MB,升级包都塞不进去。后来一查,/usr/share 下面光 locale 和 man 就占了 12MB。这些在服务器上无所谓,但在嵌入式设备上就是浪费。
精简的核心原则就一条:只保留系统启动和业务运行必需的文件。其他的一律砍掉。
精简目标:
- 启动流程能走通(init、shell、核心库)
- 业务进程能运行(依赖的 so、配置文件)
- 调试工具按需保留(strace、gdbserver 等只在开发版保留)
4.2 /bin 和 /sbin:砍掉用不上的命令
/bin 和 /sbin 是命令的大本营。很多嵌入式系统用 busybox,一个二进制文件就提供了几十个命令。但 busybox 默认会编译很多你根本用不上的 applet。
我建议的做法是:
- 按需配置 busybox:在 busybox 的 .config 里,只勾选你需要的命令。比如只需要 shell、mount、init、reboot,那就只保留这些。
- 删除符号链接:busybox 安装时会为每个命令创建符号链接。如果你只保留了 10 个命令,那 /bin 下就只有 10 个链接,干净利落。
- 考虑用 toybox 替代:toybox 比 busybox 更轻量,代码更精简。如果你的系统对体积极度敏感,可以试试。
避坑指南:
我曾经在某个项目里,为了省空间把 /sbin/init 删了,结果系统启动到一半就挂了。后来才发现,内核启动参数里指定了 init=/sbin/init,而 busybox 的 init 符号链接指向了 /bin/busybox。所以删之前,一定要确认符号链接的指向关系。
下面是一个典型的精简前后对比:
| 目录 | 精简前(命令数) | 精简后(命令数) | 说明 |
|---|---|---|---|
| /bin | 120+ | 15~20 | 只保留 sh、mount、cat、echo、ls(调试用)等 |
| /sbin | 80+ | 5~8 | 只保留 init、reboot、poweroff、mdev/udev |
| /usr/bin | 200+ | 0~5 | 业务进程放这里,其他全删 |
| /usr/sbin | 50+ | 0~3 | 网络配置工具按需保留 |
4.3 /lib:库文件是体积大户
/lib 目录存放的是动态链接库(.so 文件)。这是 rootfs 里最占空间的地方之一。一个 libc.so 就 1~2MB,再加上 libm、libpthread、libstdc++,轻松上 10MB。
怎么优化?我总结了三个步骤:
- 第一步:用 ldd 分析依赖。把你所有的业务进程和关键命令用 ldd 跑一遍,看看它们到底依赖哪些 so。很多 so 其实根本没人用。
- 第二步:考虑静态链接。对于体积敏感的场景,可以把业务进程静态链接。虽然单个二进制文件会变大,但省掉了整个 lib 目录。我有个项目,静态链接后 rootfs 从 32MB 降到了 8MB。
- 第三步:使用 musl libc 替代 glibc。musl 比 glibc 小得多,而且兼容性不错。如果你的应用不依赖 glibc 的特殊功能,换 musl 能省 1~2MB。
注意:
删 so 文件时一定要小心。我曾经删了一个 libnss_files.so,结果 getpwnam() 调用直接返回 NULL,导致某个进程无法启动。删之前,先用 ldd 确认没有隐藏依赖。
4.4 /usr:最容易藏污纳垢的地方
/usr 目录在嵌入式系统里经常被滥用。很多 build 系统会把头文件、静态库、文档、locale 都塞进去。这些东西在开发机上没问题,但在目标设备上就是垃圾。
我建议的清理清单:
- /usr/include:全部删除。头文件只在编译时需要,运行时不需要。
- /usr/lib:只保留 .so 文件,删除 .a 静态库和 .la libtool 文件。
- /usr/share:删除 locale、man、info、doc 等。只保留必要的配置文件(如 zoneinfo)。
- /usr/src:全部删除。内核源码不会在目标设备上编译。
一个实际案例:
某款路由器,rootfs 原始大小 28MB。清理 /usr/share/locale 后降到 22MB,再删除 /usr/include 和静态库后降到 16MB。总共省了 12MB,占原始体积的 43%。
4.5 符号链接:别让链接成为累赘
符号链接本身不占多少空间,但数量多了也会浪费 inode。而且,混乱的链接关系会让维护变得困难。
我遇到过最夸张的情况:某个 rootfs 里 /bin 下有 200 多个符号链接,全部指向 /bin/busybox。但实际只用了 30 个命令。剩下的 170 个链接就是纯浪费。
优化方法很简单:
- 只创建用到的链接。在 busybox 的安装脚本里,指定只安装你勾选的 applet。
- 合并目录。有些系统把 /bin 和 /sbin 合并到 /usr/bin,然后用符号链接指向。我个人不太推荐这种做法,因为会破坏 FHS 标准,导致某些脚本找不到命令。
- 检查 dangling link。用 find -L -type l 找出所有断开的符号链接,然后删掉。
4.6 整体优化流程图
下面这张图展示了 rootfs 目录结构优化的完整流程。你可以把它当作一个检查清单,每做完一步就打个勾。
4.7 实战:一个精简脚本示例
下面是我常用的一个精简脚本片段。它会在构建 rootfs 后自动清理无用文件。你可以根据自己的需求修改。
#!/bin/bash
# rootfs 精简脚本
ROOTFS_DIR=$1
echo "=== 清理 /usr/include ==="
rm -rf $ROOTFS_DIR/usr/include
echo "=== 清理静态库 ==="
find $ROOTFS_DIR -name "*.a" -delete
find $ROOTFS_DIR -name "*.la" -delete
echo "=== 清理 locale(只保留 en_US) ==="
find $ROOTFS_DIR/usr/share/locale -mindepth 1 -maxdepth 1 ! -name "en_US" -exec rm -rf {} +
echo "=== 清理文档 ==="
rm -rf $ROOTFS_DIR/usr/share/doc
rm -rf $ROOTFS_DIR/usr/share/man
rm -rf $ROOTFS_DIR/usr/share/info
echo "=== 删除 dangling 符号链接 ==="
find $ROOTFS_DIR -type l -xtype l -delete
echo "=== 精简完成 ==="
du -sh $ROOTFS_DIR
小技巧:
跑完脚本后,用 du -sh $ROOTFS_DIR/* 看看每个目录的大小。如果某个目录特别大,就进去看看是什么文件。我经常用这个方法发现一些「意外惊喜」——比如某个开发库忘记删了。
4.8 验证:精简后一定要测试
精简完 rootfs 后,别急着烧录。先做一轮验证:
- 启动测试:用 QEMU 或实际硬件启动,看能不能进 shell。
- 功能测试:跑一遍业务进程,确认所有功能正常。
- 压力测试:模拟异常情况,比如断电重启、内存不足等。
我记得有一次,精简完 rootfs 后系统能正常启动,但业务进程跑了一个小时后挂了。查了半天,发现是某个 so 被误删了,但启动时没加载,直到业务进程调用某个函数时才崩溃。所以,长时间运行测试也很重要。