3、rootfs基础概念:文件系统层次结构(FHS)、根文件系统内容、挂载点、init进程
嵌入式Linux系统的启动过程可以简化为三个阶段:引导加载程序(Bootloader)启动内核、内核完成硬件初始化和驱动加载、最后挂载根文件系统并启动用户空间的首个进程。其中,根文件系统(rootfs)是用户空间存在的基石,它决定了系统启动后能够提供哪些功能、如何组织文件、以及如何与内核交互。本章将围绕文件系统层次结构标准(FHS)、根文件系统的典型内容、挂载点的作用以及init进程的启动机制展开,帮助学员建立对rootfs的完整认知框架。
上图展示了rootfs四个核心概念之间的逻辑关系。FHS提供目录组织规范,rootfs是遵循该规范的具体文件集合;挂载点负责将不同存储介质或内核虚拟文件系统接入目录树;init进程则是rootfs上启动的第一个用户进程,负责后续系统初始化。下面逐一深入分析。
3.1 文件系统层次结构标准(FHS)
核心原理
FHS(Filesystem Hierarchy Standard)是由Linux基金会维护的目录结构规范,其目的是让不同发行版和嵌入式系统在文件组织上保持一致性。对于嵌入式Linux开发者而言,遵循FHS意味着:
- 可预测性:系统管理员和应用程序能够预期文件位置,例如可执行文件在/bin或/usr/bin,配置文件在/etc。
- 可维护性:升级、备份、恢复操作可以基于目录语义进行,而不是依赖特定文件路径。
- 跨平台兼容性:基于FHS构建的rootfs可以更容易地移植到不同硬件平台。
嵌入式系统通常不需要完整实现FHS的所有目录,但核心目录必须存在。下表列出了嵌入式Linux rootfs中最常用的FHS目录及其用途:
| 目录 | 用途 | 嵌入式系统是否必需 |
|---|---|---|
| /bin | 基本用户命令(ls, cp, sh等) | 必需 |
| /sbin | 系统管理命令(ifconfig, fdisk等) | 必需 |
| /etc | 配置文件、启动脚本 | 必需 |
| /dev | 设备节点文件 | 必需 |
| /proc | 进程和内核信息虚拟文件系统 | 强烈建议 |
| /sys | 设备模型和驱动信息虚拟文件系统 | 强烈建议 |
| /tmp | 临时文件 | 建议 |
| /var | 可变数据(日志、锁文件) | 视需求 |
| /usr | 用户程序和数据(可合并到/) | 可选 |
| /lib | 共享库和内核模块 | 必需 |
| /mnt | 临时挂载点 | 建议 |
| /home | 用户主目录 | 通常不需要 |
判断方法
在构建rootfs时,判断目录是否必需的标准是:系统启动和基本功能运行是否依赖该目录。例如,如果使用BusyBox提供shell和工具,/bin和/sbin是必需的;如果使用动态链接的glibc,/lib必须包含ld-linux.so和libc.so;如果内核需要挂载proc和sysfs,则/proc和/sys目录必须存在。
案例拆解
以一个典型的基于Buildroot生成的嵌入式rootfs为例,其目录结构通常如下:
/
├── bin -> usr/bin
├── sbin -> usr/sbin
├── etc
├── lib -> usr/lib
├── proc
├── sys
├── tmp
├── usr
│ ├── bin
│ ├── sbin
│ └── lib
├── dev
├── mnt
└── init
这里使用了符号链接将/bin、/sbin、/lib指向/usr下的对应目录,这是现代嵌入式系统常见的做法,目的是减少目录层级,方便将/usr作为只读分区挂载,而将/etc、/var等可变数据放在可写分区。
常见误区
误区1:认为FHS是强制标准,必须完全遵守。实际上嵌入式系统可以根据需求裁剪,例如没有用户概念的设备可以省略/home。
误区2:忽略/proc和/sys目录。如果内核配置了CONFIG_PROC_FS和CONFIG_SYSFS,但rootfs中没有对应挂载点,系统启动后许多工具(如ps、mount)将无法正常工作。
3.2 根文件系统内容
核心原理
根文件系统不仅仅是目录结构,它包含了系统运行所需的全部用户空间文件。从内容角度,rootfs可以划分为以下几类:
- 可执行程序:包括init、shell、系统工具、应用程序等。
- 库文件:动态链接库(.so)和静态库(.a),如libc、libm、libpthread。
- 配置文件:/etc目录下的各种配置,如inittab、fstab、passwd、profile。
- 设备节点:/dev下的字符设备和块设备节点,如console、null、ttyS0。
- 虚拟文件系统挂载点:/proc、/sys、/dev/pts等目录,用于挂载内核提供的虚拟文件系统。
- 启动脚本:/etc/init.d/下的脚本或/etc/rcS.d/下的启动链接。
- 符号链接:如/bin/sh指向/bin/busybox,/lib/ld-linux.so.2指向具体库版本。
操作流程
构建rootfs内容的基本流程如下:
- 选择基础工具集:决定使用BusyBox还是完整GNU工具集。BusyBox体积小(约1MB),适合资源受限的嵌入式系统。
- 准备C库:选择glibc(功能全、体积大)、uClibc-ng(轻量级)或musl libc(轻量且兼容性好)。
- 创建目录骨架:按照FHS创建必要的目录。
- 复制可执行文件和库:使用交叉编译工具链生成二进制,并复制到rootfs对应目录。
- 编写配置文件:包括/etc/inittab、/etc/fstab、/etc/profile等。
- 创建设备节点:使用mknod命令或通过devtmpfs自动创建。
- 添加启动脚本:编写init脚本,完成系统初始化。
案例拆解
以最小rootfs为例,仅包含以下内容即可启动一个可交互的shell:
/
├── bin/busybox # 提供sh、ls、cat等命令
├── lib/libc.so.6 # glibc动态库
├── lib/ld-linux.so.3 # 动态链接器
├── etc/inittab # init配置
├── etc/init.d/rcS # 启动脚本
├── dev/console # 控制台设备节点
├── dev/null # 空设备
├── proc/ # 挂载点
└── sys/ # 挂载点
这个rootfs大小约2-3MB,可以运行在具有2MB Flash和8MB RAM的MCU上。其中busybox以静态链接方式编译,可以进一步减少对libc的依赖。
常见误区
经验提醒:不要忘记创建/dev/console设备节点。如果该节点缺失,内核在启动init时会报错"Kernel panic - not syncing: No init found",因为init无法打开控制台输出错误信息。
3.3 挂载点
核心原理
挂载点(Mount Point)是Linux文件系统中将存储设备或虚拟文件系统接入目录树的目录位置。在rootfs中,挂载点具有特殊意义:
- 根挂载:内核启动时根据启动参数(如root=/dev/mmcblk0p2)挂载根文件系统到/。
- 虚拟文件系统挂载:/proc、/sys、/dev/pts、/tmp等需要在启动时通过mount命令挂载。
- 可移动设备挂载:SD卡、U盘等通过/mnt或/media挂载。
- 分区挂载:将不同分区挂载到/home、/var等目录,实现数据分离。
挂载点的选择直接影响系统启动速度和可靠性。例如,如果/etc位于只读分区,系统启动时无法写入临时文件;如果/tmp位于内存文件系统(tmpfs),则重启后临时文件自动清除。
操作流程
在嵌入式系统中,挂载操作通常在init脚本中完成。典型的挂载流程如下:
- 挂载proc:
mount -t proc proc /proc - 挂载sysfs:
mount -t sysfs sysfs /sys - 挂载devtmpfs:
mount -t devtmpfs devtmpfs /dev - 挂载tmpfs到/tmp:
mount -t tmpfs tmpfs /tmp - 根据/etc/fstab挂载其他分区
案例拆解
一个典型的/etc/fstab文件内容如下:
# device mountpoint fstype options dump pass
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
devtmpfs /dev devtmpfs defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
/dev/mmcblk0p3 /data ext4 defaults 0 2
这里将/data分区挂载为可写数据区,而rootfs本身可以设置为只读挂载,提高系统可靠性。如果rootfs是只读的,/etc/resolv.conf等需要动态更新的文件可以通过符号链接指向/tmp或/data。
常见误区
风险说明:如果/proc或/sys没有挂载,许多系统工具会报错。例如,ps命令依赖/proc,lsusb依赖/sys。更严重的是,如果/dev没有挂载devtmpfs,设备节点将无法自动创建,导致无法访问串口、网卡等设备。
3.4 init进程
核心原理
init是内核启动后执行的第一个用户空间进程,其PID固定为1。init进程的职责包括:
- 系统初始化:执行启动脚本,挂载文件系统,配置网络,启动服务。
- 进程管理:作为所有进程的祖先,负责收养孤儿进程,处理僵尸进程。
- 运行级别管理:传统SysV init通过运行级别(0-6)控制系统状态。
- 信号处理:响应Ctrl+Alt+Del等系统信号。
在嵌入式Linux中,常见的init实现包括:
| init实现 | 特点 | 适用场景 |
|---|---|---|
| BusyBox init | 轻量级,配置简单(/etc/inittab) | 大多数嵌入式系统 |
| System V init | 传统方案,运行级别,脚本复杂 | 较完整的嵌入式Linux |
| systemd | 并行启动,依赖管理,功能强大 | 资源充足的嵌入式设备 |
| 自定义init | 完全控制,最小化 | 极简系统或特殊需求 |
操作流程
以BusyBox init为例,其启动流程如下:
- 内核执行
/init(或/sbin/init)程序。 - init读取
/etc/inittab配置文件。 - 执行
sysinit动作,通常运行/etc/init.d/rcS脚本。 - 根据inittab配置启动getty(终端登录)或直接启动应用程序。
- 进入等待循环,处理子进程退出和信号。
一个典型的/etc/inittab文件内容:
::sysinit:/etc/init.d/rcS
::respawn:/sbin/getty -L ttyS0 115200 vt100
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
案例拆解
假设一个工业控制设备,要求开机后自动运行数据采集程序,同时提供串口调试接口。其inittab可以配置为:
::sysinit:/etc/init.d/rcS
::respawn:/usr/bin/collector
::respawn:/sbin/getty -L ttyS0 115200 vt100
这里collector是数据采集程序,使用respawn动作确保程序崩溃后自动重启。getty提供串口登录功能,方便调试。如果采集程序需要访问网络,可以在rcS脚本中配置网络接口。
常见误区
经验提醒:如果init进程崩溃或退出,内核会抛出"Kernel panic - not syncing: Attempted to kill init!"错误。因此,init进程必须保持运行,不能像普通程序一样退出。使用respawn动作可以确保关键服务在异常退出后自动重启。
总结复盘:本章围绕rootfs的四个核心概念展开。FHS提供了目录组织规范,rootfs是具体实现;挂载点将不同存储介质和虚拟文件系统接入目录树;init进程是系统启动的起点。理解这些概念是构建可靠嵌入式Linux系统的基础。在实际项目中,建议先明确系统需求(功能、存储、内存限制),再选择合适的工具集和init方案,最后通过迭代测试验证rootfs的完整性和稳定性。