4. Buildroot实战:从零构建一个最小根文件系统

说实话,很多工程师第一次接触嵌入式Linux时,都被复杂的根文件系统吓住了。我当年也一样,对着几百兆的rootfs.tar.gz发呆——这玩意儿到底哪些是必须的?

Buildroot最让我喜欢的一点就是:它让你清清楚楚地看到,一个最小根文件系统到底长什么样。今天我们就动手干一票,从零开始,构建一个能跑起来的最小系统。

本章目标:用Buildroot生成一个仅包含BusyBox、内核模块和必要glibc库的根文件系统,大小控制在8MB以内,能在QEMU或真实硬件上启动到shell。

4.1 准备工作:获取Buildroot并配置目标

先拉代码。我个人习惯用长期支持版本,稳定,坑少。

# 下载Buildroot 2023.02 LTS
wget https://buildroot.org/downloads/buildroot-2023.02.tar.gz
tar xf buildroot-2023.02.tar.gz
cd buildroot-2023.02

然后执行配置界面。这里我选的是ARM Cortex-A9,因为手头有块Zynq开发板,方便验证。

make menuconfig

关键配置项如下表。你如果用的是其他架构,照着改就行。

配置项 推荐值 说明
Target Architecture ARM (little endian) 根据你的硬件选
Target Architecture Variant cortex-a9 我项目里用过A9和A53
Target Binary Format ELF 默认即可
Target ABI EABIhf 硬浮点,性能更好
Toolchain type Buildroot internal 省事,自动下载
Root filesystem type ext4 / cpio ext4适合SD卡,cpio适合initramfs

小技巧:第一次编译建议用internal toolchain,Buildroot会自动下载匹配的gcc、binutils和glibc。我早期图省事用外部工具链,结果ABI不匹配,折腾了两天。

4.2 核心配置:砍掉一切不需要的

Buildroot默认配置会带一堆包。我们要做最小系统,就得狠心砍。

进入 Target packages 菜单,把下面这些统统关掉:

  • Networking applications → 全关(除非你需要网络)
  • Shell and utilities → 只保留 busybox
  • Development tools → 全关(目标板上不需要gcc)
  • Text editors and viewers → 全关
  • System tools → 全关

然后回到主菜单,进入 Filesystem images,只勾选你需要的格式。我一般选 ext4 和 cpio 两种。

注意:关掉 Development tools 后,/usr/include 和 /usr/lib/pkgconfig 就不会被安装。这能省下好几MB空间。但如果你后续要编译内核模块,记得保留 kernel headers。

4.3 BusyBox配置:麻雀虽小五脏俱全

BusyBox是整个根文件系统的灵魂。Buildroot会自动编译它,但默认配置会启用很多我们用不到的命令。

执行以下命令进入BusyBox配置:

make busybox-menuconfig

我个人习惯只保留这些核心命令:

  • Coreutils: cat, chmod, cp, dd, df, ln, ls, mkdir, mv, rm, rmdir, touch
  • Shell: ash (默认shell,够用)
  • Init: init (必须)
  • Login: getty, login, passwd (可选,单用户系统可以关)
  • Editors: vi (建议保留,调试时有用)
  • Misc: mount, umount, reboot, poweroff

其他像 awk、sed、grep 这些,如果你不做脚本解析,完全可以关掉。我曾经在一个存储项目里把BusyBox裁剪到只剩28个命令,根文件系统总共才4.2MB。

4.4 内核模块与设备驱动

最小系统要不要内核模块?看情况。如果你用initramfs启动,建议把必要的驱动编译进内核,而不是作为模块。因为initramfs加载模块需要 modprobe 和 depmod,这俩又会引入一堆依赖。

在Buildroot中配置内核:

make linux-menuconfig

关键配置:

  • Device Drivers → 只保留你硬件需要的驱动
  • File systems → 勾选 ext4、proc、sysfs、devtmpfs
  • General setup → 勾选 Initial RAM filesystem and RAM disk

经验之谈:我踩过一个坑——忘了勾选 devtmpfs,结果启动后 /dev 下面空空如也,连 /dev/console 都没有。系统卡在 Kernel panic - not syncing: VFS: Unable to mount root fs。排查了整整一个下午。

4.5 开始编译:第一次构建

配置完成后,执行编译:

make -j$(nproc)

第一次编译会下载工具链、内核源码、BusyBox源码等,耗时较长。我建议用 -j4 或 -j8,别贪心开太多线程,否则容易内存溢出。

编译完成后,输出文件在 output/images/ 目录下:

output/images/
├── rootfs.ext4          # ext4格式根文件系统
├── rootfs.cpio          # cpio格式(用于initramfs)
├── zImage               # 内核镜像
├── .dtb          # 设备树
└── u-boot.bin           # 如果选了U-Boot

用 du -sh 看看大小:

$ du -sh output/images/rootfs.ext4
6.2M    output/images/rootfs.ext4

嗯,6.2MB,符合预期。

4.6 验证:在QEMU中启动

没有硬件?没关系,用QEMU验证。我写了个简单的启动脚本:

#!/bin/bash
# 启动最小根文件系统
qemu-system-arm \
  -M vexpress-a9 \
  -kernel output/images/zImage \
  -dtb output/images/vexpress-v2p-ca9.dtb \
  -drive file=output/images/rootfs.ext4,if=sd,format=raw \
  -append "root=/dev/mmcblk0 console=ttyAMA0" \
  -nographic

