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 等位置。
嵌入式系统常见的挂载流程如下:
- 内核启动,挂载 initramfs 或由 bootloader 传入的 rootfs。
- 执行
/sbin/init或/etc/init.d/rcS。 - 根据
/etc/fstab或启动脚本挂载 proc、sysfs、tmpfs、数据分区。 - 应用启动,按需访问各挂载点下的文件。
# 查看当前挂载情况
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 抽象 + 具体实现 + 挂载拼接”。掌握 inode 与 dentry 的分工、挂载点的作用以及常见文件系统的适用场景,才能在嵌入式项目中正确设计 rootfs 布局、排查文件读写问题,并为后续制作 BusyBox rootfs 打下基础。