3. 文件系统构建流程

构建一个可启动的文件系统镜像,并不是简单地“把文件拷进去”。它是一条从源码到镜像的流水线:源码经过配置裁剪、编译、链接,生成可执行程序与库,再按目录布局安装到根文件系统树中,最后打包成镜像。依赖裁剪之所以有效,是因为这条流水线的每个阶段都留下了可介入的“闸门”——在错误的位置裁剪,要么裁不掉,要么把系统裁坏。

核心认知:文件系统镜像的“体积”和“依赖”不是最后打包时才决定的,而是在配置、编译、链接、安装、打包五个阶段被逐层固化的。裁剪必须沿着依赖传播的方向,从上游向下游推进。

一、完整构建流水线的五个阶段

以典型的嵌入式 Linux 根文件系统为例,从源码到镜像通常经历以下阶段:

  1. 源码获取与配置:拉取 BusyBox、glibc/musl、systemd 或 init 脚本等源码,通过 Kconfig、meson、autotools 等配置系统选择功能开关。
  2. 编译:将源码编译为目标架构的 .o 目标文件,此阶段决定哪些功能被真正编入。
  3. 链接:把 .o 与静态库、动态库链接成可执行文件或共享库,此阶段决定符号级依赖。
  4. 安装与布局:执行 make install,把二进制、库、配置、脚本按 FHS 目录结构放入 rootfs 暂存目录。
  5. 镜像生成:用 mkfs、genimage、mkimage 等工具把暂存目录打包成 ext4、squashfs、ubifs 或 cpio 镜像。

下面这张图展示了这五个阶段以及依赖裁剪的介入点位置:

文件系统构建流水线与依赖裁剪介入点 ① 源码与配置 Kconfig / meson .config 功能开关 ② 编译 gcc / clang 生成 .o 目标文件 ③ 链接 ld / lld 符号解析与库绑定 ④ 安装与布局 make install rootfs 目录树 ⑤ 镜像 mkfs genimage 依赖裁剪介入点(沿流水线方向逐层收紧) 配置裁剪 关闭 CONFIG_* 移除功能模块 编译裁剪 -ffunction-sections -fdata-sections 链接裁剪 --gc-sections -Wl,--as-needed 安装裁剪 strip 符号表 删除 .a / 头文件 镜像裁剪 压缩算法选择 去重与稀疏化 依赖传播方向:上游决定下游,裁剪必须从上游开始 功能开关 决定代码是否参与编译 符号引用 决定库与函数是否被链接 文件安装 决定镜像中实际存在的文件 镜像体积 最终结果

二、各阶段的依赖介入点与操作

每个阶段都有对应的裁剪手段,越靠上游,裁剪效果越彻底,但风险也越高。下表汇总了各阶段的介入方式与典型命令:

阶段 依赖介入点 典型操作/参数 裁剪效果 风险
配置 Kconfig 功能开关 CONFIG_FEATURE_*=n 彻底移除功能代码 误关导致启动失败
编译 函数/数据段分离 -ffunction-sections -fdata-sections 为链接裁剪做准备 几乎无风险
链接 无用段回收 -Wl,--gc-sections 移除未引用代码段 可能误删弱符号
链接 按需链接库 -Wl,--as-needed 不链接未使用库 需确认符号来源
安装 符号表剥离 strip --strip-unneeded 减小二进制体积 无法调试
安装 删除开发文件 移除 .a、头文件、pkgconfig 显著减小 rootfs 影响后续编译
镜像 压缩与去重 squashfs + xz、硬链接合并 压缩镜像体积 解压耗时增加

三、案例:BusyBox 根文件系统的裁剪实操

以 BusyBox 构建最小根文件系统为例,展示从配置到镜像的完整裁剪链路:

# 1. 配置阶段:关闭不需要的 applet
make defconfig
# 编辑 .config,关闭 telnetd、httpd 等
sed -i 's/CONFIG_TELNETD=y/CONFIG_TELNETD=n/' .config
sed -i 's/CONFIG_HTTPD=y/CONFIG_HTTPD=n/' .config

# 2. 编译阶段:启用段分离
make CFLAGS="-ffunction-sections -fdata-sections -Os"

# 3. 链接阶段:回收无用段
make LDFLAGS="-Wl,--gc-sections -Wl,--as-needed"

# 4. 安装阶段:剥离符号并删除开发文件
make install
find _install -name "*.a" -delete
find _install -name "*.h" -delete
find _install -type f -exec strip --strip-unneeded {} \;

# 5. 镜像阶段:生成 squashfs
mksquashfs _install rootfs.squashfs -comp xz -noappend

经过上述五步,一个包含基本 shell、init 和常用工具的根文件系统可以从数十 MB 压缩到几 MB。关键在于:配置阶段关掉的 applet 根本不会进入编译,链接阶段回收的段不会出现在二进制中,安装阶段删除的文件不会进入镜像。

四、常见误区与复盘

误区一:只在镜像阶段做裁剪。很多开发者习惯在最后用 rm 删除文件来减小镜像,但被删除的文件可能已经被其他程序依赖,运行时才暴露“No such file or directory”。正确做法是在配置和链接阶段就切断依赖。
误区二:忽略动态库的传递依赖。用 ldd 查看一个二进制时,只看到直接依赖的库,但那些库自身还有依赖。裁剪时必须递归检查,否则会出现“库在但符号缺失”的问题。
复盘建议:每次裁剪后,用 readelf -d 检查动态段,用 nm -u 检查未定义符号,用 find rootfs -type f | xargs file 确认文件类型。建立“裁剪前基线—裁剪操作—裁剪后验证”的闭环,才能既减小体积又不破坏功能。

总结来说,文件系统构建流程是一条依赖逐层固化的流水线。配置决定“编什么”,编译决定“怎么编”,链接决定“连什么”,安装决定“放什么”,镜像决定“压什么”。依赖裁剪的最佳策略是:优先在上游配置阶段做减法,在中游链接阶段做回收,在下游安装阶段做清理,最后在镜像阶段做压缩。沿着这条路径推进,才能在保证系统可启动的前提下,把镜像体积压到最小。