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 层接收请求,根据路径和挂载信息找到对应文件系统实例,再调用该文件系统注册的函数指针,最后访问存储介质或内存数据。

用户进程 open / read / write

系统调用层

VFS 统一接口

ext4 文件系统驱动

XFS 文件系统驱动

NFS 网络文件系统

procfs / sysfs 伪文件系统

块设备驱动

网络协议栈

内核内存数据结构

其中,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 系统调用

路径查找 dentry / inode

创建 struct file

调用 f_op->open

返回文件描述符 fd

read(fd, buf, n)

通过 fd 定位 struct file

调用 f_op->read

查询页缓存 page cache

返回读取结果

具体步骤概括为:

  1. 用户调用 open("/home/user/file.txt", O_RDONLY)。
  2. VFS 从根目录或相对目录开始,逐级解析路径,查询 dentry cache,必要时请求具体文件系统的 inode 操作。
  3. 找到目标 inode 后,VFS 创建一个 struct file,填充打开模式、当前位置和操作集合。
  4. 调用具体文件系统的 open 方法。
  5. 返回一个最小未使用的文件描述符给用户。
  6. 用户调用 read(fd, buf, n)。
  7. VFS 根据 fd 找到对应 struct file,调用 f_op->read。
  8. 通用读取路径会先查询页缓存 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 的架构,有助于学习操作系统、内核开发以及设计自己的文件系统。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