深入理解虚拟文件系统(VFS)
1. 什么是虚拟文件系统
虚拟文件系统(Virtual File System,简称 VFS)是操作系统内核中的一个抽象软件层。它向上为所有文件相关的系统调用提供统一的接口,向下将请求分派给具体的文件系统实现。用户空间程序只需要使用 open、read、write、close 等标准接口,而不必知道文件实际存储于 ext4、XFS、NFS 还是 tmpfs。对内核开发者来说,VFS 定义了文件系统的通用对象模型和操作接口,一个新的文件系统只要实现这些接口,就可以挂载到系统中正常工作。
2. 为什么需要 VFS
VFS 的存在主要解决以下几个问题:
- 屏蔽底层差异:不同磁盘文件系统的物理组织格式完全不同,网络文件系统的访问方式更不一样。VFS 通过一层抽象把这些差异隔离在具体文件系统模块内部。
- 统一命名空间:Linux 把不同设备、不同分区上的文件系统挂载到同一棵目录树下,用户可以跨文件系统访问文件,而不必关心它们分属哪个挂载点。
- 简化文件系统开发:新文件系统不必修改内核通用代码,只需实现 VFS 规定的超级块操作、inode 操作、file 操作等函数指针集合。
- 支持非块设备文件系统:如 procfs、sysfs、tmpfs、devpts、socket 文件系统,本身没有磁盘上的结构,但通过 VFS 对外表现出统一的文件接口。
3. VFS 的总体架构
从一次文件访问的视角看,调用路径大致如下:用户进程通过系统调用进入内核,内核 VFS 层接收请求,根据路径和挂载信息找到对应文件系统实例,再调用该文件系统注册的函数指针,最后访问存储介质或内存数据。
其中,VFS 本身不是一个真实的文件系统,它既不管理磁盘,也不存储文件内容,而是一组公共数据结构和函数指针的集合。
4. VFS 的四大核心对象
VFS 围绕四个核心对象展开,分别是超级块、索引节点、目录项和文件对象。理解它们是理解 VFS 的关键。
| 对象 | 内核结构体 | 表示内容 |
|---|---|---|
| 超级块 | struct super_block | 一个已挂载文件系统的整体信息,如块大小、根目录、超级块操作集合 |
| 索引节点 | struct inode | 一个文件或目录在内核中的元数据表示,如权限、大小、位置 |
| 目录项 | struct dentry | 路径查询与缓存的最小单位,保存父目录、子节点关系 |
| 文件对象 | struct file | 一次打开操作产生的上下文,如文件偏移、打开模式、操作集合 |
此外,与数据读写密切相关的还有 struct address_space,它负责维护页缓存,把文件偏移映射到内存页。
4.1 超级块 super_block
每当一个文件系统被挂载时,内核会为该挂载实例创建一个 struct super_block。它记录了该文件系统的基本参数,并指向根 dentry 和一组超级块操作函数。一个磁盘分区上的 ext4 文件系统,在同一时刻通常只会挂载一次,但 bebind mount 等特殊方式也可能产生多个挂载点关系。简化结构如下:
struct super_block {
unsigned long s_blocksize;
struct dentry *s_root;
const struct super_operations *s_op;
void *s_fs_info;
struct list_head s_inodes;
};
4.2 索引节点 inode
struct inode 是文件的内核代表。它保存文件模式、大小、所有者等信息,并且包含两套操作接口:一套用于 inode 本身,例如创建、删除、查找目录项;另一套用于文件内容,如 read、write、mmap。简化结构如下:
struct inode {
umode_t i_mode;
uid_t i_uid;
gid_t i_gid;
loff_t i_size;
struct super_block *i_sb;
const struct inode_operations *i_op;
const struct file_operations *i_fop;
};
4.3 目录项 dentry
struct dentry 是路径查找的核心。它通常不是磁盘上的持久化对象,而是内核根据路径名创建的缓存对象。一个 dentry 关联一个 inode,同时保存父 dentry 指针。dentry cache 能够极大加快后续同名路径的解析速度。
struct dentry {
struct inode *d_inode;
struct dentry *d_parent;
const struct dentry_operations *d_op;
};
4.4 文件对象 file
每次调用 open 都会创建一个 struct file,它属于打开的文件描述符,而不是文件本身。f_pos 记录当前读写偏移,f_op 指向具体文件系统的文件操作集合。被 fork 出来的子进程可能共享同一个 file 对象,从而实现文件偏移的共享。
struct file {
struct path f_path;
loff_t f_pos;
fmode_t f_mode;
const struct file_operations *f_op;
};
4.5 地址空间 address_space
struct address_space 管理内存页缓存,与普通文件读取密切相关。它把文件逻辑偏移与内存页建立映射,使 read 请求优先命中缓存,未命中时再向底层文件系统请求磁盘数据。
5. 关键操作接口
VFS 通过操作函数表来多态地调用具体文件系统。不同文件系统实现同一接口,因此内核通用代码可以保持不变。下面以文件操作集合为例:
struct file_operations {
int (*open) (struct inode *, struct file *);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*release) (struct inode *, struct file *);
};
用户调用 read(fd, buf, n) 时,系统根据 fd 找到对应的 struct file,再调用 file->f_op->read,将请求交给具体文件系统实现。
6. 从 open 到 read 的调用流程
下面以 open 和 read 一次读文件为例,说明 VFS 如何串联各个对象。
具体步骤概括为:
- 用户调用
open("/home/user/file.txt", O_RDONLY)。 - VFS 从根目录或相对目录开始,逐级解析路径,查询 dentry cache,必要时请求具体文件系统的 inode 操作。
- 找到目标 inode 后,VFS 创建一个
struct file,填充打开模式、当前位置和操作集合。 - 调用具体文件系统的 open 方法。
- 返回一个最小未使用的文件描述符给用户。
- 用户调用
read(fd, buf, n)。 - VFS 根据 fd 找到对应
struct file,调用f_op->read。 - 通用读取路径会先查询页缓存 page cache;缓存命中则直接复制数据,否则调用底层文件系统读取块设备并填充缓存。
7. 文件系统注册与挂载
具体文件系统需要先向内核注册自己的 struct file_system_type,才能在挂载时被识别。注册的主要字段包括文件系统名称和 mount 回调。挂载时,内核调用相应 mount 方法,由该方法创建并填充超级块,进而建立根 dentry 和 inode。
struct file_system_type {
const char *name;
struct dentry *(*mount)(struct file_system_type *, int,
const char *, void *);
void (*kill_sb)(struct super_block *);
};
文件系统模块初始化时调用 register_filesystem(指向 file_system_type 的指针),卸载时调用 unregister_filesystem。挂载命令 mount 会让内核按名称查找已注册的文件系统类型,再执行其 mount 函数。
8. 路径查找与 dentry cache
路径查找的目标是把字符串路径转换为 inode。Linux 采用逐段解析,从当前目录或根目录出发,每次取出一个路径分量,通过 dentry cache 快速查找。若缓存命中,则直接取得 dentry 和 inode;否则调用父目录 inode 提供的 lookup 操作,让具体文件系统去磁盘上读取并建立新的 dentry。
dentry cache 内部使用哈希表和 LRU 链表管理,同时使用引用计数。被使用中的 dentry 不会被回收;未使用但仍在缓存中的 dentry 可以被回收,以释放内存。负 dentry 还可以缓存“路径不存在”的结果,加速无效路径的再次解析。
9. 缓存与性能优化
VFS 与文件系统之间的性能很大程度依赖多层缓存:
- 页缓存 page cache:缓存文件内容,read / write 优先在内存页中进行,减少磁盘访问。
- dentry cache:缓存目录项与路径解析结果,减少路径遍历。
- inode cache:缓存 inode 元数据,避免每次获取文件属性都读取磁盘。
- 写回机制:修改后的页先标记为脏页,由后台线程异步写回磁盘,提高写入吞吐,同时兼顾数据安全。
这些缓存共同服务于普通文件读写,也服务于 mmap、sendfile 等高级操作。
10. 常见文件系统与 VFS 的关系
VFS 将具体文件系统隔离在统一接口之后,因此不同文件系统对 VFS 的意义不同:
- ext4 / XFS / Btrfs:管理真实块设备,实现完整的磁盘格式与元数据操作。
- NFS / CIFS:作为客户端,将 VFS 请求转换为网络协议消息。
- procfs / sysfs:没有磁盘存储,通过回调动态生成内核信息,配合只读或控制接口使用。
- tmpfs:数据保存在内存或交换分区中,关机即消失,但提供高速读写。
正是 VFS 的统一抽象,使这些差异巨大的文件系统能够共存于同一棵目录树中,用户和大部分内核代码都无需感知具体底层实现。
11. 总结
虚拟文件系统是操作系统中连接用户空间与底层存储的重要抽象层。它围绕超级块、inode、dentry、file 和 address_space 等对象建立模型,通过函数指针表实现多态分派,并通过路径查找与多级缓存提升性能。理解 VFS 的架构,有助于学习操作系统、内核开发以及设计自己的文件系统。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)