3. 文件系统基础

在嵌入式 Linux 中,文件系统并不是“一个盘对应一棵目录树”这么简单。内核需要同时支持 ext4、YAFFS2、UBIFS、squashfs、tmpfs、proc、sysfs 等多种实现,还要让用户始终看到统一的 / 目录树。支撑这一切的核心机制,就是 VFS(Virtual File System,虚拟文件系统)。理解 VFS 及其四大对象——超级块、inode、dentry、file,是读懂挂载、路径查找和根文件系统启动过程的前提。

一、Linux 文件系统层次:从用户到存储介质

Linux 文件系统在纵向上分为四层,每一层职责清晰:

  • 用户层:应用程序通过 open()、read()、write()、stat() 等系统调用访问文件,只认路径和文件描述符。
  • VFS 层:提供统一的抽象接口,维护目录树、路径解析、权限检查、缓存(dentry cache、page cache)。
  • 具体文件系统层:ext4、F2FS、UBIFS、squashfs、NFS 等各自实现 VFS 规定的操作集(super_operations、inode_operations、file_operations)。
  • 块设备/MTD 层:通过块设备驱动、MTD 子系统或网络与真实存储介质交互。

这种分层带来的好处是:应用无需关心底层是 eMMC、NAND 还是网络存储;更换文件系统类型时,上层代码几乎不用修改。

二、VFS 四大对象与 inode、dentry

VFS 用四个核心对象描述“一个被打开的文件”:

对象内核结构含义生命周期
超级块struct super_block描述一个已挂载的文件系统实例挂载到卸载
索引节点struct inode描述一个文件/目录的元数据与数据位置被引用期间缓存
目录项struct dentry描述路径中的一个名字与 inode 的映射dentry cache 缓存
文件对象struct file描述一个进程打开的文件实例open 到 close

inode 是文件的“身份证”:它保存文件类型、权限、大小、时间戳、数据块指针,但不包含文件名。文件名保存在目录项中。因此同一个 inode 可以有多个名字(硬链接),而删除一个名字并不一定释放 inode。

dentry 是路径解析的缓存单元。例如访问 /etc/init.d/rcS 时,内核会依次查找 /、etc、init.d、rcS 对应的 dentry。命中 dentry cache 时无需再读磁盘目录,这对嵌入式系统启动速度影响很大。

关键关系:dentry → inode → 数据块。路径查找先由 dentry 找到 inode,再由 inode 找到文件数据;struct file 则把进程的 fd 与某个 dentry/inode 绑定起来。

三、挂载点与挂载机制

Linux 的目录树是一棵“拼接树”。每个文件系统先挂载到某个目录,这个目录就是挂载点。挂载后,原目录下的内容被隐藏,访问该路径会进入新文件系统。根文件系统挂载在 /,其他分区可挂到 /home、/mnt/data、/proc 等位置。

嵌入式系统常见的挂载流程如下:

  1. 内核启动,挂载 initramfs 或由 bootloader 传入的 rootfs。
  2. 执行 /sbin/init 或 /etc/init.d/rcS。
  3. 根据 /etc/fstab 或启动脚本挂载 proc、sysfs、tmpfs、数据分区。
  4. 应用启动,按需访问各挂载点下的文件。
# 查看当前挂载情况
mount | grep -E "rootfs|proc|sys|tmpfs"

# 典型嵌入式挂载命令
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
mount -t ubifs ubi0:rootfs /mnt/data

# 查看 inode 与挂载点
df -i
ls -li /etc/passwd

挂载点本身也是一个目录,它有自己的 inode。挂载时,VFS 会把挂载点 dentry 与新文件系统的根 dentry 连接起来,形成跨文件系统的路径跳转。

四、常见文件系统类型与选型

嵌入式场景中,文件系统类型的选择取决于存储介质、读写需求和可靠性要求:

  • ext4:eMMC/SD 卡常用,日志型,成熟稳定,适合可读写根文件系统。
  • squashfs:只读压缩文件系统,常用于固件 rootfs,节省空间且防篡改。
  • UBIFS:面向 NAND Flash,配合 UBI 层做磨损均衡和坏块管理。
  • JFFS2/YAFFS2:早期 NAND/NOR 文件系统,现已逐步被 UBIFS 取代。
  • tmpfs:内存文件系统,用于 /tmp、/run,掉电即失。
  • proc/sysfs/devtmpfs:伪文件系统,向用户暴露内核状态、设备和参数。
  • NFS:网络文件系统,常用于开发阶段挂载主机 rootfs。

选型口诀:只读固件用 squashfs,可写数据用 ext4/UBIFS,临时数据用 tmpfs,内核接口用 proc/sysfs,开发调试用 NFS。

五、案例:一次“文件消失”的排障复盘

某嵌入式设备升级后,应用写入 /data/config.json 重启即丢失。排查发现:/data 目录并未挂载独立分区,而是位于只读 squashfs 根文件系统之上;应用写入实际落在内存中的 overlay 或直接失败。修复方式是在启动脚本中增加:

mkdir -p /data
mount -t ubifs ubi0:data /data
# 或使用 tmpfs 作为临时可写层
mount -t tmpfs tmpfs /data

复盘要点:先确认路径属于哪个挂载点,再确认该文件系统是否可写、是否持久化。 使用 df -h /data、mount | grep data、findmnt /data 可快速定位。

六、常见误区

  • 把 inode 当文件名:inode 不含文件名,硬链接共享 inode,软链接有独立 inode。
  • 认为挂载点目录必须为空:非空目录也可挂载,但原内容会被隐藏,卸载后恢复。
  • 忽略 dentry cache 的负缓存:查找不存在的路径也会缓存,大量随机路径可能占用内存。
  • 在只读 rootfs 上直接写配置:应使用独立可写分区或 overlayfs。
  • 混淆 VFS 与具体文件系统:VFS 是接口层,不负责实际块分配和磨损均衡。
Linux 文件系统层次与 VFS 核心对象 用户层:应用程序 / Shell open() · read() · write() · stat() · mount() · 路径 /etc/init.d/rcS VFS 虚拟文件系统层 统一接口 · 路径解析 · 权限检查 · dentry cache · page cache super_block 已挂载文件系统实例 inode 元数据 + 数据块位置 dentry 路径名 → inode 映射 struct file 进程打开的文件实例 具体文件系统层 ext4 squashfs UBIFS tmpfs proc/sysfs NFS 块设备 / MTD / 网络层 eMMC · SD · NAND Flash · NOR Flash · 网络存储 挂载点示例:/ → rootfs /proc → proc /sys → sysfs /data → ubifs /tmp → tmpfs

总结来看,Linux 文件系统的本质是“VFS 抽象 + 具体实现 + 挂载拼接”。掌握 inode 与 dentry 的分工、挂载点的作用以及常见文件系统的适用场景,才能在嵌入式项目中正确设计 rootfs 布局、排查文件读写问题,并为后续制作 BusyBox rootfs 打下基础。