3. 文件系统裁剪:移除不必要的目录与文件、精简库文件、使用 BusyBox
文件系统裁剪,说白了就是给嵌入式系统「瘦身」。
我刚开始做裁剪时,总觉得这步很简单——删删文件不就完了?结果第一次裁剪完,系统启动到一半就崩了。嗯,从那以后我再也不敢小看它了。
这一章,我们聊聊文件系统裁剪的三个核心动作:移除不必要的目录与文件、精简库文件、使用 BusyBox。这三板斧砍下去,你的根文件系统能从几十 MB 瘦到几 MB。
核心目标:在保证系统功能完整的前提下,将根文件系统体积压缩到最小。通常嵌入式 Linux 的根文件系统目标在 2~8 MB 之间。
3.1 移除不必要的目录与文件
一个标准的 Linux 根文件系统,目录结构是固定的(bin、sbin、usr、lib、etc、dev、proc、sys、tmp、var、root、home、mnt、opt、run)。但嵌入式环境里,很多目录其实是空的,或者根本用不上。
我个人习惯的做法是:先列一个「必留清单」,剩下的全砍。
| 目录 | 是否保留 | 说明 |
|---|---|---|
| /bin, /sbin | 保留 | 存放 BusyBox 及核心命令 |
| /lib | 保留 | 动态库和加载器 |
| /etc | 保留 | 配置文件,但只保留必要的 |
| /dev | 保留 | 设备节点,可用 devtmpfs 自动挂载 |
| /proc, /sys | 保留 | 虚拟文件系统,启动时挂载 |
| /tmp | 保留 | 临时文件,挂载为 tmpfs |
| /var | 可选 | 日志和运行时数据,可链接到 /tmp |
| /root | 可选 | root 用户家目录,可删除或保留空目录 |
| /home | 删除 | 嵌入式系统通常没有多用户 |
| /mnt, /opt, /run | 删除 | 按需创建,不需要默认保留 |
| /usr | 合并 | 可将 /usr/bin、/usr/sbin 合并到 /bin、/sbin |
小技巧:我习惯把 /var 直接做成指向 /tmp 的软链接。这样日志和运行时数据都写在 tmpfs 上,重启自动清空,既省空间又省心。
文件层面的裁剪,我建议按这个顺序来:
- 删除文档和帮助文件:/usr/share/doc、/usr/share/man、/usr/share/info 这些,嵌入式设备上没人看。
- 删除 locale 和 i18n 数据:只保留 en_US.UTF-8,或者干脆全删。
- 删除不必要的头文件:/usr/include 只在编译时需要,运行时可以删。
- 删除静态库:*.a 文件只在静态链接时需要,动态链接环境下可以删。
- 删除调试符号:用
strip命令处理所有可执行文件和动态库。
# 删除文档和 locale
rm -rf $ROOTFS/usr/share/doc
rm -rf $ROOTFS/usr/share/man
rm -rf $ROOTFS/usr/share/locale
# strip 所有可执行文件和动态库
find $ROOTFS -type f -executable -exec strip --strip-all {} \;
find $ROOTFS -name "*.so*" -exec strip --strip-unneeded {} \;
注意:strip 操作不可逆。我曾经有一次 strip 了 libc.so,结果系统启动后所有动态链接的程序都报段错误。后来才发现,strip --strip-all 会把动态库的符号表也删掉,导致动态链接器无法解析符号。正确的做法是对动态库使用 --strip-unneeded,只删除调试符号,保留动态链接需要的符号。
3.2 精简库文件
库文件是文件系统体积的大头。一个完整的 glibc 动辄十几 MB,对于嵌入式系统来说太奢侈了。
精简库文件,我一般走三条路:
- 使用 uClibc 或 musl:替代 glibc,体积能降到 1/3 甚至更少。
- 只保留实际用到的库:用
ldd或readelf -d分析每个可执行文件的依赖,只复制需要的 .so 文件。 - 静态链接:对于核心工具,直接静态编译,省掉动态库。
你想想看,一个 BusyBox 如果静态编译,体积也就 1 MB 左右,根本不需要 libc 的支持。但代价是更新库时需要重新编译所有静态链接的程序。
我个人更推荐动态链接 + 按需复制库文件的方案。具体做法:
#!/bin/bash
# 分析依赖并复制库文件
ROOTFS=$1
BIN_DIR=$ROOTFS/bin
LIB_DIR=$ROOTFS/lib
# 收集所有可执行文件
find $BIN_DIR $ROOTFS/sbin -type f -executable | while read f; do
# 用 ldd 列出依赖
ldd $f 2>/dev/null | grep "=> /" | awk '{print $3}' | while read lib; do
cp -n $lib $LIB_DIR/ 2>/dev/null
# 处理软链接
if [ -L "$lib" ]; then
target=$(readlink -f $lib)
cp -n $target $LIB_DIR/ 2>/dev/null
fi
done
done
避坑指南:我曾经用这个脚本复制库文件,结果漏掉了 ld-linux.so(动态链接器本身)。系统启动后,所有动态链接的程序都报「No such file or directory」。记住,ld-linux.so 是动态链接的基石,必须手动复制到 /lib 下。
另外,库文件里有很多符号链接。比如 libc.so.6 可能指向 libc-2.31.so。复制时一定要保留符号链接结构,否则动态链接器找不到正确的库版本。
3.3 使用 BusyBox
BusyBox 是嵌入式 Linux 的瑞士军刀。它把几百个常用 Linux 命令打包成一个可执行文件,通过符号链接或 argv[0] 来区分调用哪个命令。
我刚开始用 BusyBox 时,觉得它就是个「阉割版」的工具集。后来才发现,它的配置选项非常灵活,你可以按需开启或关闭每个命令的功能。
BusyBox 的裁剪流程:
- 配置:运行
make menuconfig,选择需要的命令和功能。 - 编译:
make,生成 busybox 可执行文件。 - 安装:
make install CONFIG_PREFIX=$ROOTFS,自动创建符号链接。
配置时,我建议重点关注这几个选项:
| 配置项 | 建议 | 说明 |
|---|---|---|
| CONFIG_STATIC | 按需开启 | 静态编译,省掉 libc 依赖,但体积增大 |
| CONFIG_FEATURE_INSTALLER | 开启 | 支持 make install 自动创建符号链接 |
| CONFIG_FEATURE_SUID | 按需开启 | 支持 suid 权限,如 ping、mount |
| CONFIG_FEATURE_USE_INITTAB | 开启 | 支持 /etc/inittab 启动脚本 |
| CONFIG_FEATURE_TOP | 关闭 | top 命令在嵌入式环境很少用 |
| CONFIG_FEATURE_VI | 关闭 | vi 编辑器体积大,用 sed/awk 替代 |
经验之谈:我一般只开启 40~60 个命令,包括 init、sh、ls、cp、mv、rm、cat、echo、mount、umount、ps、kill、ifconfig、ping、sed、awk、grep、find、tar、gzip 等。其他命令如 ftpget、telnetd、httpd 等,按实际需求开启。
编译完成后,用 busybox --list 可以查看所有已编译的命令列表。如果发现少了某个命令,回去重新配置即可。
# 查看 BusyBox 支持的命令
$ ./busybox --list
[, [[, acpid, addgroup, adduser, adjtimex, ar, arp, arping, ash, ...
嗯,这里要注意:BusyBox 的 ash shell 和 GNU bash 有差异。有些脚本在 bash 下跑得好好的,换到 ash 就报语法错误。我遇到过最典型的是 [[ ]] 条件测试,ash 不支持,必须改成 [ ]。
3.4 知识体系总览
下面这张图,把文件系统裁剪的三大块串起来了。你可以把它当作裁剪时的检查清单。
3.5 裁剪后的验证
文件系统裁剪完,别急着烧录。先做一轮验证,不然启动时出问题,排查起来很痛苦。
我个人的验证流程:
- 检查目录结构:确保 /dev、/proc、/sys、/tmp 等必要目录存在。
- 检查动态库依赖:用
chroot进入新文件系统,运行每个命令,看有没有「cannot open shared object file」的错误。 - 检查符号链接:确保 BusyBox 的符号链接都正确创建,没有断链。
- 启动测试:用 QEMU 或真实硬件启动,观察内核日志,看有没有文件系统相关的错误。
# 用 chroot 测试文件系统
sudo chroot $ROOTFS /bin/sh
# 在 chroot 环境中测试命令
/bin/ls /lib
/bin/mount -t proc proc /proc
/bin/ps
重要提醒:chroot 环境需要 root 权限。而且 chroot 后,/dev 和 /proc 需要手动挂载,否则很多命令会报错。我一般会在 chroot 前先挂载好:mount --bind /dev $ROOTFS/dev 和 mount -t proc proc $ROOTFS/proc。
好了,文件系统裁剪的核心内容就这些。记住:裁剪不是目的,稳定运行才是。每次裁剪后都要做完整的回归测试,确保没有砍掉不该砍的东西。