启动后,你应该能看到内核启动日志,最后出现 buildroot login: 提示。输入 root(默认无密码),就能进入shell了。

验证清单:

  • cat /proc/cpuinfo → 能看到ARM Cortex-A9
  • mount → 确认proc、sysfs、devtmpfs已挂载
  • ls /bin → 只有BusyBox提供的命令
  • df -h → 根文件系统占用空间

4.7 根文件系统结构解析

启动后,看看这个最小系统长什么样:

# ls -la /
drwxr-xr-x   2 root root    1024 Jan  1 00:00 bin
drwxr-xr-x   2 root root    1024 Jan  1 00:00 dev
drwxr-xr-x   4 root root    1024 Jan  1 00:00 etc
drwxr-xr-x   2 root root    1024 Jan  1 00:00 lib
drwxr-xr-x   2 root root    1024 Jan  1 00:00 proc
drwxr-xr-x   2 root root    1024 Jan  1 00:00 sbin
drwxr-xr-x   2 root root    1024 Jan  1 00:00 sys
drwxr-xr-x   2 root root    1024 Jan  1 00:00 tmp
drwxr-xr-x   5 root root    1024 Jan  1 00:00 usr
drwxr-xr-x   2 root root    1024 Jan  1 00:00 var

每个目录的作用:

目录 内容 最小系统是否必需
/bin BusyBox链接(cat, ls, sh等) 必需
/sbin 系统管理命令(init, mount, reboot) 必需
/lib glibc/uclibc动态库、内核模块 必需
/etc 配置文件(inittab, fstab, passwd) 必需
/dev 设备节点(由devtmpfs自动管理) 必需
/proc 进程信息虚拟文件系统 必需
/sys 内核对象虚拟文件系统 推荐
/tmp 临时文件(通常挂载为tmpfs) 推荐
/usr 用户程序和数据 可选
/var 可变数据(日志、锁文件) 可选

4.8 最小系统的核心逻辑

下面这张图展示了最小根文件系统的启动流程和组件依赖关系。我画这张图时,特意把关键路径标红了。

最小根文件系统启动流程与组件依赖 最小根文件系统启动流程与组件依赖 Bootloader (U-Boot) Linux Kernel + DTB 挂载根文件系统 (ext4/cpio) /sbin/init (BusyBox) /etc/inittab 解析 /bin/sh (ash) 关键依赖组件 • /lib/libc.so.6 (glibc) • /lib/ld-linux.so.3 • /dev/console • /proc (procfs) 常见失败点 • 缺少devtmpfs → 无/dev • inittab语法错误 → 启动卡死 • 库版本不匹配 → exec失败 • 内核未配置ext4 → 挂载失败

从这张图可以看得很清楚:最小系统的核心就是 Bootloader → 内核 → 根文件系统 → init → shell 这条链。任何一个环节断了,系统就起不来。

4.9 避坑指南

我在多个项目里折腾Buildroot,踩过的坑不少。挑几个典型的说说:

坑1:inittab配置错误导致启动卡死

我曾经在 /etc/inittab 里写错了tty设备名,结果init一直尝试打开不存在的串口,系统卡在 init started: BusyBox v1.36.0 就不动了。排查方法:用 console=ttyAMA0 内核参数指定正确串口,或者在inittab里用 tty1 配合 framebuffer。

坑2:动态库链接问题

最小系统里 /lib 目录只有glibc的几个核心库。如果你编译了一个依赖 libpthread.so 或 librt.so 的程序,但没把它们打包进去,运行时会报 error while loading shared libraries。解决办法:用 readelf -d your_binary | grep NEEDED 查看依赖,然后手动拷贝。

调试利器:如果系统启动到一半卡住,可以在内核命令行加 initcall_debug 和 loglevel=8,能看到每个初始化函数的执行情况。我靠这个定位过三次驱动加载顺序的问题。

4.10 扩展:添加一个自定义程序

最小系统跑起来后,你肯定想加点自己的东西。Buildroot支持 BR2_PACKAGE_OVERRIDE_FILE 机制,可以让你在不修改官方包的情况下覆盖源码。

举个例子,我想在根文件系统里放一个 /hello 程序:

  1. 在Buildroot根目录创建 local/hello/ 文件夹
  2. 写一个简单的 hello.c
  3. 创建 local/hello/hello.mk 和 local/hello/Config.in
  4. 在 package/Config.in 里添加 source "local/hello/Config.in"
  5. 重新 make menuconfig 勾选,然后编译

这样你的程序就会被编译并安装到根文件系统的 /usr/bin/hello。我在好几个项目里都用这个方式快速集成自研工具,比写完整的Buildroot包省事多了。

4.11 本章小结

从零构建一个最小根文件系统,其实就三步:配置Buildroot、裁剪不必要的包、编译验证。核心是理解 根文件系统里到底需要什么——一个内核、一个init程序、一个shell、必要的库和配置文件,仅此而已。

你想想看,一个能跑Linux的设备,根文件系统才6MB。这在20年前简直不敢想象。Buildroot把这种极简哲学发挥到了极致,这也是我偏爱它的原因。