# VFS 架构全景 —— Linux 的"一切皆文件"是怎么实现的

> 下一篇 [vfs-file-operations.md](/concepts/vfs/vfs-file-operations.md) 逐字段拆解 `file_operations` vtable 里每一个函数指针的分发逻辑，[vfs-and-socket.md](/concepts/vfs/vfs-and-socket.md) 讲 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 用四类对象建模一切文件系统：

```plantuml
@startuml
left to right direction
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<meta>> #E1BEE7
  BorderColor<<meta>>     #7B1FA2
  BackgroundColor<<live>> #BBDEFB
  BorderColor<<live>>     #1976D2
}
rectangle "**super_block**\n文件系统元信息\n─────────────\n· 设备号 (s_dev)\n· 块大小 (s_blocksize)\n· super_operations → ops\n· 根 dentry (s_root)" <<meta>> as SB
rectangle "**inode**\n文件本体\n─────────────\n· i_ino (inode 号)\n· i_mode (类型+权限)\n· i_size (大小)\n· i_fop (默认 file_ops)\n· i_sb → super_block" <<meta>> as INODE
rectangle "**dentry**\n目录项缓存\n─────────────\n· d_name (文件名)\n· d_inode → inode\n· d_parent → 父目录\n· d_sb → super_block" <<meta>> as DENTRY
rectangle "**file**\n打开文件实例\n─────────────\n· f_op → file_operations\n· f_pos (当前偏移)\n· f_flags (O_RDWR...)\n· f_count (引用计数)\n· private_data (socket等)" <<live>> as FILE
SB --> DENTRY : s_root
DENTRY --> INODE : d_inode
FILE --> DENTRY : f_path.dentry
FILE --> INODE : f_inode
INODE --> SB : i_sb
note bottom of FILE
  **super_block / inode / dentry → 文件系统层**（磁盘/持久化视角）
  **file → 进程层**（运行时/会话视角）
  同一个 inode 可以被多个 file 打开（open 多次 or fork）
end note
@enduml
```

> **图析**：四个对象的生命周期截然不同。`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 手工实现。

```plantuml
@startuml
skinparam shadowing false
participant "用户进程\nread(fd, buf, 4096)" as U #C8E6C9
participant "VFS 层\nksys_read(fd)" as VFS #BBDEFB
participant "file->f_op->read" as FOP
participant "具体实现\n(四选一)" as IMPL #FFF9C4
U -> VFS : read(fd, buf, 4096)
VFS -> VFS : fdget(fd) → struct file
VFS -> VFS : 检查 f_mode 是否有 FMODE_READ
VFS -> FOP : file->f_op->read(file, buf, 4096, &f_pos)
FOP -> IMPL : **普通文件**\ngeneric_file_read_iter()\n读 page cache / 触发磁盘
FOP -> IMPL : **pipe**\npipe_read()\n从管道缓冲区读
FOP -> IMPL : **socket**\nsock_read_iter()\n→ sock->ops->recvmsg
FOP -> IMPL : **procfs**\nseq_read()\n动态生成 `cat /proc/cpuinfo`
note right of IMPL
同一个 read()，四套完全不同的实现
- VFS 只认「你有没有 file_operations」
- 不关心你是什么文件系统
end note
@enduml
```

> **图析**：`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` 是一张"大而全"的表，但具体文件系统只填它**能支持的**那几个：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<yes>> #C8E6C9
  BorderColor<<yes>>     #388E3C
  BackgroundColor<<no>>  #FFCDD2
  BorderColor<<no>>      #C62828
}
rectangle "file_operations {\n  .read = ...\n  .write = ...\n  .poll = ...\n  .mmap = ...\n  .ioctl = ...\n  .llseek = ...\n  .release = ...\n}" as VFS_FOPS
rectangle "ext4" as EXT4 {
  rectangle ".read ✓" <<yes>> as ER
  rectangle ".write ✓" <<yes>> as EW
  rectangle ".poll ✓" <<yes>> as EP
  rectangle ".mmap ✓" <<yes>> as EM
  rectangle ".llseek ✓" <<yes>> as EL
}
rectangle "pipe" as PIPE {
  rectangle ".read ✓" <<yes>> as PR
  rectangle ".write ✓" <<yes>> as PW
  rectangle ".poll ✓" <<yes>> as PP
  rectangle ".mmap ✗" <<no>> as PM
  rectangle ".llseek ✗" <<no>> as PL
}
rectangle "socket" as SK {
  rectangle ".read ✓" <<yes>> as SR
  rectangle ".write ✓" <<yes>> as SW
  rectangle ".poll ✓" <<yes>> as SP
  rectangle ".mmap ✗" <<no>> as SM
  rectangle ".llseek ✗" <<no>> as SL
}
note bottom of SK : socket 不支持 mmap → 返回 -ENODEV\nsocket 不支持 llseek → no_llseek（ESPIPE）\nVFS 不强制实现所有字段，空指针即不支持
@enduml
```

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

