4、rootfs基础概念:文件系统层次结构(FHS)、根文件系统内容、init进程与系统初始化流程

在RK3588嵌入式Linux开发中,rootfs(根文件系统)是系统启动后挂载在根目录"/"上的完整文件系统集合,它不仅是内核启动后用户态程序的载体,更是整个系统能够正常运行的基础。理解rootfs的构成、文件系统层次结构(FHS)以及init进程的启动流程,是进行系统裁剪、定制和排障的前提。本章将从这三个核心维度展开,帮助读者建立对RK3588根文件系统的整体认知。

RK3588 rootfs 核心概念关系图 FHS 目录层次 /bin /sbin /usr /etc /lib /dev /proc /sys /tmp /var /home /root 静态/动态/运行时数据分离 系统/用户/可变数据分层 → 决定目录规划与挂载点 rootfs 核心内容 ① 目录结构(FHS) ② 动态库 /lib *.so* ③ 设备节点 /dev ④ 配置文件 /etc ⑤ 应用程序 /usr/bin ⑥ init 进程及启动脚本 → 最小可运行集合 init 与系统初始化 ① 内核启动 → 挂载rootfs ② 执行 /sbin/init (PID=1) ③ 读取 /etc/inittab ④ 执行 rcS 启动脚本 ⑤ 启动 getty/服务进程 ⑥ 进入运行级别 → 用户态第一个进程 RK3588 系统启动完整链路 BootROM U-Boot SPL U-Boot Kernel rootfs + init 固化ROM DDR初始化 加载内核 设备驱动 用户态服务 ★ 调试入口:串口console → 内核日志 → init日志 → 应用日志

上图展示了RK3588系统从BootROM到rootfs的完整启动链路,以及FHS、rootfs内容、init进程三者之间的逻辑关系。下面我们将逐一深入剖析每个核心概念。

4.1 文件系统层次结构(FHS)

4.1.1 核心原理:FHS的目录分类逻辑

FHS(Filesystem Hierarchy Standard)是Linux系统目录结构的标准规范,它定义了系统运行时各目录的用途和存放内容。对于RK3588嵌入式系统而言,遵循FHS有助于保持系统的一致性和可维护性。FHS将目录划分为四个基本类别:静态/动态、可共享/不可共享。静态数据指系统运行时不需要修改的文件(如/bin下的可执行程序),动态数据指运行中会变化的文件(如/var下的日志)。可共享数据指可以被多个主机共享的文件(如/usr),不可共享数据则指本机特有的文件(如/etc)。

目录类别典型内容RK3588注意事项
/bin静态/不可共享基本命令(ls, cp, sh)必须包含busybox或coreutils
/sbin静态/不可共享系统管理命令(init, mount)init程序必须位于此或/bin
/usr静态/可共享用户程序、库文件可放在独立分区,但需提前挂载
/etc动态/不可共享配置文件、启动脚本inittab、rcS、fstab等关键文件
/lib静态/不可共享动态链接库、内核模块需包含ld-linux-aarch64.so.1
/dev动态/不可共享设备节点可使用devtmpfs自动挂载
/proc动态/虚拟内核运行时信息必须挂载procfs
/sys动态/虚拟设备模型信息必须挂载sysfs
/tmp动态/不可共享临时文件通常使用tmpfs
/var动态/不可共享日志、缓存、锁文件嵌入式系统可精简

4.1.2 判断方法:如何规划RK3588的目录结构

在构建RK3588 rootfs时,首先需要根据实际需求确定目录的最小集合。一个典型的嵌入式最小rootfs至少需要以下目录:/bin、/sbin、/etc、/lib、/dev、/proc、/sys、/tmp。判断目录是否必要的标准是:系统启动到shell提示符出现,以及运行核心服务(如网络、存储)所需的最小文件集合。可以使用find命令检查每个目录的占用空间,使用ldd命令检查动态库依赖。

经验提示:在RK3588开发中,建议将/usr、/var、/home等目录保留在rootfs中(即使为空),以避免某些软件在安装或运行时因目录不存在而报错。同时,/etc目录下的fstab文件应明确列出需要自动挂载的分区。

