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。

我建议的做法是:

  1. 按需配置 busybox:在 busybox 的 .config 里,只勾选你需要的命令。比如只需要 shell、mount、init、reboot,那就只保留这些。
  2. 删除符号链接:busybox 安装时会为每个命令创建符号链接。如果你只保留了 10 个命令,那 /bin 下就只有 10 个链接,干净利落。
  3. 考虑用 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 个链接就是纯浪费。

优化方法很简单:

  1. 只创建用到的链接。在 busybox 的安装脚本里,指定只安装你勾选的 applet。
  2. 合并目录。有些系统把 /bin 和 /sbin 合并到 /usr/bin,然后用符号链接指向。我个人不太推荐这种做法,因为会破坏 FHS 标准,导致某些脚本找不到命令。
  3. 检查 dangling link。用 find -L -type l 找出所有断开的符号链接,然后删掉。

4.6 整体优化流程图

下面这张图展示了 rootfs 目录结构优化的完整流程。你可以把它当作一个检查清单,每做完一步就打个勾。

rootfs 目录结构优化流程 1. 分析业务依赖 2. 清理 /bin /sbin 命令 3. 优化 /lib 库文件 4. 清理 /usr 目录 5. 整理符号链接 6. 验证启动与功能 关键检查点 • ldd 分析所有二进制 • 确认 init 和 shell 可用 • 测试业务进程启动 常见陷阱 • 删了 init 导致无法启动 • 漏了 nss 库导致用户查询失败 • 符号链接指向错误路径

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 被误删了,但启动时没加载,直到业务进程调用某个函数时才崩溃。所以,长时间运行测试也很重要。