3、Rootfs制作基础:Busybox编译与安装、构建最小根文件系统、init进程与inittab配置、系统启动流程

好,咱们今天聊聊Rootfs制作。说实话,很多做驱动的朋友,一提到根文件系统就头疼。我刚开始带项目那会儿,也踩过不少坑。其实说白了,Rootfs就是Linux系统启动后,内核挂载的第一个文件系统。它里面装着init进程、各种库、配置文件,还有你那些应用程序。

没有Rootfs,内核启动到一半就卡住了——它会直接panic,告诉你“VFS: Unable to mount root fs”。嗯,这个错误我见过太多次了。

3.1 Busybox:麻雀虽小,五脏俱全

构建最小根文件系统,首选工具就是Busybox。它把几百个常用的Linux命令打包成一个可执行文件,通过符号链接来区分不同的命令。你想想看,一个完整的ls、cp、mv、cat,在Busybox里可能只占几百KB。

我个人习惯用Busybox做嵌入式系统的Rootfs,原因很简单:省空间、易维护、社区活跃。

3.1.1 下载与配置

先下载源码:

wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xjf busybox-1.36.1.tar.bz2
cd busybox-1.36.1

配置的时候,我建议用默认配置再微调:

make defconfig
make menuconfig

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

配置项 推荐值 说明
Build static binary (no shared libs) 选中 静态编译,避免动态库依赖问题
Cross Compiler prefix arm-linux-gnueabihf- 根据你的目标架构设置
Installation prefix ./_install 安装路径,后面会用到
注意: 如果你用的是动态编译,一定要把对应的动态库(比如libc.so、ld-linux.so)拷贝到Rootfs里。我曾经在项目里忘了拷libc.so,结果所有命令都报“file not found”,排查了半天才发现是库的问题。

3.1.2 编译与安装

配置好之后,直接编译:

make -j$(nproc)
make install

编译完成后,_install目录下就会出现bin、sbin、usr等子目录,以及一个linuxrc文件。这个linuxrc其实就是Busybox的init程序,后面我们会用到它。

3.2 构建最小根文件系统

有了Busybox的安装目录,我们就可以开始组装Rootfs了。我一般会手动创建几个必要的目录:

mkdir -p rootfs/{bin,sbin,etc,dev,proc,sys,usr/{bin,sbin},lib,root,tmp,var/{log,run}}

然后把Busybox的_install内容拷贝进去:

cp -a busybox-1.36.1/_install/* rootfs/

接下来,创建设备节点。这是新手最容易忽略的地方。没有/dev/null、/dev/console这些设备节点,系统启动时会报错。

sudo mknod rootfs/dev/null c 1 3
sudo mknod rootfs/dev/console c 5 1
sudo mknod rootfs/dev/ttyS0 c 4 64  # 串口设备,看你的硬件
小技巧: 如果你用的是udev或mdev,可以在启动时动态创建设备节点。但最小系统里,我建议先手动创建几个关键节点,保证系统能跑起来再说。

3.3 init进程与inittab配置

内核启动完成后,会尝试执行第一个用户空间程序——init。在Busybox里,这个init就是/bin/init(或者linuxrc)。init进程会读取/etc/inittab文件,根据里面的配置来启动各种服务。

一个典型的inittab文件长这样:

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

每一行的格式是:id:runlevels:action:process。对于Busybox来说,id和runlevels通常可以留空。action字段是关键,它决定了进程在什么情况下启动。

action 含义
sysinit 系统初始化时执行,只执行一次
askfirst 在终端上显示“Please press Enter to activate this console”,等待用户按回车后才启动shell
respawn 进程终止后自动重启
once 只启动一次,不重启
ctrlaltdel 按下Ctrl+Alt+Del时执行
shutdown 系统关机时执行

我一般会在/etc/init.d/rcS里写系统初始化脚本,比如挂载文件系统、配置网络、启动服务等:

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
mdev -s  # 动态创建设备节点
echo "System initialized"
重点: rcS脚本必须有可执行权限(chmod +x),否则init进程无法执行它。我见过有人把rcS写成普通文本文件,结果系统启动后啥反应都没有。

3.4 系统启动流程

理解了上面的内容,我们来看看整个启动流程。我画了一张图,帮你理清思路:

嵌入式Linux系统启动流程 阶段1:Bootloader(U-Boot) 加载内核镜像到内存,传递启动参数(bootargs) 阶段2:内核启动 解压内核、初始化硬件、挂载根文件系统 阶段3:init进程启动 内核执行 /sbin/init(Busybox init),读取 /etc/inittab 阶段4:系统初始化 执行 /etc/init.d/rcS,挂载 proc/sysfs/tmpfs,创建设备节点 阶段5:用户空间就绪 启动shell(/bin/sh),等待用户输入命令

整个流程其实就五步:

  1. Bootloader:U-Boot加载内核,设置bootargs(比如root=/dev/mmcblk0p2 console=ttyS0,115200)
  2. 内核启动:内核解压、初始化各子系统、挂载根文件系统
  3. init进程:内核执行第一个用户程序,通常是/sbin/init
  4. 系统初始化:init读取inittab,执行rcS脚本,挂载虚拟文件系统
  5. 用户空间:启动shell,系统就绪
个人经验: 调试启动问题时,我一般会在内核启动参数里加上initcall_debug和loglevel=8,这样能看到内核初始化每个驱动的详细信息。如果卡在某个地方,基本就是那个驱动或者硬件有问题。

嗯,到这里,Rootfs制作的基础知识就讲完了。你可能会问:为什么我的系统启动后没有shell?为什么挂载根文件系统失败?别急,后面我们会专门讲这些问题的排查方法。先把今天的内容消化掉,动手做一遍,比看十遍都管用。

避坑指南: 我曾经在项目里犯过一个低级错误——把inittab文件格式写错了,少了一个冒号。结果init进程直接忽略了这个文件,系统启动后没有任何反应。排查了半天,最后用串口打印才发现是inittab解析失败。所以,写inittab的时候一定要仔细检查格式,每个字段都不能少。

记住一句话:最小Rootfs的核心就是Busybox + inittab + rcS。把这三样搞明白,剩下的就是往里面加东西了。