1. VFS概述
虚拟文件系统(VFS,Virtual File System)是 Linux 内核中最具抽象力的软件层之一。它位于用户空间与具体存储介质之间,为所有文件系统操作提供统一的接口规范。理解 VFS 的定位、设计目标及其与具体文件系统的协作关系,是掌握整个内核存储栈的起点。
1.1 VFS 在 Linux 内核中的位置
从内核整体分层架构来看,VFS 处于系统调用层与具体文件系统实现之间的中间层。下图展示了 VFS 在内核存储栈中的精确位置:
关键认知:VFS 层是内核中唯一一个“不直接操作硬件”却管理所有文件访问的抽象层。它通过定义一套标准的数据结构和函数指针接口,让上层的系统调用与下层的具体文件系统实现完全解耦。
1.2 VFS 的设计目标与核心作用
VFS 的设计目标可以归纳为以下四个核心维度:
| 设计目标 | 具体含义 | 实现手段 |
|---|---|---|
| 统一抽象 | 对所有文件系统提供一致的编程接口 | 定义通用文件模型(超级块、inode、dentry、file) |
| 多文件系统共存 | 同时挂载 ext4、XFS、NFS、procfs 等不同文件系统 | 通过 file_system_type 注册表管理 |
| 命名空间隔离 | 每个进程可拥有独立的文件系统视图 | mount 命名空间 + 根目录切换 |
| 性能与可扩展性 | 最小化抽象层开销,支持新文件系统快速接入 | inode 缓存、dentry 缓存、RCU 锁 |
VFS 的核心作用体现在三个层面:
- 接口标准化:用户空间通过
open()、read()、write()、close()等 POSIX 系统调用操作文件,VFS 将这些调用转化为对具体文件系统回调函数的调用。 - 对象模型化:VFS 定义了四种核心对象——
super_block(超级块)、inode(索引节点)、dentry(目录项)、file(文件对象),每种对象都包含一组操作函数指针。 - 路径解析:VFS 负责将用户提供的路径字符串(如
/home/user/doc.txt)逐级解析为对应的 dentry 和 inode,这是文件系统访问的必经之路。
1.3 VFS 与具体文件系统的关系
VFS 与具体文件系统之间是“契约式”的协作关系。VFS 定义接口规范,具体文件系统负责实现这些接口。下图展示了这种关系的核心结构:
VFS 与具体文件系统的协作流程可以概括为以下步骤:
- 注册阶段:文件系统在初始化时通过
register_filesystem()将自身注册到 VFS 的全局链表file_systems中。 - 挂载阶段:用户执行
mount命令时,VFS 根据文件系统类型名查找注册表,调用该文件系统的mount()回调,创建super_block对象。 - 访问阶段:进程发起
open()系统调用,VFS 进行路径解析,逐级查找 dentry 和 inode,最终调用具体文件系统的lookup()和open()回调。 - 读写阶段:
read()/write()调用经过 VFS 的file_operations分发,进入具体文件系统的read_iter()/write_iter()实现。
设计精髓:VFS 不关心具体文件系统如何在磁盘上组织数据块、如何管理空闲空间、如何实现日志。它只要求具体文件系统提供一组约定的回调函数,并在内核对象中填充对应的函数指针。这种“面向接口编程”的思想使得 Linux 可以同时支持数十种文件系统,且新增一种文件系统时不需要修改 VFS 核心代码。
1.4 VFS 的四种核心对象概览
VFS 的通用文件模型建立在四种核心内核对象之上。下表总结了每种对象的职责、生命周期和关键操作:
| 对象 | 内核结构 | 职责 | 生命周期 | 关键操作 |
|---|---|---|---|---|
| 超级块 | struct super_block |
管理整个文件系统的元数据(块大小、挂载选项、根 inode) | 从挂载到卸载 | alloc_inode、destroy_inode、sync_fs |
| 索引节点 | struct inode |
表示一个文件或目录的元数据(权限、大小、时间戳、数据块指针) | 从首次访问到内存回收 | lookup、create、unlink、setattr |
| 目录项 | struct dentry |
路径名与 inode 之间的映射关系,维护目录树结构 | 从路径解析到 dentry 缓存回收 | d_revalidate、d_delete、d_compare |
| 文件对象 | struct file |
表示一个打开的文件实例(当前读写位置、访问模式、引用计数) | 从 open() 到 close() | read_iter、write_iter、mmap、poll |
易混淆点:inode 与 file 的区别。inode 代表文件本身的元数据,与路径无关,多个硬链接共享同一个 inode;file 代表进程打开文件时的上下文,每次
open() 创建一个新的 file 对象,拥有独立的 f_pos(当前读写位置)。同一个 inode 可以被多个 file 对象同时引用。
1.5 VFS 的接口契约:操作函数表
VFS 通过四个操作函数表实现与具体文件系统的解耦。每个操作表本质上是一个函数指针结构体:
// 超级块操作:管理整个文件系统
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *inode);
int (*write_inode)(struct inode *inode, struct writeback_control *wbc);
void (*evict_inode)(struct inode *inode);
int (*sync_fs)(struct super_block *sb, int wait);
};
// inode 操作:管理单个文件的元数据和目录内容
struct inode_operations {
struct dentry *(*lookup)(struct inode *dir, struct dentry *dentry,
unsigned int flags);
int (*create)(struct inode *dir, struct dentry *dentry,
umode_t mode, bool excl);
int (*unlink)(struct inode *dir, struct dentry *dentry);
int (*mkdir)(struct inode *dir, struct dentry *dentry, umode_t mode);
int (*setattr)(struct dentry *dentry, struct iattr *attr);
};
// 文件操作:管理打开文件实例的读写行为
struct file_operations {
loff_t (*llseek)(struct file *file, loff_t offset, int whence);
ssize_t (*read_iter)(struct kiocb *iocb, struct iov_iter *iter);
ssize_t (*write_iter)(struct kiocb *iocb, struct iov_iter *iter);
int (*mmap)(struct file *file, struct vm_area_struct *vma);
__poll_t (*poll)(struct file *file, struct poll_table_struct *wait);
};
每个具体文件系统在初始化时,会将自己的实现函数填入这些结构体,并将指针赋值给 VFS 对象的对应字段。例如,ext4 在创建 inode 时会设置:
// ext4 文件系统的 inode 操作表赋值(简化)
static const struct inode_operations ext4_dir_inode_operations = {
.lookup = ext4_lookup,
.create = ext4_create,
.unlink = ext4_unlink,
.mkdir = ext4_mkdir,
.setattr = ext4_setattr,
};
// 在 ext4_iget() 中赋值给 inode->i_op
inode->i_op = &ext4_dir_inode_operations;
学习提示:后续章节将深入分析每种操作表的每个回调函数在内核中的实现细节。当前阶段,你只需要记住:VFS 的接口契约就是这四个操作函数表,具体文件系统通过填充这些表来“接入”VFS 框架。
1.6 本章小结
- VFS 位于系统调用层与具体文件系统之间,是内核存储栈的核心抽象层。
- VFS 的四大设计目标:统一抽象、多文件系统共存、命名空间隔离、性能与可扩展性。
- VFS 与具体文件系统通过“操作函数表”建立契约关系,VFS 定义接口,具体文件系统提供实现。
- 四种核心对象(super_block、inode、dentry、file)构成了 VFS 通用文件模型的基础。
- 理解 VFS 的定位和设计哲学,是后续深入学习路径解析、缓存管理、并发控制等高级主题的前提。