3、rootfs基础概念:文件系统层次结构(FHS)、根文件系统内容、挂载点、init进程

嵌入式Linux系统的启动过程可以简化为三个阶段:引导加载程序(Bootloader)启动内核、内核完成硬件初始化和驱动加载、最后挂载根文件系统并启动用户空间的首个进程。其中,根文件系统(rootfs)是用户空间存在的基石,它决定了系统启动后能够提供哪些功能、如何组织文件、以及如何与内核交互。本章将围绕文件系统层次结构标准(FHS)、根文件系统的典型内容、挂载点的作用以及init进程的启动机制展开,帮助学员建立对rootfs的完整认知框架。

rootfs 核心概念关系图 根文件系统 (rootfs) FHS 目录标准 目录与文件内容 挂载点管理 init 进程 /bin /sbin /etc /var /dev /proc /sys /tmp BusyBox / glibc / 配置 设备节点 / 符号链接 /mnt /media 可移动设备 /proc /sys 内核虚拟文件 PID 1 进程 启动脚本 / 服务 关键理解: 1. FHS 规定了目录结构,rootfs 是具体实现 2. 挂载点将不同存储介质/虚拟文件系统接入目录树 3. init 是内核启动的第一个用户进程,负责系统初始化

上图展示了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内容的基本流程如下:

  1. 选择基础工具集:决定使用BusyBox还是完整GNU工具集。BusyBox体积小(约1MB),适合资源受限的嵌入式系统。
  2. 准备C库:选择glibc(功能全、体积大)、uClibc-ng(轻量级)或musl libc(轻量且兼容性好)。
  3. 创建目录骨架:按照FHS创建必要的目录。
  4. 复制可执行文件和库:使用交叉编译工具链生成二进制,并复制到rootfs对应目录。
  5. 编写配置文件:包括/etc/inittab、/etc/fstab、/etc/profile等。
  6. 创建设备节点:使用mknod命令或通过devtmpfs自动创建。
  7. 添加启动脚本:编写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脚本中完成。典型的挂载流程如下:

  1. 挂载proc:mount -t proc proc /proc
  2. 挂载sysfs:mount -t sysfs sysfs /sys
  3. 挂载devtmpfs:mount -t devtmpfs devtmpfs /dev
  4. 挂载tmpfs到/tmp:mount -t tmpfs tmpfs /tmp
  5. 根据/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为例,其启动流程如下:

  1. 内核执行/init(或/sbin/init)程序。
  2. init读取/etc/inittab配置文件。
  3. 执行sysinit动作,通常运行/etc/init.d/rcS脚本。
  4. 根据inittab配置启动getty(终端登录)或直接启动应用程序。
  5. 进入等待循环,处理子进程退出和信号。

一个典型的/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的完整性和稳定性。