VFS(Virtual File System,虚拟文件系统)是Linux系统中一个非常重要的概念,是Linux设计思想的核心。作为Linux内核中的一个关键抽象层,它的主要作用包括:为用户程序提供了统一的文件访问接口。支持并兼容不同的文件系统(如ext4、XFS、NFS等)。屏蔽底层实现细节。
Linux 支持数十种文件系统,它们可能位于本地磁盘、内存、网络或特殊设备上。如果没有 VFS,内核就需要为每种文件系统分别实现一套系统调用接口,这不仅使内核变得臃肿,也让应用程序难以跨文件系统移植。VFS 的核心作用就是抽象与统一

为上层应用提供一致的 API。为下层文件系统定义清晰的接口规范,使得新的文件系统可以像"插件"一样方便地加入内核。实现不同文件系统在同一目录树下的共存(例如 / 用 ext4,/proc 用 procfs,/sys 用 sysfs)随着内核版本的迭代,文件系统的数量还在增加。
当进程发起一个文件操作时,系统调用进入内核,调用 VFS 的通用函数。VFS 根据操作所涉及的目录或文件,找到其所属的具体文件系统,然后调用该文件系统注册的实现函数
用户空间进程 (应用程序)
↓ (系统调用:open, read, write...)
Linux 内核系统调用接口
↓
虚拟文件系统 (VFS) ←→ 目录项缓存 (dentry cache)、inode 缓存
↓
具体文件系统 (ext4, tmpfs, NFS...)
↓
块设备层 / 网络协议栈 / 内存管理...
↓
物理设备 (硬盘、网络、内存...)
每种文件系统都有一套独立的文件访问接口。如果没有VFS,用户程序必须熟悉每种文件系统的文件访问接口,才能够很好地使用文件系统,这样显然是非常不合理的。VFS的出现,将文件系统的文件访问工作交给内核,用户程序只需要学习少量的系统调用(约为10多种)就能够轻松地访问各种文件系统。VFS的核心设计思想是多态(面相对象编程的三大特性之一),用户程序调用相同的文件访问接口能够实现不同的功能。这种设计方式可以屏蔽底层的细节,用户程序不再需要关注底层实现细节,只需要学会调用系统调用即可。另外,这种设计方式非常便于增加新文件系统,新文件系统添加至内核不会对原有的文件系统有任何影响(文件系统之间不存在耦合关系)。

VFS 的核心对象模型
VFS 基于面向对象的思想,定义了四种主要对象类型,每种对象都包含数据以及指向操作函数表的指针。具体文件系统需要提供这些函数的具体实现。
**超级块 (super_block):**表示一个已挂载的文件系统实例。Linux挂载文件系统时,会在内存创建一个超级块,用于管理文件系统。
**索引节点 (inode):**表示文件系统中的一个文件或目录,用于访问文件系统中的文件。
**目录项 (dentry):**表示文件路径中的一个分量,是Linux文件树的树节点,将文件路径名与对应的inode关联起来。
**文件对象 (file):**表示进程中一个已打开文件的实例,同一个文件可以被多个进程同时打开,每打开一次文件都要创建一个文件对象。
ext4文件系统如下:

