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
# 应该输出版本号

如果启动卡住了,别慌。我教你一个排查思路:

  1. 先看内核启动日志,有没有“Kernel panic - not syncing”
  2. 再看根文件系统挂载,有没有“VFS: Cannot open root device”
  3. 最后看init进程,有没有“Failed to execute /init”

嗯,大部分问题都出在第三步——要么init脚本写错了,要么BusyBox没编译进去。

好了,这一节的内容就到这。Buildroot构建根文件系统,说白了就是三步:配置、编译、添加应用。你只要把最小系统跑通了,后面加功能就是往里面塞包的事。下一节我们聊聊固件签名,那才是量产安全的关键环节。