4、根文件系统构建(Rootfs):从零开始打造智能音箱的“家”

根文件系统,说白了就是Linux系统启动后看到的第一个“家”。没有它,内核就算跑起来了,也干不了任何事。我刚开始做BSP时,总觉得内核移植完就万事大吉,结果系统卡在“Kernel panic - not syncing: VFS: Unable to mount root fs”上,折腾了一整天才发现是根文件系统没配好。嗯,这坑我替你们踩过了。

4.1 Busybox构建最小根文件系统

Busybox是什么?它就像一个瑞士军刀,把几百个Linux命令打包成一个可执行文件。你想想看,嵌入式设备存储空间那么金贵,总不能把完整的coreutils都塞进去吧?Busybox就是干这个的。

构建步骤其实不复杂,我习惯这样操作:

# 下载Busybox源码
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xjf busybox-1.36.1.tar.bz2
cd busybox-1.36.1

# 配置,我一般用defconfig再手动调整
make defconfig
make menuconfig

在menuconfig里,有几个关键选项必须注意:

  • 静态编译:选中“Build Busybox as a static binary (no shared libs)”。为什么?因为根文件系统还没建好,动态链接库都找不到,静态编译最省心。
  • 安装路径:设置“Destination path for 'make install'”,比如../rootfs
  • 核心命令:init、sh、mount、ls、cp、mv、cat这些必须保留。我遇到过有人把ls都砍了,调试时连文件都看不到,那叫一个尴尬。

个人经验:我建议第一次构建时,先把所有命令都编译进去,等系统跑起来了再慢慢裁剪。别一开始就追求极致精简,否则出了问题都不知道是哪个命令缺失导致的。

编译安装:

make -j4
make install

安装完成后,你会得到一个_install目录,里面就是最基础的根文件系统骨架。但别高兴太早,这还只是个空壳子,需要手动补充一些关键目录:

cd _install
mkdir -p proc sys dev etc/init.d tmp var/log mnt

4.2 glibc vs musl:C库的选择难题

这个问题我当年纠结了很久。glibc和musl,到底选哪个?

特性 glibc musl
体积 大(约2-5MB) 小(约400KB)
兼容性 极好,几乎所有软件都支持 较好,但部分软件需要打补丁
性能 成熟稳定,但内存占用高 轻量高效,适合资源受限设备
许可证 LGPL MIT

我个人习惯是:智能音箱这种产品,首选musl。为什么?

  • 智能音箱的Flash通常只有64MB或128MB,glibc太占地方了。
  • musl的代码质量很高,我在项目中遇到过musl处理信号和线程时比glibc更干净利落。
  • 不过要注意,如果你要用到某些深度依赖glibc特性的库(比如某些老旧的音频编解码库),那还是乖乖用glibc吧。

避坑指南:我曾经在一个项目里,因为用了musl,结果某个第三方语音识别库在malloc对齐上出了问题,排查了整整两天。最后发现是那个库写死了glibc的内存对齐行为。所以,选musl之前,先确认你的所有第三方库都支持它。

如果你决定用glibc,记得把库文件拷贝到根文件系统里:

# 假设你的交叉编译器在 /opt/gcc-arm-xxx
cp /opt/gcc-arm-xxx/arm-linux-gnueabihf/libc/lib/*.so* rootfs/lib/
cp /opt/gcc-arm-xxx/arm-linux-gnueabihf/libc/usr/lib/*.so* rootfs/usr/lib/

4.3 init进程与inittab配置

内核启动完成后,会启动第一个用户空间进程——init。Busybox的init会读取/etc/inittab文件,决定接下来干什么。

一个典型的inittab长这样:

::sysinit:/etc/init.d/rcS
::askfirst:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r

我来解释一下每一行的含义:

  • ::sysinit:/etc/init.d/rcS:系统初始化时执行rcS脚本。这个脚本里一般放挂载文件系统、设置网络、启动服务等操作。
  • ::askfirst:-/bin/sh:在控制台上启动一个shell,启动前会先询问用户。如果你不想交互,可以用::respawn:-/bin/sh
  • ::ctrlaltdel:/sbin/reboot:按下Ctrl+Alt+Del时重启系统。
  • ::shutdown:/bin/umount -a -r:关机时卸载所有文件系统。

rcS脚本我一般这么写:

#!/bin/sh
# 挂载虚拟文件系统
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp

# 创建设备节点
mdev -s

# 设置主机名
echo "smart-speaker" > /proc/sys/kernel/hostname

# 启动网络(如果有)
ifconfig lo 127.0.0.1 up

注意:rcS脚本必须有可执行权限!我见过有人忘了加chmod +x,结果系统启动后卡在init那里,怎么查都查不出来原因。嗯,别问我怎么知道的。

4.4 NFS挂载调试根文件系统

开发阶段,最痛苦的是什么?每次修改根文件系统都要重新烧录Flash。太慢了!NFS挂载就是解决这个问题的利器。

原理很简单:把根文件系统放在开发主机上,目标板通过网络挂载它。这样你改完文件,重启目标板就能看到效果。

配置步骤:

  1. 主机端(Ubuntu)配置NFS服务器
sudo apt install nfs-kernel-server
sudo mkdir -p /srv/nfs/rootfs
# 把你的rootfs目录拷贝到这里
sudo cp -a /path/to/your/rootfs/* /srv/nfs/rootfs/
# 编辑/etc/exports,添加一行
echo "/srv/nfs/rootfs *(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports
sudo exportfs -a
sudo systemctl restart nfs-kernel-server
  1. 目标板内核配置:确保内核开启了NFS客户端支持:
File systems  --->
  Network File Systems  --->
    <*> NFS client support
    [*] NFS client support for NFS version 3
    [*] NFS client support for NFS version 4
  1. U-Boot启动参数
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.10
setenv nfsroot /srv/nfs/rootfs
setenv bootargs console=ttyS0,115200 root=/dev/nfs nfsroot=${serverip}:${nfsroot},v3,tcp ip=${ipaddr}:${serverip}:192.168.1.1:255.255.255.0::eth0:off
saveenv
boot

个人技巧:我习惯在主机上建一个build_rootfs.sh脚本,每次修改完根文件系统后自动同步到NFS目录。这样连重启目标板都省了,直接sync一下就行。

调试阶段用NFS,等所有功能都验证通过了,再把根文件系统打包成镜像烧录到Flash。这个流程我用了好多年,效率提升不是一星半点。

好了,根文件系统这块就聊到这儿。下一章我们讲讲设备树和驱动框架,那又是另一片天地了。