VFS 架构全景 —— Linux 的"一切皆文件"是怎么实现的
下一篇 vfs-file-operations 逐字段拆解
file_operationsvtable 里每一个函数指针的分发逻辑,vfs-and-socket 讲 socket 怎么借 VFS 之力实现read()/poll()/close()。本篇聚焦 VFS 本身:它是什么、为什么设计成这样、核心对象之间怎么关联。
一、一句话:VFS 是什么
VFS(Virtual File System)是 Linux 内核中的一层多态框架——它定义了一套统一的文件操作接口(file_operations),所有文件系统(ext4 / XFS / NFS / procfs / sysfs / sockfs / pipefs / devtmpfs ...)各自提供自己的实现,用户态的 open() / read() / write() / close() 只认接口、不认底层。
没有 VFS 时,每种文件类型需要一套独立的系统调用(ext4_read / nfs_read / pipe_read / socket_read ...),应用代码得判断"这个 fd 是哪种文件"再决定调哪个。VFS 用函数指针表分发这件事一次解决。
二、四大核心对象
VFS 用四类对象建模一切文件系统:

图析:四个对象的生命周期截然不同。
super_block对应用户的 mount 操作——每挂载一个文件系统就创建一个,卸载时销毁;inode代表磁盘上的"文件本体",不管有没有人打开它,只要在磁盘上就存在(内核用 inode cache 缓存);dentry是路径名到 inode 的缓存映射——ls /home/user时内核不读磁盘,先查 dentry cache,命中就直接拿到 inode;file 是"打开文件"的会话对象——每次open()都新创建一个,close()销毁,多个进程 open 同一个文件会得到不同的struct file(共享同一个 inode),各管各的f_pos(偏移量)。一个 inode 对应 0 个或多个 file——没人打开就 0 个,多次 open 或多个进程各自 open 就多个。
对象职责对比
| 对象 | 对应操作 | 生命周期 | 共享范围 |
|---|---|---|---|
super_block | mount | mount → umount | 同一文件系统的所有进程共享 |
inode | mknod/creat | 文件存在 → 文件删除 | 同一文件的所有 open 共享 |
dentry | 路径查找 | 缓存在 dcache,内存压力回收 | 同一路径名全局共享(dcache 是全局的) |
| file | open | open → close | 进程私有(fork 后父子共享) |
三、多态分发:VFS 怎么把 read() 路由到正确的文件系统
内核不靠 if-else 判断 fd 类型——靠的是函数指针表(vtable),和 C++ 的虚函数表是同一个思想,只是 Linux 用 C 手工实现。

图析:
ksys_read()只管三件事——(1) 从 fd 拿到struct file,(2) 检查f_mode允许读,(3) 调用file->f_op->read()。至于这个read是指向generic_file_read_iter(普通文件)还是pipe_read(管道)还是sock_read_iter(socket),VFS 不关心。vtable 分发让 VFS 天然支持"新文件类型"——只要实现file_operations里的必要字段并注册,用户态就能用read()/write()/poll()统一操作它。fuse、procfs、debugfs、cgroupfs 都是这样挂进去的。
不是所有文件类型都支持所有操作
file_operations 是一张"大而全"的表,但具体文件系统只填它能支持的那几个:

图析:
file_operations是一张"能力表"——填了哪个函数指针,这个文件类型就支持哪种操作。ext4 普通文件几乎全支持;pipe 不支持mmap(管道是内存缓冲区,不需要映射)、不支持llseek(pipe 是 FIFO 流,没有"位置"概念);socket 同样不支持mmap和llseek。用户调用不支持的操作用,VFS 返回 -ENODEV 或 -ESPIPE,而不是崩溃。
四、VFS 在整个 Linux 内核中的位置

图析:VFS 在 syscall 和文件系统之间充当路由器——所有文件相关的系统调用(
read、write、open、close、stat、mmap、poll...)先进 VFS,VFS 做权限检查、fd 解析、文件锁检查等通用操作,然后file->f_op->read()一跳进入具体的文件系统实现。不是所有文件系统都需要块层——pipe 和 socket 的数据在内核内存中,根本不需要磁盘;procfs 的数据在seq_read里动态生成,也不需要块层。只有 ext4/XFS 等磁盘文件系统才继续往下走submit_bio→ 驱动 → DMA。
五、与仓库其他模块的关联
VFS 不是孤立的——它是 syscall 的下一站、file 的容器、socket 的身份证、mmap 的入口、read/write 的必经之路:
| 关联模块 | 文档 | 关系 |
|---|---|---|
| 系统调用 | syscall | syscall 陷入内核后第一站就是 VFS |
| fd 表 | files-struct | files_struct.fd_array[fd] → struct file,是 VFS 的入口 |
| I/O 全链路 | read-write-process | VFS 在 10 层调用链中处于第 5 层,负责分发到具体 FS |
| file_operations 详解 | vfs-file-operations | 本篇姊妹篇,逐字段拆解每个函数指针 |
| socket 与 VFS | vfs-and-socket | socket 怎么借 VFS 之力实现 read/write/poll/close |
| mmap | mmap | mmap() → VFS → file->f_op->mmap → 建立 VMA |
| 信号驱动的 IO | signals-kernel | F_SETSIG 通过 VFS 的 ioctl 路径设置 |
六、VFS 相关系统调用速查
| 系统调用 | VFS 入口 | 分发到 |
|---|---|---|
read(fd, ...) | ksys_read | file->f_op->read / file->f_op->read_iter |
write(fd, ...) | ksys_write | file->f_op->write / file->f_op->write_iter |
open(path, ...) | do_sys_open | path_openat → dir->f_op->open + file->f_op->open |
close(fd) | __close_fd | file->f_op->release(f_count 归零时才调) |
poll(...) / epoll_ctl(...) | do_sys_poll | file->f_op->poll |
mmap(...) | ksys_mmap | file->f_op->mmap |
lseek(fd, ...) | ksys_lseek | file->f_op->llseek |
ioctl(fd, ...) | do_vfs_ioctl | file->f_op->unlocked_ioctl / file->f_op->compat_ioctl |
stat(path, ...) | vfs_statx | path.dentry->d_inode(不走 file,走 inode) |
fcntl(fd, F_DUPFD, ...) | do_fcntl | 操作 file / files_struct,不经过具体 FS |
七、延伸阅读
- vfs-file-operations —
file_operationsvtable 逐字段详解 - vfs-from-process — 进程视角:五跳链路、open/read/close 全时序
- vfs-and-socket — socket 与 VFS 的深度融合:两套 vtable、read() 分发链、三大好处
- syscall — 系统调用的执行过程及开销
- files-struct — 进程 fd 表(
files_struct)全景 - read-write-process — read+write 10 层完整调用链
- mmap — mmap 原理:VFS → VMA → 缺页
总结
VFS 是夹在系统调用和具体文件系统之间的多态路由层——通过 file->f_op 函数指针表把 read()/write()/poll() 分发到 ext4/pipe/socket/procfs 各自的实现,让用户态只需要认"文件描述符"这一个概念、不用关心底层到底是什么文件类型。