4、根文件系统构建:Busybox制作最小根文件系统、Yocto/OpenEmbedded入门、根文件系统挂载流程

说到嵌入式Linux,根文件系统是绕不开的话题。很多初学者觉得内核编译完就万事大吉了,其实不然。你想想看,内核启动后总得有个地方挂载根文件系统吧?没有它,系统连最基本的init进程都跑不起来。我个人习惯把根文件系统比作房子的地基——内核是骨架,根文件系统才是真正让系统活起来的东西。

4.1 Busybox制作最小根文件系统

Busybox是什么?说白了,它就是一个“瑞士军刀”式的工具集。把几百个Linux命令打包成一个二进制文件,体积小得惊人。我在项目中遇到过需要把根文件系统压缩到2MB以内的场景,Busybox几乎是唯一的选择。

4.1.1 编译Busybox

先下载源码,然后配置。我个人习惯用menuconfig,直观。

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

# 配置
make menuconfig

这里有个关键点:一定要选择静态编译。为什么?因为动态链接需要libc库,而你的根文件系统里可能还没有这些库。我曾经在这上面栽过跟头——编译完发现busybox跑不起来,报错说找不到共享库,折腾了半天才发现是链接方式的问题。

关键配置项:

  • Settings → Build static binary (no shared libs) → 选中
  • Settings → Cross compiler prefix → 设置为你的交叉编译工具链前缀
  • Linux System Utilities → mount → 确保选中(挂载文件系统用)
# 编译
make -j4
make install

编译完成后,会在_install目录下生成完整的根文件系统骨架。嗯,这里要注意,这个骨架还缺一些关键目录。

4.1.2 构建根文件系统目录结构

Busybox只提供了命令和库文件,目录结构需要我们自己搭建。我一般这样操作:

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

这些目录的作用:

  • proc:挂载proc文件系统,提供进程信息
  • sys:挂载sysfs,提供设备信息
  • dev:设备节点,至少要有console和null
  • tmp:临时文件存放
  • etc:配置文件,最关键的是inittab

4.1.3 编写inittab

inittab是Busybox的初始化配置文件。我习惯写一个最简单的版本:

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

其中rcS是启动脚本,负责挂载文件系统和初始化设备:

#!/bin/sh
# /etc/init.d/rcS
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
mdev -s

小技巧:mdev是Busybox自带的设备管理工具,相当于简化版的udev。在资源受限的嵌入式设备上,mdev比udev轻量得多。我之前的项目里,用mdev替代udev后,启动时间缩短了将近1秒。

4.1.4 制作文件系统镜像

最后一步,把根文件系统打包成镜像。常用的格式有initramfs、jffs2、squashfs等。这里以initramfs为例:

# 制作cpio归档
find . | cpio -H newc -o | gzip > ../rootfs.cpio.gz

这个rootfs.cpio.gz可以直接作为内核的initramfs使用。在U-Boot中设置:

bootargs=console=ttyS0,115200 root=/dev/ram0 initrd=0x42000000,8M

4.2 Yocto/OpenEmbedded入门

Busybox适合快速原型验证,但如果你要做产品级的系统,Yocto才是正道。说实话,Yocto的学习曲线确实陡峭,我第一次接触时也被它的概念搞得头晕。但用熟了之后,你会发现它真的很强大。

4.2.1 Yocto的核心概念

Yocto的核心是BitBake——一个任务执行引擎。它根据recipe(配方)来构建软件包。每个recipe描述了从哪里下载源码、如何编译、如何安装。

概念 说明
Recipe (.bb) 描述如何构建一个软件包
Layer 一组recipe的集合,用于组织代码
Machine 目标硬件平台的配置
Image 最终生成的根文件系统镜像

4.2.2 搭建Yocto环境

我建议用Poky作为起点,它是Yocto的参考发行版:

# 克隆Poky
git clone git://git.yoctoproject.org/poky
cd poky

# 初始化构建环境
source oe-init-build-env build

# 配置目标平台
# 编辑 conf/local.conf,设置MACHINE变量
MACHINE = "qemuarm"

注意:Yocto第一次构建会下载大量源码包,建议配置本地镜像源。我曾经在客户现场演示时,因为网络问题下载失败,场面一度非常尴尬。从那以后,我每次都会提前准备好本地缓存。

4.2.3 构建最小系统

# 构建core-image-minimal
bitbake core-image-minimal

这个命令会构建一个最小的Linux系统,包含Busybox、glibc、内核等。构建完成后,镜像在tmp/deploy/images/目录下。

Yocto的强大之处在于它的可定制性。你可以通过添加layer来支持不同的硬件平台、添加不同的软件包。比如要支持Qt,只需要添加meta-qt5 layer:

bitbake-layers add-layer ../meta-qt5
bitbake core-image-qt

4.3 根文件系统挂载流程

理解了怎么制作根文件系统,还得知道内核是怎么把它挂载起来的。这个流程我梳理过很多次,每次讲课时都会强调——这是理解嵌入式Linux启动过程的关键。

4.3.1 挂载流程详解

内核启动后,挂载根文件系统大致分这几步:

  1. 内核解压并初始化:start_kernel()函数执行,初始化各种子系统
  2. 挂载根文件系统:根据bootargs中的root参数,找到根文件系统设备
  3. 执行init进程:挂载成功后,内核查找并执行/sbin/init(或/bin/init)
  4. init读取inittab:Busybox的init读取/etc/inittab,执行系统初始化
  5. 启动shell:最后进入用户交互界面

这里有个细节很多人会忽略:内核挂载根文件系统时,如果指定了root=/dev/mmcblk0p2,但对应的驱动还没加载,就会挂载失败。我遇到过这种情况——SD卡驱动编译成模块,但根文件系统在SD卡上,这就成了“先有鸡还是先有蛋”的问题。

解决方案:把关键驱动编译进内核,或者使用initramfs。initramfs是一个临时的根文件系统,内核先挂载它,加载必要的驱动,然后再切换到真正的根文件系统。

4.3.2 initramfs vs 直接挂载

方式 优点 缺点
initramfs 灵活,可以加载驱动后再挂载真正的根文件系统 占用内存,启动稍慢
直接挂载 启动快,内存占用少 需要所有驱动都编译进内核

我个人更倾向于initramfs的方式。虽然启动时间会多几百毫秒,但灵活性大大提升。特别是在产品开发阶段,经常需要调试不同的根文件系统,initramfs可以让你在不烧写Flash的情况下快速测试。

4.3.3 调试技巧

如果根文件系统挂载失败,内核会打印错误信息。常见的错误有:

  • VFS: Cannot open root device:找不到根文件系统设备,检查bootargs中的root参数
  • Kernel panic - not syncing: No init found:找不到init进程,检查根文件系统是否完整
  • Failed to execute /sbin/init:init进程损坏或权限不对

调试建议:在内核启动参数中添加initcall_debug和loglevel=8,可以打印更详细的启动信息。我曾经靠这个参数定位到一个USB驱动初始化顺序的问题,省了整整两天的调试时间。

好了,根文件系统这块的内容就讲到这里。从Busybox的最小系统到Yocto的完整构建,再到挂载流程的细节,每一步都有它的门道。下节课我们会讲设备驱动开发,到时候再聊。