4. 文件系统裁剪:BusyBox构建最小根文件系统

文件系统裁剪,说白了就是给系统「瘦身」。车载嵌入式设备存储空间有限,动不动就几百兆的根文件系统肯定不行。我做过一个项目,Flash总共才128MB,系统、应用、数据全塞进去,根文件系统必须控制在30MB以内。这时候,BusyBox就是我们的救星。

4.1 BusyBox:瑞士军刀式的工具集

BusyBox把几百个Linux命令打包成一个二进制文件。它通过符号链接来区分调用哪个命令。比如你执行ls,实际上调用的还是/bin/busybox ls。

构建最小根文件系统,我一般分三步走:

  1. 配置BusyBox:只选需要的命令,关掉调试符号
  2. 编译安装:生成_install目录
  3. 手工补全:添加必要的设备节点、配置文件、库文件

先看配置这一步。我个人习惯用make menuconfig,图形界面直观一些。

# 下载解压
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar xvf busybox-1.36.1.tar.bz2
cd busybox-1.36.1

# 配置
make menuconfig

# 关键配置项
# Settings -> Build static binary (no shared libs)  [建议不选,除非你很清楚后果]
# Settings -> Cross compiler prefix = arm-linux-gnueabihf-
# Settings -> Destination path = ./_install
我的经验:静态编译虽然省去了拷贝动态库的麻烦,但二进制体积会膨胀到1MB以上。车载场景下,我更倾向于动态编译,配合musl库,整体反而更小。

配置完成后,编译安装:

make -j4
make install

安装完成后,_install目录下就有了bin、sbin、usr等基本结构。但这只是骨架,还需要手动添加:

  • 设备节点:/dev/null、/dev/console、/dev/tty*
  • 初始化脚本:/etc/inittab、/etc/init.d/rcS
  • 配置文件:/etc/fstab、/etc/passwd(至少要有root)
  • 库文件:从交叉工具链中拷贝需要的.so
# 创建设备节点
mkdir -p _install/dev
sudo mknod _install/dev/null c 1 3
sudo mknod _install/dev/console c 5 1

# 创建基本目录
mkdir -p _install/etc/init.d
mkdir -p _install/proc
mkdir -p _install/sys
mkdir -p _install/tmp
mkdir -p _install/var

# 编写初始化脚本
cat > _install/etc/inittab << 'EOF'
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
EOF

cat > _install/etc/init.d/rcS << 'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
echo "Welcome to Minimal Linux"
EOF
chmod +x _install/etc/init.d/rcS
注意:设备节点必须用mknod创建,不能直接拷贝。我曾经图省事从开发板拷贝了/dev目录,结果启动时控制台报错,折腾了半天才发现是主次设备号不对。

4.2 glibc vs musl vs uclibc:C库三选一

C库是根文件系统里体积最大的部分之一。glibc动辄5MB以上,对于车载系统来说太奢侈了。musl和uclibc就是为嵌入式场景设计的轻量级替代品。

特性 glibc musl uclibc-ng
体积(最小配置) ~5MB ~400KB ~300KB
POSIX兼容性 ★★★★★ ★★★★☆ ★★★☆☆
性能 高 中高 中
动态链接器 ld-linux.so ld-musl-*.so ld-uClibc.so
多线程支持 NPTL 自实现 NPTL(部分)
适用场景 通用Linux 嵌入式/容器 极简嵌入式

我个人的选择原则很简单:

  • glibc:除非你的应用强依赖glibc特有的功能(比如iconv、libnss),否则别用。车载IVI系统如果跑Qt、Wayland这些重型组件,可能不得不选glibc。
  • musl:我的首选。体积适中,兼容性好,而且musl的代码质量很高,安全审计报告比glibc干净得多。我最近几个项目都用的musl。
  • uclibc-ng:追求极致体积时考虑。但要注意,有些软件包对uclibc支持不好,编译时可能需要打补丁。
避坑指南:我曾经在一个项目里用uclibc编译Python,结果折腾了两周,最后还是换回了musl。如果你要跑脚本语言或复杂中间件,建议直接上musl。

切换C库需要重新编译整个工具链。以musl为例,可以用musl-cross-make项目快速搭建:

