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 挂载流程详解
内核启动后,挂载根文件系统大致分这几步:
- 内核解压并初始化:start_kernel()函数执行,初始化各种子系统
- 挂载根文件系统:根据bootargs中的root参数,找到根文件系统设备
- 执行init进程:挂载成功后,内核查找并执行/sbin/init(或/bin/init)
- init读取inittab:Busybox的init读取/etc/inittab,执行系统初始化
- 启动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的完整构建,再到挂载流程的细节,每一步都有它的门道。下节课我们会讲设备驱动开发,到时候再聊。