超级块对象 (super_block)
代表一个已挂载的文件系统。
包含该文件系统的全局信息,例如:
块大小、最大文件大小。挂载选项、设备标识符。
指向各种操作表的指针(如 s_op,指向 super_operations 结构体,其中包含分配 inode、销毁 inode、同步文件系统等方法)。
每个挂载点(如 /home)对应一个超级块对象。
cpp
struct super_block {
dev_t s_dev; /* 设备号 */
unsignedchar s_blocksize_bits; /* 文件系统数据块大小的位数 */
unsignedlong s_blocksize; /* 文件系统数据块大小(字节为单位)*/
loff_t s_maxbytes; /* 该文件系统支持的最大文件长度 */
struct file_system_type *s_type;/* 文件系统类型 */
conststruct super_operations *s_op;/* 超级块操作函数表 */
unsignedlong s_magic; /* 魔数,标识文件系统类型(如 Ext4 为 0xEF53)*/
struct dentry *s_root; /* 超级块(文件系统)根目录 */
struct block_device *s_bdev;/* 指向块设备 */
void *s_fs_info; /* 文件系统私有数据指针 */
struct list_head s_inodes; /* 指向文件系统中所有 inode 的链表头 */
......
};
super_block是文件系统的控制中心,用于管理整个文件系统。
主要作用:存储文件系统的全局信息和运行状态,减少磁盘I/O。提供文件系统操作方法(s_op文件系统函数表)。以s_root为根目录,建立和维护文件系统内部文件树。通过s_fs_info可以访问实际文件系统超级块。
索引节点对象 (inode)
代表一个文件或目录的元数据(不包含文件名)。
包含:
文件类型(普通文件、目录、符号链接等)、权限、属主、时间戳。
文件大小、数据块指针(指向实际数据的位置)。
指向操作表的指针(i_op 指向 inode_operations,如创建文件、删除文件、创建链接等;i_fop 指向 file_operations,用于打开后的文件操作)。
每个文件(或目录)在磁盘上有一个对应的 inode,在内存中也有一份缓存副本。
cpp
struct inode {
umode_t i_mode; /* 文件类型和访问权限 */
kuid_t i_uid; /* 文件所有者用户ID */
kgid_t i_gid; /* 文件所有者组ID */
conststruct inode_operations *i_op;/* inode操作函数表 */
struct super_block *i_sb; /* 指向所属超级块 */
struct address_space *i_mapping;/* 文件页缓存 */
unsignedlong i_ino; /* inode号,在同一个文件系统内,通过i_ino字段唯一标识一个文件 */
union {
constunsignedint i_nlink; /* 硬链接数 */
};
loff_t i_size; /* 文件大小(字节)*/
struct timespec64 i_atime;/* 最后访问时间 */
struct timespec64 i_mtime;/* 最后修改时间 */
struct timespec64 __i_ctime;/* 状态变更时间 */
blkcnt_t i_blocks; /* 文件占用的块数量 */
union {
conststruct file_operations *i_fop;/* 文件操作函数表 */
};
struct address_space i_data; /* 页缓存实体,i_mmaping指向i_data */
void *i_private; /* 私有指针 */
......
};
inode包含基本的文件元数据,但不包含文件实际数据(因为inode同样属于抽象层,不能和具体实现有耦合关系)。ext4文件系统中文件的实际数据存储在ext4_inode_info结构中,ext4_inode_info表示一个具体的ext4文件系统文件,ext4_inode_info内部包含一个inode,二者之间为组合关系。不同的文件系统这部分的具体实现存在差异。inode有两个重要的成员i_op(inode操作函数表)和i_fop(文件操作函数表)。
i_op主要负责处理与文件或目录本身相关的元数据操作和结构性操作,如:创建和创建文件、删除文件、查找文件、修改权限等。 i_fop的类型为struct file_operations结构。
i_fop函数表中定义的函数接口是Linux编程中经常会用到的函数,平时调用的文件操作相关的函数接口都是通过i_fop来实现的。VFS之所以能够为用户程序提供统一的文件访问接口,都是因为i_fop默默地完成了这些文件访问接口的具体实现。

当用户程序执行文件操作时,内核通过文件路径查找文件树找到目标文件对应的dentry,再从dentry获取inode,再通过inode的i_fop找到实际文件系统的文件操作函数表,最后调用函数表中对应的函数。
每种文件系统类型的文件操作函数表都不一样(上图只列举了一些常用的文件操作函数表)。文件创建的过程中,内核根据文件系统类型动态的给i_fop赋值,从而实现"接口统一、具体实现不一样"的动态多态特性。
目录项对象 (dentry)
代表路径中的一个组成部分,用于路径名解析。
主要包含:
文件名(例如 "test.txt")。
指向父目录 dentry 的指针。
指向该文件/目录对应 inode 的指针。
一个哈希表,用于快速查找(目录项缓存 dcache)。
dentry 对象主要用于加速路径查找,避免频繁读取磁盘。它不一定对应磁盘上的持久结构,大部分时间存在于内存中。
dentry是Linux文件树的树节点,对应的结构为struct dentry。每个文件和目录都会对应一个dentry,dentry之间通过父子关系形成一个树形结构(类似于家族树)。如果把Linux文件树比作一个地图,每个dentry都是一个小的站点,文件路径就是导航路线,文件路径用于指导内核快速定位到目标文件对应的dentry。
cpp
struct dentry {
struct dentry *d_parent; /* 指向父目录dentry */
struct qstr d_name; /* 文件名 */
struct inode *d_inode; /* 指向与该目录项关联的inode */
struct super_block *d_sb; /* 指向该dentry所属文件系统的超级块 */
struct list_head d_child; /* 将当前dentry插入到其父目录dentry的d_subdirs链表中 */
struct list_head d_subdirs; /* 链表头,指向该目录下的所有子dentry */
......
};
dentry的d_inode和i_sb成员分别指向索引节点(inode)和超级块(super_block)。内核需要通过inode和super_block这两个对象来访问文件系统。inode在Linux系统中用来唯一标识一个文件,对应的结构为struct inode。inode存储了文件的元数据以及提供了文件的操作接口,但是它并不包含的文件实际数据,因为inode属于抽象层,不能够和任何具体实现有耦合关系。每个inode都有一个唯一编号(inode号),用于在文件系统中索引文件。super_block是文件系统的一个实例,对应的结构为struct super_block,它包含了文件系统的元数据信息(如:文件系统类型、容量信息、结构参数、位置信息等),内核需要借助这些元数据信息才能访问文件系统。
dentry不包含文件的实际数据,只包含文件名、指向inode和super_block的指针以及与其他dentry的关联关系。d_name成员为文件名,d_inode成员指向inode(表示具体的文件),d_sb指向super_block(表示具体的文件系统)。 子dentry(文件或目录)的d_parent指向父dentry,表示二者为父子关系。多个子dentry可以指向相同的父dentry(对应一个目录下可以创建多个文件和子目录)
父dentry通过d_subdirs链表来维护所有的子dentry,子dentry通过d_child链表节点插入父dentry的d_subdirs链表。dentry通过d_parent、d_child、d_subdirs三个成员和其他dentry构成树形结构,从而形成Linux文件树。