4.1.3 案例拆解:一个最小RK3588 rootfs的目录清单

假设我们使用Buildroot构建一个最小系统,其生成的rootfs目录结构如下(仅列出关键部分):

/
├── bin/          # busybox 链接
├── sbin/         # init, mount, ifconfig
├── etc/
│   ├── inittab   # init 配置
│   ├── rcS       # 启动脚本
│   ├── fstab     # 挂载表
│   └── passwd    # 用户信息
├── lib/
│   ├── ld-linux-aarch64.so.1
│   ├── libc.so.6
│   └── modules/  # 内核模块
├── dev/          # 设备节点(devtmpfs)
├── proc/         # procfs 挂载点
├── sys/          # sysfs 挂载点
├── tmp/          # tmpfs
├── usr/
│   ├── bin/      # 应用程序
│   └── lib/      # 应用依赖库
└── var/
    ├── log/      # 日志目录
    └── run/      # 运行时PID文件

这个结构约占用15-30MB空间(不含内核模块),足以支撑一个基本的网络服务和串口调试功能。在实际项目中,如果使用QT或GStreamer等大型框架,/usr目录会显著增大。

4.1.4 常见误区与总结

风险提醒:不要将/lib和/usr/lib合并或随意移动。在RK3588的64位系统中,动态链接器的路径是硬编码在可执行文件中的(通过ELF解释器指定),如果/lib/ld-linux-aarch64.so.1缺失或路径错误,所有动态链接的程序都将无法启动,表现为"Exec format error"或"No such file or directory"。

总结:FHS是rootfs设计的蓝图。在RK3588项目中,应遵循"最小化但完整"的原则,确保每个目录都有明确用途,避免将临时文件写入只读分区。

4.2 根文件系统内容详解

4.2.1 核心原理:rootfs的组成要素

一个可用的rootfs不仅仅是目录的集合,它必须包含以下关键内容:init程序(通常是/sbin/init)、shell(如/bin/sh)、基本命令工具(busybox)、动态链接库、设备节点、配置文件以及挂载信息。在RK3588平台上,由于使用ARM64架构,所有可执行文件和库都必须是AArch64格式。

4.2.2 判断方法:验证rootfs的完整性

在将rootfs烧录到RK3588的存储介质(eMMC、SD卡或NVMe)之前,可以使用以下方法在主机上验证其完整性:

  1. chroot测试:在x86主机上使用chroot /path/to/rootfs /bin/sh,但需注意架构不同,需使用qemu-aarch64-static模拟。
  2. 文件类型检查:使用file /path/to/rootfs/bin/busybox确认是"ELF 64-bit LSB executable, ARM aarch64"。
  3. 依赖检查:使用aarch64-linux-gnu-readelf -d /path/to/rootfs/bin/busybox查看NEEDED字段,确认所有依赖库都存在。
  4. 目录权限检查:确保/tmp权限为1777,/dev权限为755。

4.2.3 案例拆解:busybox与glibc的依赖关系

以最常见的busybox为例,它提供了init、sh、ls、cp等200多个命令的简化实现。在RK3588上,我们通常选择静态编译busybox(CONFIG_STATIC=y),这样可以避免动态库依赖,减小rootfs体积。但如果需要运行其他动态链接的应用程序(如Python、Qt),则必须提供完整的glibc或musl库。

# 检查busybox的链接方式
$ file rootfs/bin/busybox
rootfs/bin/busybox: ELF 64-bit LSB executable, ARM aarch64, dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0

# 查看依赖库
$ aarch64-linux-gnu-readelf -d rootfs/bin/busybox | grep NEEDED
 0x0000000000000001 (NEEDED)  Shared library: [libc.so.6]

如果选择动态编译,则libc.so.6和ld-linux-aarch64.so.1必须存在于/lib目录中。在RK3588的Yocto或Buildroot构建中,这些库通常由工具链自动部署。

4.2.4 常见误区与总结

