3、构建根文件系统:使用Buildroot构建最小根文件系统、添加自定义应用
好,咱们接着往下走。上一节我们把内核搞定了,现在该聊聊根文件系统了。说白了,根文件系统就是你的嵌入式设备启动后,能看到的那个“C盘”。没有它,内核启动到一半就卡住了,啥也干不了。
我个人习惯用Buildroot来干这个活。为什么?因为它够轻量,够快,而且配置起来非常直观。你想想看,Yocto虽然功能强大,但光下载源码就得半天,对于量产固件打包来说,Buildroot的“最小化”理念更贴合我们的需求。
3.1 为什么选Buildroot?
我在项目中遇到过好几次这样的场景:客户要求固件体积必须控制在8MB以内,而且启动时间不能超过3秒。用Buildroot,我可以精确控制每一个包的去留。它不像发行版那样给你塞一堆用不上的驱动和库。
Buildroot的核心思想就一句话:你需要什么,就编译什么。它不像Yocto那样搞一套复杂的层(Layer)机制,而是通过一个menuconfig界面,让你像点菜一样勾选需要的组件。
关键优势:
- 编译速度快:全量编译一个最小系统,我实测过,i7处理器大概5-8分钟
- 体积小:最小根文件系统可以做到2MB以内
- 配置简单:一个.config文件搞定所有
- 交叉编译链自带:不用你手动折腾arm-linux-gnueabihf-gcc
3.2 下载与配置Buildroot
嗯,这里要注意。Buildroot的版本选择有讲究。我建议用长期支持版(LTS),比如2023.02.x或2024.02.x。太新的版本可能会有一些实验性的bug,影响量产稳定性。
# 下载Buildroot LTS版本
wget https://buildroot.org/downloads/buildroot-2024.02.tar.gz
tar -xzf buildroot-2024.02.tar.gz
cd buildroot-2024.02/
# 配置目标平台
make menuconfig
进入menuconfig界面后,你需要配置以下几项。我直接给你列个清单,照着勾就行:
| 配置项 | 路径 | 推荐值 |
|---|---|---|
| Target Architecture | Target options | ARM (little endian) |
| Target Architecture Variant | Target options | cortex-a7 / cortex-a9(根据你的芯片) |
| Target Binary Format | Target options | ELF |
| Enable MMU | Target options | 勾选 |
| Toolchain type | Toolchain | Buildroot internal toolchain |
| Kernel headers | Toolchain | Same as kernel being built |
| Root filesystem type | Filesystem images | ext4 / squashfs / cpio(看需求) |
我的小技巧:量产固件我一般选squashfs。它是只读压缩文件系统,体积小,而且防篡改。配合overlayfs做数据分区,既安全又灵活。
3.3 构建最小根文件系统
配置完成后,直接运行make。Buildroot会自动下载源码、打补丁、交叉编译,最后生成根文件系统镜像。
# 开始编译(-j后面跟CPU核心数,我一般用4)
make -j4
# 编译完成后,输出文件在 output/images/ 目录下
ls output/images/
# 你会看到:rootfs.ext4 rootfs.tar zImage ...
我第一次用Buildroot时,犯过一个低级错误——忘了配置设备节点。结果系统启动后,/dev目录是空的,连console都没有。嗯,这里要提醒你:
避坑指南:如果你用的是devtmpfs(内核自动管理设备节点),记得在内核配置中勾选 CONFIG_DEVTMPFS=y。否则你的根文件系统里必须手动创建 /dev/console 和 /dev/null。
最小根文件系统包含什么?说白了就三样东西:
- BusyBox:提供ls、cp、sh等基本命令
- glibc或uClibc:C运行库,所有程序都依赖它
- init程序:通常是BusyBox自带的init,或者你自己写的
我个人习惯把根文件系统做成cpio格式,直接跟内核打包在一起。这样启动时内核自解压,一步到位,省去了挂载文件系统的步骤。
# 生成cpio格式的根文件系统
make menuconfig
# Filesystem images → cpio the root filesystem → 勾选
make -j4
# 然后你可以把内核和根文件系统合并
cat output/images/zImage output/images/rootfs.cpio > my_firmware.bin
3.4 添加自定义应用
好了,最小系统跑起来了。但你的产品肯定有自己的业务逻辑,对吧?比如一个温控器,需要读取传感器、控制继电器、上报数据到云端。这些就是自定义应用。
Buildroot添加自定义应用有两种方式,我分别说一下:
方式一:BR2_EXTERNAL机制(推荐)
这种方式不会污染Buildroot源码树,适合团队协作和版本管理。
# 1. 创建外部目录结构
mkdir -p my_app/package/my_app/
mkdir -p my_app/board/my_company/my_board/
# 2. 创建包的makefile
cat > my_app/package/my_app/my_app.mk << 'EOF'
MY_APP_VERSION = 1.0
MY_APP_SITE = $(BR2_EXTERNAL_MY_APP_PATH)/package/my_app/src
MY_APP_SITE_METHOD = local
define MY_APP_BUILD_CMDS
$(MAKE) CC=$(TARGET_CC) -C $(@D)
endef
define MY_APP_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 $(@D)/my_app $(TARGET_DIR)/usr/bin/my_app
endef
$(eval $(generic-package))
EOF
# 3. 创建Config.in
cat > my_app/package/my_app/Config.in << 'EOF'
config BR2_PACKAGE_MY_APP
bool "my_app"
help
My custom application for temperature control.
EOF
# 4. 创建源码目录
mkdir -p my_app/package/my_app/src/
# 把你的main.c、Makefile放进去
# 5. 使用外部目录配置Buildroot
make BR2_EXTERNAL=../my_app menuconfig
# 在Target packages → Custom packages → 勾选 my_app
为什么推荐这种方式?我在一个量产项目中,同时维护了3个不同客户的产品。每个客户的自定义应用不同,但底层系统一样。用BR2_EXTERNAL,我只需要切换不同的外部目录,就能生成不同的固件。爽不爽?
方式二:直接覆盖(快速原型)
如果你只是做个原型验证,不想搞那么复杂,可以直接把编译好的二进制文件放到Buildroot的overlay目录里。
# 1. 创建overlay目录
mkdir -p board/my_company/my_board/rootfs_overlay/
# 2. 把你的应用放进去
cp my_app board/my_company/my_board/rootfs_overlay/usr/bin/
# 3. 在Buildroot中配置overlay路径
make menuconfig
# System configuration → Root filesystem overlay directories
# 填入:board/my_company/my_board/rootfs_overlay
这种方式简单粗暴,但有个坑——它不会帮你处理依赖库。如果你的应用是动态链接的,记得把.so文件也拷进去。我曾经因为这个,在客户现场调试了一下午,最后发现是libpthread.so没拷全。
3.5 启动脚本与初始化
应用放进去了,怎么让它开机自启?Buildroot默认使用BusyBox的init系统,配置文件是/etc/inittab。
# 在rootfs_overlay中创建inittab
cat > board/my_company/my_board/rootfs_overlay/etc/inittab << 'EOF'
::sysinit:/etc/init.d/rcS
::respawn:/sbin/getty -L ttyS0 115200 vt100
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
EOF
# 创建rcS启动脚本
cat > board/my_company/my_board/rootfs_overlay/etc/init.d/rcS << 'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
# 启动你的自定义应用
/usr/bin/my_app -d # -d表示daemon模式
EOF
chmod +x board/my_company/my_board/rootfs_overlay/etc/init.d/rcS
我的经验:量产固件里,我习惯把应用做成守护进程(daemon)。用fork()创建子进程,父进程退出,子进程调用setsid()脱离终端控制。这样即使你拔掉串口线,应用也不会挂掉。
3.6 验证与调试
编译完成后,用QEMU或者直接烧录到开发板上验证。我一般先跑QEMU,快速确认根文件系统能正常挂载、应用能启动。
# 用QEMU启动验证
qemu-system-arm -M virt -kernel output/images/zImage \
-initrd output/images/rootfs.cpio \
-append "console=ttyAMA0 root=/dev/ram0" \
-nographic
# 进入系统后,检查你的应用
# ls /usr/bin/my_app
# /usr/bin/my_app -v
# 应该输出版本号
如果启动卡住了,别慌。我教你一个排查思路:
- 先看内核启动日志,有没有“Kernel panic - not syncing”
- 再看根文件系统挂载,有没有“VFS: Cannot open root device”
- 最后看init进程,有没有“Failed to execute /init”
嗯,大部分问题都出在第三步——要么init脚本写错了,要么BusyBox没编译进去。
好了,这一节的内容就到这。Buildroot构建根文件系统,说白了就是三步:配置、编译、添加应用。你只要把最小系统跑通了,后面加功能就是往里面塞包的事。下一节我们聊聊固件签名,那才是量产安全的关键环节。