文件对象 (file)
代表一个进程已经打开的文件。
包含:
当前文件读写位置(偏移量)。
打开模式(读、写、追加等)。
指向对应 dentry 的指针。
指向 file_operations 表的指针(其中包含 read、write、llseek、mmap 等具体操作方法)。
文件对象在进程打开文件时创建,关闭时释放。多个进程可以打开同一个文件,各自拥有独立的 file 对象,共享同一个 inode(但可能有各自的文件偏移)。
cpp
struct file {
fmode_t f_mode; /* 文件打开模式(读/写/执行)*/
loff_t f_pos; /* 当前文件偏移量(读写位置)*/
unsigned int f_flags; /* 打开标志(如O_NONBLOCK、O_SYNC)*/
struct fown_struct f_owner; /* 文件属主信息,用于信号通知 */
struct path f_path; /* 文件路径信息(dentry + mount)*/
struct inode *f_inode; /* 指向inode的指针,文件元数据 */
const struct file_operations *f_op; /* 文件操作函数表(read/write等)*/
struct address_space *f_mapping; /* 文件地址空间,用于mmap */
......
} ;
文件对象(file)用于记录已打开文件的状态信息,一个文件可以被多个进程多次打开,每次打开一个文件都需要创建一个新的file。
用户程序调用open函数打开一个文件时,内核会根据open函数传入的文件路径查找文件树,找到目标文件对应的dentry,再通过dentry找到inode。如果文件不存在(文件树中没有对应的dentry和inode),open函数可以创建文件并生成dentry和inode。
获取到inode后,内核会创建一个新的file,同时将inode的关键信息复制给file,如:file的f_op将指向inode的i_fop,file将直接使用inode的文件操作函数表。file的f_mapping成员将指向inode的f_mmaping,通过file可以访问inode的页缓存。同时file的f_inode将指向inode,file和inode进行绑定。
内核最后会申请一个未使用的fd(文件描述符),将fd和file进行映射,并将这种映射关系记录在进程的已打开文件表。后续的文件操作,只需要通过fd查找进程已打开文件表获取file,就能通过file访问文件了,从而避免每次文件访问都需要索引Linux文件树。

操作示例
①用户调用 open("/home/user/notes.txt", O_RDONLY)。
②系统调用进入内核,VFS 接收请求。
③VFS 进行路径名解析:从根目录 / 或当前目录开始,逐级查找 dentry 缓存。
如果 home 的 dentry 不在缓存中,则从其父目录的 inode 中读取目录内容,创建 dentry 并关联 inode。继续解析 user、notes.txt,最终找到目标文件的 dentry,并获取其 inode。
④VFS 根据 inode 中的信息确定文件所属的文件系统(例如 ext4),并找到该文件系统已注册的 file_operations 表。
⑤VFS 分配一个新的 file 对象,设置其文件操作指针为该文件系统的实现,并将文件位置初始化为 0。
⑥调用具体文件系统的 open 实现(如果需要执行额外的操作,如 NFS 的网络连接)。
⑦返回一个文件描述符给用户进程,该描述符指向内核中的 file 对象。
此后,用户调用 read(fd, ...) 时,VFS 通过 fd 找到对应的 file 对象,再通过 file->f_op->read 调用具体文件系统的读函数,最终读取数据并返回。
VFS 的重要功能
**文件系统注册与挂载:**每种文件系统在初始化时向 VFS 注册自己(提供超级块读取函数等)。挂载时 VFS 会调用该函数读取超级块,建立根 dentry 和 inode。
缓存机制:
dentry 缓存:加速路径解析。inode 缓存:减少对磁盘 inode 的重复读取。页缓存(page cache):虽不完全属于 VFS,但与 VFS 紧密相关,用于缓存文件数据页,提高读写性能。
**特殊文件系统:**如 procfs、sysfs、devpts 等,它们没有对应的磁盘设备,而是在访问时动态生成信息,VFS 同样通过标准接口管理它们。
Linux 的虚拟文件系统是内核中一个精巧的设计,它通过定义一组通用的对象和操作接口,成功地将各种不同类型的文件系统统一到同一个命名空间下,为用户空间提供了透明、高效的文件访问能力。VFS 的存在极大地增强了 Linux 的可扩展性和兼容性,也是"一切皆文件"这一设计哲学的核心支撑。