## 四、VFS 在整个 Linux 内核中的位置

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<u>> #C8E6C9
  BorderColor<<u>>     #388E3C
  BackgroundColor<<v>> #BBDEFB
  BorderColor<<v>>     #1976D2
  BackgroundColor<<f>> #FFF9C4
  BorderColor<<f>>     #F9A825
  BackgroundColor<<b>> #E1BEE7
  BorderColor<<b>>     #7B1FA2
}
rectangle "用户态\n─────────────────\n应用代码: read/write/open/close/poll/mmap...\nglibc 包装" <<u>> as U
rectangle "系统调用入口\n─────────────────\nentry_SYSCALL_64\nsys_read → ksys_read" as SYSCALL
rectangle "**VFS 层**\n─────────────────\n· 通用路径权限检查\n· fd → struct file 转换\n· file->f_op->read() 分发\n· struct file / dentry / inode 管理" <<v>> as VFS_LAYER
rectangle "文件系统实现层\n─────────────────\next4 · XFS · NFS · procfs\nsysfs · sockfs · pipefs · devtmpfs\nfuse · debugfs · cgroupfs ..." <<f>> as FS
rectangle "块层 + 驱动 + 设备\n─────────────────\nbio 提交 · I/O 调度 · NVMe/SCSI 驱动\nDMA 引擎 · 磁盘/SSD" <<b>> as BLOCK
U -down-> SYSCALL
SYSCALL -down-> VFS_LAYER
VFS_LAYER -down-> FS
VFS_LAYER -right-> BLOCK : 仅普通文件需要
note right of VFS_LAYER
  **VFS 是夹在 syscall 和实际 FS 之间的统一层**
  pipe/socket/procfs 不需要块层
  它们直接在内核内存中完成 I/O
end note
@enduml
```

> **图析**：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 的必经之路：

| 关联模块 | 文档 | 关系 |
|---------|------|------|
| 系统调用 | [../process/syscall.md](/concepts/process/syscall.md) | syscall 陷入内核后第一站就是 VFS |
| fd 表 | [../process/task-resources/files-struct.md](/concepts/process/task-resources/files-struct.md) | `files_struct.fd_array[fd]` → `struct file`，是 VFS 的入口 |
| I/O 全链路 | [../io/read-write-process.md](/concepts/io/read-write-process.md) | VFS 在 10 层调用链中处于第 5 层，负责分发到具体 FS |
| file_operations 详解 | [vfs-file-operations.md](/concepts/vfs/vfs-file-operations.md) | 本篇姊妹篇，逐字段拆解每个函数指针 |
| socket 与 VFS | [vfs-and-socket.md](/concepts/vfs/vfs-and-socket.md) | socket 怎么借 VFS 之力实现 read/write/poll/close |
| mmap | [../elf/mmap.md](/concepts/elf/mmap.md) | `mmap()` → VFS → `file->f_op->mmap` → 建立 VMA |
| 信号驱动的 IO | [../process/task-resources/signals-kernel.md](/concepts/process/task-resources/signals-kernel.md) | `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.md](/concepts/vfs/vfs-file-operations.md) — `file_operations` vtable 逐字段详解
- [vfs-from-process.md](/concepts/vfs/vfs-from-process.md) — 进程视角：五跳链路、open/read/close 全时序
- [vfs-and-socket.md](/concepts/vfs/vfs-and-socket.md) — socket 与 VFS 的深度融合：两套 vtable、read() 分发链、三大好处
- [../process/syscall.md](/concepts/process/syscall.md) — 系统调用的执行过程及开销
- [../process/task-resources/files-struct.md](/concepts/process/task-resources/files-struct.md) — 进程 fd 表（`files_struct`）全景
- [../io/read-write-process.md](/concepts/io/read-write-process.md) — read+write 10 层完整调用链
- [../elf/mmap.md](/concepts/elf/mmap.md) — mmap 原理：VFS → VMA → 缺页

## 一句话总结

**VFS 是夹在系统调用和具体文件系统之间的多态路由层——通过 `file->f_op` 函数指针表把 `read()`/`write()`/`poll()` 分发到 ext4/pipe/socket/procfs 各自的实现，让用户态只需要认"文件描述符"这一个概念、不用关心底层到底是什么文件类型。**
