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-A9mount→ 确认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 → 内核 → 根文件系统 → 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 程序:
- 在Buildroot根目录创建
local/hello/文件夹 - 写一个简单的
hello.c - 创建
local/hello/hello.mk和local/hello/Config.in - 在
package/Config.in里添加source "local/hello/Config.in" - 重新
make menuconfig勾选,然后编译
这样你的程序就会被编译并安装到根文件系统的 /usr/bin/hello。我在好几个项目里都用这个方式快速集成自研工具,比写完整的Buildroot包省事多了。
4.11 本章小结
从零构建一个最小根文件系统,其实就三步:配置Buildroot、裁剪不必要的包、编译验证。核心是理解 根文件系统里到底需要什么——一个内核、一个init程序、一个shell、必要的库和配置文件,仅此而已。
你想想看,一个能跑Linux的设备,根文件系统才6MB。这在20年前简直不敢想象。Buildroot把这种极简哲学发挥到了极致,这也是我偏爱它的原因。