4. 文件系统裁剪:BusyBox构建最小根文件系统
文件系统裁剪,说白了就是给系统「瘦身」。车载嵌入式设备存储空间有限,动不动就几百兆的根文件系统肯定不行。我做过一个项目,Flash总共才128MB,系统、应用、数据全塞进去,根文件系统必须控制在30MB以内。这时候,BusyBox就是我们的救星。
4.1 BusyBox:瑞士军刀式的工具集
BusyBox把几百个Linux命令打包成一个二进制文件。它通过符号链接来区分调用哪个命令。比如你执行ls,实际上调用的还是/bin/busybox ls。
构建最小根文件系统,我一般分三步走:
- 配置BusyBox:只选需要的命令,关掉调试符号
- 编译安装:生成
_install目录 - 手工补全:添加必要的设备节点、配置文件、库文件
先看配置这一步。我个人习惯用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
配置完成后,编译安装:
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支持不好,编译时可能需要打补丁。
切换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 删除不必要的库与工具
文件系统裁剪,最核心的原则就一句话:用不到的,统统删掉。但怎么知道哪些是用不到的?我有一套标准流程:
- 分析依赖:用
readelf -d或ldd查看每个二进制文件的动态库依赖 - 按需拷贝:只拷贝被实际引用的库文件
- 剔除符号表:用
strip去掉调试信息 - 清理文档和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网络服务和几个监控脚本,稳得很。