git clone https://github.com/richfelker/musl-cross-make
cd musl-cross-make

# 配置目标架构
echo "TARGET = arm-linux-musleabihf" > config.mak
echo "COMMON_CONFIG += --with-arch=armv7-a --with-fpu=neon --with-float=hard" >> config.mak

make -j4
make install

编译完成后,用arm-linux-musleabihf-gcc代替原来的gcc,链接时自动使用musl库。

4.3 删除不必要的库与工具

文件系统裁剪,最核心的原则就一句话:用不到的,统统删掉。但怎么知道哪些是用不到的?我有一套标准流程:

  1. 分析依赖:用readelf -d或ldd查看每个二进制文件的动态库依赖
  2. 按需拷贝:只拷贝被实际引用的库文件
  3. 剔除符号表:用strip去掉调试信息
  4. 清理文档和locale:/usr/share、/usr/lib/locale这些目录直接清空

来看一个实际例子。假设我们的根文件系统里有/bin/busybox和/usr/bin/myapp两个二进制文件,怎么确定需要哪些库?

# 查看busybox的依赖
$ arm-linux-readelf -d _install/bin/busybox | grep NEEDED
 0x00000001 (NEEDED)             Shared library: [libm.so.6]
 0x00000001 (NEEDED)             Shared library: [libc.so.6]

# 查看myapp的依赖
$ arm-linux-readelf -d _install/usr/bin/myapp | grep NEEDED
 0x00000001 (NEEDED)             Shared library: [libpthread.so.0]
 0x00000001 (NEEDED)             Shared library: [libc.so.6]
 0x00000001 (NEEDED)             Shared library: [libm.so.6]

然后从交叉工具链中拷贝这些库:

# 找到库文件路径
$ arm-linux-gcc -print-sysroot
/home/user/toolchain/arm-linux-musleabihf/arm-linux-musleabihf/sysroot

# 拷贝需要的库
mkdir -p _install/lib
cp /path/to/sysroot/lib/libc.so.6 _install/lib/
cp /path/to/sysroot/lib/libm.so.6 _install/lib/
cp /path/to/sysroot/lib/libpthread.so.0 _install/lib/
cp /path/to/sysroot/lib/ld-linux.so.3 _install/lib/

接下来是strip操作。strip可以去掉符号表和调试信息,体积能减少50%以上:

# strip二进制文件
arm-linux-strip _install/bin/busybox
arm-linux-strip _install/usr/bin/myapp

# strip库文件(保留符号表用于调试,但去掉调试信息)
arm-linux-strip --strip-debug _install/lib/*.so*
小技巧:可以用find命令配合-exec批量strip。但要注意,有些库strip后可能无法正常工作,比如libpthread.so。我一般先strip一个,测试没问题再批量操作。

最后,清理掉所有非必要文件:

# 删除文档
rm -rf _install/usr/share/doc
rm -rf _install/usr/share/man
rm -rf _install/usr/share/info

# 删除locale(只保留C.UTF-8或en_US)
rm -rf _install/usr/lib/locale/*
cp -r /path/to/locale/C.UTF-8 _install/usr/lib/locale/

# 删除静态库
find _install -name "*.a" -delete

# 删除头文件
rm -rf _install/usr/include

经过这一轮裁剪,一个原本50MB的根文件系统,通常能压缩到10MB以内。我做过最极致的一个项目,最终根文件系统只有4.2MB,跑一个TCP网络服务和几个监控脚本,稳得很。

核心原则:裁剪不是目的,稳定才是。每删一个文件,都要问自己:这个文件真的没用吗?删了之后系统还能正常启动吗?我的建议是:先做最小系统,验证通过后,再按需添加组件。而不是先做完整系统,再盲目删除。
文件系统裁剪核心流程 完整根文件系统(~50MB) ① BusyBox 替换标准工具 ② 选择C库(musl/uclibc) ③ readelf 分析动态库依赖 ④ strip + 删除文档/locale/头文件 最小根文件系统(~5-10MB) 关键指标 • 体积减少 80%+ • 启动时间 < 3秒 • 内存占用 < 32MB • 仅保留必要组件