关键要点:rootfs的"最小化"不等于"残缺"。一个健康的rootfs应该能够支持系统启动、基本I/O操作、网络通信和动态库加载。在RK3588项目中,建议保留ldd、mount、ifconfig等调试工具,以便在目标板上进行排障。

总结:rootfs的内容决定了系统的功能边界。在构建时,应优先保证init、shell、libc和基本设备节点的完整性,再根据应用需求添加其他组件。

4.3 init进程与系统初始化流程

4.3.1 核心原理:init的角色与启动顺序

init是Linux内核启动后执行的第一个用户态进程,其PID恒为1。在RK3588系统中,init负责完成以下任务:挂载虚拟文件系统、配置网络接口、启动系统服务、生成控制台登录进程。内核通过rdinit=或init=内核参数指定init程序的路径,默认查找/sbin/init、/etc/init、/bin/init和/bin/sh。

4.3.2 操作流程:从内核到shell的完整初始化序列

RK3588的启动流程可分解为以下步骤:

  1. 内核挂载rootfs:内核根据root=参数找到存储设备,挂载为只读或读写模式。
  2. 执行init:内核启动/sbin/init,此时PID=1。
  3. 读取/etc/inittab:init解析inittab,确定运行级别和需要启动的进程。
  4. 执行rcS脚本:通常/etc/init.d/rcS会被调用,完成文件系统检查、挂载、网络配置等。
  5. 启动getty:在串口或虚拟终端上启动getty,等待用户登录。
  6. 进入运行级别:根据inittab配置,启动相应级别的服务。
# 典型的RK3588 /etc/inittab 内容
::sysinit:/etc/init.d/rcS
ttyFIQ0::askfirst:-/bin/sh
::restart:/sbin/init
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r

4.3.3 案例拆解:RK3588串口控制台初始化排障

背景:某项目使用RK3588定制开发板,串口无任何输出,系统无法登录。初始现象:上电后串口终端完全黑屏,无BootROM信息。关键数据:使用逻辑分析仪测量UART TX引脚,发现无电平跳变。

分析过程:首先排除硬件问题,检查串口电平转换芯片(如MAX3232)供电和引脚连接。确认硬件正常后,将问题定位到软件配置。检查U-Boot环境变量console=ttyFIQ0,1500000n8,发现波特率与终端设置不匹配(终端设置为115200)。

操作步骤:

  1. 在U-Boot命令行中执行setenv console 'ttyFIQ0,115200n8'并保存。
  2. 重新启动,串口输出正常,但init启动后无shell提示。
  3. 检查/etc/inittab,发现ttyFIQ0::askfirst:-/bin/sh中的设备名错误,应为ttyS0或ttyFIQ0(取决于内核配置)。
  4. 修改inittab后,系统正常进入shell。

结果对比:修复后,串口输出完整启动日志,登录正常。风险与代价:波特率不匹配不会损坏硬件,但会浪费大量调试时间。复盘经验:在RK3588开发初期,应首先确认串口参数与硬件设计一致,并保留U-Boot的自动启动延迟(bootdelay=3),以便中断进入命令行。

4.3.4 常见误区与总结

风险提醒:不要将init程序编译为静态链接的busybox时忘记创建/dev/console设备节点。如果/dev/console不存在,内核无法将控制台重定向到init,导致系统启动后无任何输出。在rootfs中,应使用mknod -m 600 /dev/console c 5 1提前创建。

总结:init进程是系统初始化的核心,其配置(inittab)和脚本(rcS)决定了系统的启动行为。在RK3588调试中,掌握init的启动顺序和日志输出位置(dmesg、/var/log/messages)是快速定位问题的关键。

本章核心要点回顾:
  • FHS是rootfs目录规划的蓝图,嵌入式系统应遵循"最小化但完整"原则。
  • rootfs必须包含init、shell、libc、设备节点和配置文件,缺一不可。
  • init进程(PID=1)负责系统初始化,其配置在/etc/inittab,启动脚本为/etc/init.d/rcS。
  • RK3588调试中,串口参数(波特率、设备名)和/dev/console节点是最常见的启动失败原因。