3. VFS(虚拟文件系统)基础
VFS(Virtual File System,虚拟文件系统)是 Linux 内核中一个至关重要的抽象层。它为上层应用程序(如 open()、read()、write())提供了一套统一的系统调用接口,而将底层不同物理文件系统(如 ext4、F2FS、FUSE)的具体实现细节隐藏起来。在 Android 系统中,VFS 是连接用户空间与底层存储设备(如 UFS、eMMC)的桥梁,也是文件系统驱动开发者必须掌握的核心知识。
3.1 VFS 概念与架构
VFS 本质上是一个内核软件层,它定义了一组标准的文件系统操作接口(如 struct file_operations、struct inode_operations、struct super_operations)。任何想要被 Linux 支持的文件系统,都必须实现这些接口中的部分或全部函数指针。
下图展示了 VFS 在 Android 系统中的位置及其与各组件的关系:
f2fs 文件系统就是通过优化 VFS 层的缓存策略来提升随机读写性能的。
3.2 四大核心数据结构关系总览
VFS 的核心由四个主要数据结构组成,它们之间的关系可以用“文件路径查找”这一经典场景来串联:
| 数据结构 | 内核定义 | 核心作用 | 生命周期 |
|---|---|---|---|
| super_block | struct super_block |
代表一个已挂载的文件系统实例,管理全局信息(块大小、操作函数表) | 从挂载(mount)开始,到卸载(umount)结束 |
| inode | struct inode |
代表一个文件或目录的元数据(权限、大小、时间戳、数据块指针) | 从文件被创建或首次访问时加载,到 inode 被回收或文件被删除 |
| dentry | struct dentry |
代表路径中的一个分量(目录项),负责路径解析和目录缓存 | 由路径查找创建,可被 dentry cache 缓存,直至被回收 |
| file | struct file |
代表一个打开的文件描述符上下文(当前读写位置、标志、私有数据) | 从 open() 系统调用开始,到 close() 系统调用结束 |
下图清晰地展示了这四个结构体在文件打开过程中的协作关系:
3.3 超级块(super_block)
struct super_block 是 VFS 中最顶层的结构体,它代表一个已经挂载的文件系统实例。每个挂载点(如 /、/data、/system)都对应一个唯一的 super_block 对象。
3.3.1 关键字段
struct super_block {
struct list_head s_list; // 全局超级块链表
dev_t s_dev; // 块设备标识符
unsigned long s_blocksize; // 文件系统块大小 (如 4096)
loff_t s_maxbytes; // 文件最大大小
struct file_system_type *s_type; // 文件系统类型 (如 ext4_fs_type)
struct super_operations *s_op; // 超级块操作函数表
struct dentry *s_root; // 挂载点的根 dentry
void *s_fs_info; // 指向具体文件系统私有数据 (如 ext4_sb_info)
// ... 其他字段
};
3.3.2 生命周期
- 创建: 当用户执行
mount系统调用时,内核通过s_type->mount()回调函数创建super_block实例。 - 活跃: 在文件系统挂载期间,
super_block始终存在于内存中,管理着该文件系统的所有 inode 和 dentry。 - 销毁: 当执行
umount系统调用时,内核调用s_op->kill_sb()来释放super_block及其关联的所有资源。
s_fs_info 是连接 VFS 与具体文件系统私有数据的关键。你通常需要在这里存储文件系统的超级块副本、日志句柄、设备信息等。务必在 kill_sb 中正确释放 s_fs_info 指向的内存,否则会导致内存泄漏。
3.4 inode 结构体
struct inode 是 VFS 中用于描述文件或目录元数据的核心结构体。每个文件或目录在磁盘上都有一个唯一的 inode 编号,在内存中则对应一个 inode 对象。
3.4.1 关键字段
struct inode {
umode_t i_mode; // 文件类型与权限 (S_IFREG, S_IFDIR 等)
uid_t i_uid; // 文件所有者 UID
gid_t i_gid; // 文件所属组 GID
loff_t i_size; // 文件大小 (字节)
struct timespec64 i_atime; // 最后访问时间
struct timespec64 i_mtime; // 最后修改时间
struct timespec64 i_ctime; // 最后状态改变时间
unsigned long i_ino; // inode 编号
struct super_block *i_sb; // 所属超级块
struct address_space *i_mapping; // 地址空间 (用于页缓存)
struct inode_operations *i_op; // inode 操作函数表
const struct file_operations *i_fop; // 默认文件操作函数表
union {
struct pipe_inode_info *i_pipe; // 管道
struct block_device *i_bdev; // 块设备
struct cdev *i_cdev; // 字符设备
};
// ... 其他字段
};
3.4.2 生命周期
| 阶段 | 触发条件 | 关键操作 |
|---|---|---|
| 分配 | 文件创建 (create) 或首次路径查找 |
alloc_inode() 分配内存,s_op->alloc_inode() 可被重载 |
| 读取 | 路径查找时 inode 不在缓存中 | s_op->read_inode() 从磁盘读取元数据 |
| 使用 | 文件被打开或元数据被访问 | 通过 i_count 引用计数保护,i_lock 自旋锁保护字段 |
| 回收 | 引用计数降为 0,且没有 dentry 引用 | destroy_inode() 释放内存,s_op->destroy_inode() 清理私有数据 |
3.5 dentry 结构体
struct dentry(目录项)是 VFS 路径查找的核心组件。它不直接对应磁盘上的数据结构,而是内存中用于加速路径解析的缓存实体。每个 dentry 代表路径中的一个分量,例如路径 /data/app/com.example 会分解为根 dentry、data dentry、app dentry 和 com.example dentry。
3.5.1 关键字段
struct dentry {
struct qstr d_name; // 文件名 (快速比较用)
struct dentry *d_parent; // 父目录 dentry
struct inode *d_inode; // 关联的 inode (可为 NULL)
struct list_head d_child; // 父目录的子 dentry 链表
struct list_head d_subdirs; // 子 dentry 链表 (仅目录)
struct dentry_operations *d_op; // dentry 操作函数表
unsigned int d_flags; // 标志位 (DCACHE_DISCONNECTED 等)
// ... 其他字段
};
3.5.2 dentry 状态与缓存
dentry 在内存中有三种状态,理解这些状态对于调试文件系统问题至关重要:
| 状态 | 说明 | d_inode 值 | 是否在 LRU 链表 |
|---|---|---|---|
| 被使用 (Used) | dentry 正在被一个或多个 file 结构体或进程引用 |
非 NULL | 否 |
| 未被使用 (Unused) | dentry 仍然有效,但当前没有被引用,保留在缓存中 | 非 NULL | 是 (可被回收) |
| 负状态 (Negative) | dentry 对应的文件或目录不存在(用于缓存“不存在”的结果) | NULL | 是 (可被回收) |
/proc/sys/fs/dentry-state 监控 dcache 的使用情况。
3.6 file 结构体
struct file 代表一个进程打开的文件描述符。它与进程的文件描述符表(fd_array)一一对应,是进程与文件系统交互的直接接口。
3.6.1 关键字段
struct file {
struct path f_path; // 包含 dentry 和 mount 的路径信息
struct inode *f_inode; // 缓存 f_path.dentry->d_inode
const struct file_operations *f_op; // 文件操作函数表
loff_t f_pos; // 当前读写位置 (文件偏移量)
unsigned int f_flags; // 打开标志 (O_RDONLY, O_SYNC 等)
fmode_t f_mode; // 打开模式 (FMODE_READ, FMODE_WRITE)
void *private_data; // 私有数据指针 (驱动常用)
struct address_space *f_mapping; // 地址空间映射
// ... 其他字段
};
3.6.2 生命周期
- 分配与初始化: 当进程调用
open()系统调用时,内核通过get_empty_filp()分配struct file实例,然后调用do_dentry_open()进行初始化,最终调用f_op->open()回调。 - 使用: 进程通过
read()、write()、ioctl()等系统调用操作文件,这些调用最终会通过f_op中的函数指针分发到具体文件系统的实现。 - 释放: 当所有进程关闭文件描述符(
close())后,内核调用fput()递减引用计数。当引用计数降为 0 时,调用f_op->release()回调,然后释放struct file内存。
private_data 字段是驱动开发者最常用的工具。在 open() 回调中,你可以分配驱动私有数据结构(如设备上下文、缓冲区),并将其地址赋值给 private_data。在后续的 read()、write()、ioctl() 回调中,通过 filp->private_data 直接访问,无需全局变量。务必在 release() 中释放 private_data 指向的内存。
3.7 四者关系总结与生命周期对比
为了更直观地理解这四个结构体的生命周期差异,下表总结了它们的创建与销毁时机:
| 结构体 | 创建时机 | 销毁时机 | 生命周期跨度 | 典型引用关系 |
|---|---|---|---|---|
| super_block | 文件系统挂载 | 文件系统卸载 | 最长(与挂载点共存亡) | 被所有 inode 和根 dentry 引用 |
| inode | 文件创建或首次访问 | inode 缓存回收或文件删除 | 较长(可被多个 dentry 和 file 共享) | 被 dentry 的 d_inode 和 file 的 f_inode 引用 |
| dentry | 路径查找时创建 | dcache 回收或父 dentry 被释放 | 中等(可被缓存) | 被 file 的 f_path.dentry 引用 |
| file | open() 系统调用 | close() 系统调用 | 最短(与进程打开的文件描述符共存亡) | 引用 dentry 和 inode |
下图以时间轴的形式展示了这四个结构体在典型场景下的生命周期变化:
3.8 驱动开发中的常见陷阱
- 引用计数管理: 在
open()回调中,如果增加了一个 inode 或 dentry 的引用计数,务必在release()中对应减少。否则会导致结构体无法被回收,造成内存泄漏。 - 并发访问:
struct file的f_pos字段在多线程并发读写时可能被竞争。如果驱动需要维护独立的文件位置,建议使用private_data存储每个文件描述符的私有上下文。 - dentry 的负缓存: 当查找一个不存在的文件时,VFS 会创建一个负 dentry(
d_inode == NULL)。如果后续该文件被创建,需要手动使负 dentry 失效,否则路径查找会一直认为文件不存在。 - super_block 的私有数据: 在
fill_super()回调中分配的s_fs_info,必须在kill_sb()中释放。这是驱动开发中最常见的内存泄漏来源之一。
/proc/slabinfo 查看 dentry、inode_cache、filp 等 slab 缓存的使用情况。如果某个缓存的对象数量持续增长而不下降,通常意味着存在引用计数泄漏。使用 trace_printk 或 ftrace 跟踪 dput()、iput()、fput() 的调用栈,可以快速定位泄漏点。