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 上,重启自动清空,既省空间又省心。

文件层面的裁剪,我建议按这个顺序来:

  1. 删除文档和帮助文件:/usr/share/doc、/usr/share/man、/usr/share/info 这些,嵌入式设备上没人看。
  2. 删除 locale 和 i18n 数据:只保留 en_US.UTF-8,或者干脆全删。
  3. 删除不必要的头文件:/usr/include 只在编译时需要,运行时可以删。
  4. 删除静态库:*.a 文件只在静态链接时需要,动态链接环境下可以删。
  5. 删除调试符号:用 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 的裁剪流程:

  1. 配置:运行 make menuconfig,选择需要的命令和功能。
  2. 编译:make,生成 busybox 可执行文件。
  3. 安装: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 知识体系总览

下面这张图,把文件系统裁剪的三大块串起来了。你可以把它当作裁剪时的检查清单。

文件系统裁剪核心流程 根文件系统裁剪 移除目录与文件 精简库文件 使用 BusyBox 删除文档/locale 删除头文件/静态库 strip 调试符号 使用 uClibc/musl ldd 分析依赖 按需复制 .so 保留符号链接结构 make menuconfig 按需开启命令 make install 创建链接 裁剪后验证 启动测试 → 功能测试 → 压力测试 → 回归测试

3.5 裁剪后的验证

文件系统裁剪完,别急着烧录。先做一轮验证,不然启动时出问题,排查起来很痛苦。

我个人的验证流程:

  1. 检查目录结构:确保 /dev、/proc、/sys、/tmp 等必要目录存在。
  2. 检查动态库依赖:用 chroot 进入新文件系统,运行每个命令,看有没有「cannot open shared object file」的错误。
  3. 检查符号链接:确保 BusyBox 的符号链接都正确创建,没有断链。
  4. 启动测试:用 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。

好了,文件系统裁剪的核心内容就这些。记住:裁剪不是目的,稳定运行才是。每次裁剪后都要做完整的回归测试,确保没有砍掉不该砍的东西。