# 进程如何感知 VFS —— 从 `task_struct` 到 `file_operations` 的全链路

> 上一篇 [vfs-overview.md](/concepts/vfs/vfs-overview.md) 讲 VFS 本身的多态分发机制，[vfs-file-operations.md](/concepts/vfs/vfs-file-operations.md) 拆解 `file_operations` vtable 逐字段。本篇切换视角：**站在进程的角度，看它怎么通过 fd 和系统调用与 VFS 交互**——进程不"知道" VFS，但它的每一次文件操作都穿过 VFS。

## 一、一句话：进程看到的 vs 内核实际发生的

进程眼里文件操作很简单——一个整数 `fd` + 几个系统调用：

```c
int fd = open("/tmp/a.txt", O_RDONLY);   // 返回 3
read(fd, buf, 4096);                      // 从 3 读
close(fd);                                // 关闭 3
```

进程**不感知 VFS**——它不知道 `fd=3` 被换成 `pidgin` 可以读到 pipe 内容，不知道同一个 `read()` 能读磁盘文件、socket、procfs。进程只知道：**"给了我一串数字（fd），调 `read` / `write` / `close` 就能操作它"**。

VFS 的职责就是在进程**看不见的地方**，把 `fd` 这个整数翻译成正确的内核对象、把 `read()` 分发到正确的实现。

## 二、数据结构全景：从 `task_struct` 到 VFS 接口的指针链

进程到 VFS 有**五跳**——每一跳都是通过一个指针字段完成的：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<proc>> #E3F2FD
  BorderColor<<proc>>     #1565C0
  BackgroundColor<<vfs>>  #BBDEFB
  BorderColor<<vfs>>      #1976D2
  BackgroundColor<<obj>>  #C8E6C9
  BorderColor<<obj>>      #388E3C
}
rectangle "**task_struct**\n(进程描述符)\n─────────────\n· pid\n· files → files_struct *\n· fs → fs_struct *" <<proc>> as TASK
rectangle "**files_struct**\n(打开文件表)\n─────────────\n· fdt → fdtable *\n· next_fd (下次分配的fd号)\n· count (引用计数)" <<proc>> as FS
rectangle "**fdtable**\n(fd数组本体)\n─────────────\nfd[0] → stdin (file*)\nfd[1] → stdout (file*)\nfd[2] → stderr (file*)\nfd[3] → 你 open 的文件" <<proc>> as FDT
rectangle "**struct file**\n(打开文件描述)\n─────────────\n· f_op → file_operations*\n· f_pos (当前偏移)\n· f_flags (O_RDONLY...)\n· f_count (引用计数)\n· private_data (→socket/pipe...)" <<vfs>> as FILE
rectangle "**file_operations**\n(VFS 接口表)\n─────────────\n· read()\n· write()\n· poll()\n· mmap()\n· ioctl()\n· release()\n· ..." <<vfs>> as FOPS
rectangle "具体对象\n(disk inode / socket\n/ pipe / procfs)\n─────────────\nfile->private_data\n指向真正的"东西"" <<obj>> as OBJ
TASK --> FS : files
FS --> FDT : fdt
FDT --> FILE : fd[3]
FILE --> FOPS : f_op
FILE --> OBJ : private_data
note bottom of OBJ
  **进程不知道这一层**——进程只看到 fd=3
  这是 VFS 的"真正服务端"
  ext4 → inode、socket → struct socket、
  pipe → pipe_inode_info、
  procfs → seq_file
end note
note right of FILE
  **f_op 是"身份切换点"**
  file 是通用壳，f_op 决定
  它到底是 ext4/pipe/socket/procfs
end note
@enduml
```

> **图析**：五跳链路——`task_struct.files`（指向打开文件表）→ `files_struct.fdt`（指向 fd 数组）→ `fdtable.fd[3]`（数组第 3 项，即 fd=3）→ `struct file`（打开文件实例，存偏移和标志）→ `file->f_op`（指向 `file_operations` vtable）。**`f_op` 是关键的分叉口**——同一个 `struct file`，`f_op` 指向 `ext4_file_operations` 就是磁盘文件、指向 `pipefs_file_operations` 就是管道、指向 `socket_file_ops` 就是 socket。进程只看到 `fd=3`，不知道这一跳背后是什么。

### 五跳链路速查

| 跳 | 从 | 到 | 字段 | 含义 |
|----|----|----|------|------|
| ① | `task_struct` | `files_struct` | `->files` | 该进程的打开文件表 |
| ② | `files_struct` | `fdtable` | `->fdt` | fd 数组本体 |
| ③ | `fdtable` | `struct file` | `->fd[fd]` | 用 fd 数字做下标取出 `file *` |
| ④ | `struct file` | `file_operations` | `->f_op` | VFS 接口表——决定"这是什么类型的文件" |
| ⑤ | `struct file` | 具体对象 | `->private_data` | 真正的数据源——inode / socket / pipe |

> ①~③ 是**进程层**——进程有私有副本（fork 复制 files_struct，线程共享），④~⑤ 是**VFS/对象层**——所有进程通过同一套 vtable 访问。

## 三、`open()` 全过程 —— 一个 fd 是怎么连接到 VFS 的

> `open()` 是进程第一次"触碰" VFS 的时刻——此前的进程和 VFS 没有任何关系。

```plantuml
@startuml
skinparam shadowing false
skinparam participant {
  BackgroundColor #C8E6C9
  BorderColor #388E3C
}
participant "用户进程\nfd = open(\"/tmp/a.txt\", O_RDONLY)" as U
participant "syscall\nsys_open → do_sys_open" as SYS
participant "VFS — 路径查找\npath_openat" as PATH
participant "VFS — 文件创建\nget_empty_filp" as ALLOC
participant "VFS — fd 分配\nget_unused_fd_flags" as FD_ALLOC
participant "VFS — fd 绑定\nfd_install" as INSTALL
participant "ext4\n(具体文件系统)" as EXT4
U -> SYS : open(path, flags)
SYS -> SYS : 从 flags 提取 O_RDONLY / O_CREAT 等
SYS -> PATH : path_openat(dirfd=AT_FDCWD, path, flags)
PATH -> PATH : 逐级解析路径："tmp" → "a.txt"
PATH -> PATH : 查 dcache（dentry cache）
PATH -> PATH : dcache 未命中 → 查磁盘目录 inode
PATH -> EXT4 : ext4_lookup(dir, "a.txt")
EXT4 --> PATH : 返回 inode + dentry
PATH -> PATH : 权限检查（inode 的 UID/GID + 文件模式位）
PATH -> ALLOC : get_empty_filp()
ALLOC -> ALLOC : 分配 struct file 并初始化：
ALLOC -> ALLOC : · f_mode = FMODE_READ
ALLOC -> ALLOC : · f_flags = O_RDONLY
ALLOC -> ALLOC : · f_pos = 0
ALLOC -> ALLOC : · f_count = 1
ALLOC -> ALLOC : · f_op = ext4_file_operations ← **关键！**
ALLOC -> ALLOC : · private_data = NULL（普通文件不需要）
ALLOC --> PATH : struct file *
PATH -> FD_ALLOC : get_unused_fd_flags(flags)
FD_ALLOC -> FD_ALLOC : 在当前 files_struct 的 fdtable 中
FD_ALLOC -> FD_ALLOC : 找第一个空闲位 → 得到 fd=3
FD_ALLOC --> PATH : fd=3
PATH -> INSTALL : fd_install(fd=3, file)
INSTALL -> INSTALL : fdtable.fd[3] = file
PATH --> SYS : fd=3
SYS --> U : fd=3
note right of ALLOC
  **VFS 不做磁盘 I/O**
  路径查找、inode 读取发生在 PATH 阶段
  get_empty_filp() 只分配内存 + 设 f_op
end note
note right of FD_ALLOC
  fd 分配策略：
  从 allocated 位图中找最小编号的空闲位
  O_CLOEXEC 也在这里标记到 close_on_exec 位图
end note
@enduml
```

> **图析**：`open()` 分四个阶段——**(1) 路径查找**：从 `/` 开始逐级解析目录，查 dcache，dcache 未命中则掉进具体文件系统的 lookup（如 `ext4_lookup`）读磁盘目录；**(2) 分配 `struct file`**：`get_empty_filp()` 分配并初始化，**此时 `f_op` 被设为该文件系统对应的 `file_operations`**——这一步决定了后续所有 `read`/`write`/`poll` 走哪条路径；**(3) 分配 fd**：在 `fdtable` 找空闲位，返回一个整数；**(4) 绑定**：`fd_install()` 把 `file *` 写入 `fdtable.fd[fd]`。至此，进程拿到了 `fd=3`，而 `fdtable.fd[3]` 指向的 `struct file` 已经牢牢绑定了该文件系统的 `file_operations`。


> **进程此刻的"感知" **：只看到了 `fd=3` 这个返回值。它不知道路径查找经历了多少级目录、查了多少次 dcache、`ext4_lookup` 读了多少个磁盘块。它只知道——**"现在我可以对 fd=3 调 read/write/close 了"**。

### 不同文件类型 open 时 f_op 的赋值

| 系统调用 | 分配的 f_op | 走向 |
|---------|------------|------|
| `open("/tmp/a.txt", ...)` | `ext4_file_operations` | 普通文件读写 |
| `socket(AF_INET, SOCK_STREAM, 0)` | `socket_file_ops` | 网络 I/O（见 [vfs-and-socket.md](/concepts/vfs/vfs-and-socket.md)） |
| `pipe(fds)` | `pipefs_file_operations` | 管道读写 |
| `eventfd(0, 0)` | `eventfd_fops` | 事件通知 |
| `timerfd_create(...)` | `timerfd_fops` | 定时器通知 |
| `open("/proc/cpuinfo", ...)` | `proc_file_operations`（`seq_operations`） | 动态生成内容 |

> **关键洞察**：`open()` 是在用户态"选文件系统"的唯一时刻——一旦 `f_op` 赋值完成，后面所有操作都通过这同一张 vtable 分发，直到 `close()`。

## 四、`read()` 从 fd 到数据的完整路径

> 进程只需要 `read(fd, buf, len)`——VFS 在背后穿过五跳指针链到达真正的数据。

```plantuml
@startuml
skinparam shadowing false
skinparam participant {
  BackgroundColor #C8E6C9
  BorderColor #388E3C
}
participant "用户进程\nread(3, buf, 4096)" as U
participant "VFS\nksys_read(fd=3)" as VFS
participant "file_operations\nfile->f_op->read" as FOP
participant "具体实现\n(五选一)" as IMPL #FFF9C4
U -> VFS : read(3, buf, 4096)
VFS -> VFS : **①** fdget(3) → task_struct.files → fdt → fd[3] → struct file
VFS -> VFS : **②** 检查 f_mode & FMODE_READ，不通过返回 -EBADF
VFS -> VFS : **③** 更新 f_pos（读完后偏移前移）
VFS -> FOP : **④** file->f_op->read(file, buf, 4096, &f_pos)
FOP -> IMPL : **ext4**\ngeneric_file_read_iter()\n查 page cache，未命中读磁盘
FOP -> IMPL : **xfs**\nxfs_file_read_iter()\n→ page cache + 磁盘
FOP -> IMPL : **pipe**\npipe_read()\n从环形缓冲区取数据
FOP -> IMPL : **socket**\nsock_read_iter()\n→ sock->ops->recvmsg → tcp_recvmsg
FOP -> IMPL : **procfs**\nseq_read()\n动态生成字符串 → copy_to_user
IMPL --> FOP : 读取字节数（或 -EAGAIN/0 EOF）
FOP --> VFS : 返回字节数
VFS --> U : 4096 字节已写入 buf
note right of VFS
  **VFS 的"三不"原则**
  - 不管文件系统类型
  - 不管数据从哪来（磁盘/内存/网络）
  - 不管要不要阻塞等待
  只管 fd → file → f_op 这条链
end note
@enduml
```

> **图析**：`read()` 的四步——**(1) fd→file**：`fdget()` 从当前进程的 `fdtable` 取出 `file *`（无锁的 RCU 读，极快）；**(2) 权限检查**：确认 `f_mode` 允许读；**(3) 偏移更新**：读完 `f_pos` 前移；**(4) vtable 分发**：`file->f_op->read()` 一跳进入具体实现。**五条路径在 `f_op` 处分道扬镳**——ext4 走 page cache → readahead → `submit_bio` → 磁盘，pipe 直接从内存环形缓冲区取（全程在内核、无磁盘 I/O），socket 走 `tcp_recvmsg` → `sk_receive_queue` → `copy_to_user`（可能阻塞等网络数据），procfs 在 `seq_read` 里现场拼接字符串然后 `copy_to_user`（全部在内存中动态生成）。


> **进程感知**：进程只知道 `read` 返回了 4096——它不关心这 4096 字节是从磁盘扇区 DMA 来的（ext4）、从网络包重组来的（socket）、还是内核现场生成的（procfs）。**同一个接口、同一个返回值、完全不同的内核路径**——这就是 VFS 给进程提供的抽象力。

## 五、`close()` 与引用计数 —— 什么时候才真正"断开" VFS 连接

> 进程以为 `close(fd)` 就算完事，但 VFS 的引用计数意味着："真正的清理"可能要等很久。

```plantuml
@startuml
skinparam shadowing false
skinparam participant {
  BackgroundColor #C8E6C9
  BorderColor #388E3C
}
participant "父进程\nclose(3)" as PARENT
participant "VFS\n__close_fd" as VFS
participant "子进程\nclose(3)" as CHILD
participant "struct file\n(引用计数)" as FILE
PARENT -> VFS : close(3)
VFS -> VFS : fdtable.fd[3] = NULL（断开 fd 连接）
VFS -> FILE : fput(file) → f_count-- → 从 2 降到 1
FILE --> VFS : f_count = 1，还有子进程在用
VFS --> PARENT : 0（成功，但 file 没释放）
note right of FILE : **进程以为"已关闭"\n但 VFS 还保留着 file**
... 过了一段时间 ...
CHILD -> VFS : close(3)（子进程也关了）
VFS -> VFS : fdtable.fd[3] = NULL
VFS -> FILE : fput(file) → f_count-- → 从 1 降到 0
FILE -> FILE : **f_count == 0！触发真正的释放：**
FILE -> FILE : ① file->f_op->flush(file)（如果有）
FILE -> FILE : ② 如有 FMODE_WRITE，回刷脏页
FILE -> FILE : ③ file->f_op->release(inode, file) ← **关键！**
note right of FILE : **这是 VFS 真正"松手"的时刻**
FILE -> FILE : ④ 释放 struct file 内存
FILE --> VFS : 完毕
VFS --> CHILD : 0
note bottom of FILE
  **release() 对不同文件类型的含义：**
  - 普通文件 → 减 inode 引用计数
  - socket → inet_release → TCP 四次挥手
  - pipe → 最后一个 reader 关闭 → 唤醒阻塞的 writer
  - eventfd → 唤醒阻塞的 EPOLL
end note
@enduml
```

> **图析**：`close()` 分两层——**(1) fd 层断开**：`fdtable.fd[3] = NULL`，进程再也无法通过 fd=3 访问这个文件，立即生效；**(2) VFS 层释放**：`fput(file)` 递减 `f_count`，**只有归零才调 `release()`**。fork 后父子各有一份 fd 表，但共享同一个 `struct file`（`f_count=2`），双方都 `close` 才释放。


> **进程感知**：进程以为 `close()` 是"终点"——但实际上 `struct file` 和它背后的资源（inode/socket/pipe）可能还在被其他进程使用。**引用计数是 VFS 给进程的一份"假象"**——让每个进程都觉得自己独占文件，而实际上 VFS 统筹管理着多进程共享。

## 六、`fork()` 后进程与 VFS 的关系

> `fork()` 不联系 VFS，但 `fork()` 改变了进程与 VFS 的**连接拓扑**。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<parent>> #E3F2FD
  BorderColor<<parent>>     #1565C0
  BackgroundColor<<child>>  #BBDEFB
  BorderColor<<child>>      #0D47A1
  BackgroundColor<<shared>> #C8E6C9
  BorderColor<<shared>>     #388E3C
}
rectangle "**父进程 task_struct**\nfiles → files_struct_A" <<parent>> as PTASK
rectangle "**子进程 task_struct**\nfiles → files_struct_B（独立）" <<child>> as CTASK
rectangle "**files_struct_A**（父）\nfdtable_A\nfd[0] → stdin\nfd[1] → stdout\nfd[3] → file_A\nfd[4] → file_B" <<parent>> as PFS
rectangle "**files_struct_B**（子）\nfdtable_B\nfd[0] → stdin (同父的 stdin)\nfd[1] → stdout (同父的 stdout)\nfd[3] → file_A (同父的 file_A)\nfd[4] → file_B (同父的 file_B)" <<child>> as CFS
rectangle "**struct file_A**\n(f_count=2)\nf_pos=1024\nf_op → ext4_file_operations" <<shared>> as FA
rectangle "**struct file_B**\n(f_count=2)\nf_pos=0\nf_op → socket_file_ops" <<shared>> as FB
PTASK --> PFS
CTASK --> CFS
PFS --> FA : fd[3]
PFS --> FB : fd[4]
CFS --> FA : fd[3]（同一 file_A）
CFS --> FB : fd[4]（同一 file_B）
note bottom of FA
  **fork 关键行为：**
  • files_struct 被**复制**（父子各一份、但指针相同）
  • struct file **不复制**（父子共享、f_count 递增）
  • f_pos 被**共享**——一个进程 read() 移动偏移
    另一个进程的 read() 从新位置接着读！
  这就是 fork 后父子写同一个 fd
  内容"接续"而非"覆盖"的原因
end note
@enduml
```

> **图析**：`fork()` 对 VFS 的影响体现在引用计数——**(1) `files_struct` 复制**：子进程得到一份新的 `files_struct`，但 `fdtable` 里的每个 `file *` 指针都指向和父进程**同一个 `struct file`**（不是拷贝，是指向同一块内存）；**(2) 所有共享的 `struct file` 的 `f_count` 递增**：`fork()` 遍历父进程的 fd 表，对每个有效的 `file` 调 `get_file()`——`f_count++`。这意味着父子可以独立 `close(fd)`——谁的 `close()` 都只影响自己的 fd 表，**但要等双方都 close 了，VFS 才调 `release()` 做真正的清理**。

### 四种共享场景对比

| 场景 | files_struct | struct file（f_pos） | close 行为 |
|------|-------------|---------------------|-----------|
| 同一进程两次 `open` 同一文件 | 同一份 | 不同的 file，独立偏移 | 各关各的，互不影响 |
| `dup(fd)` | 同一份 | **共享 file，共享偏移** | 关一个 fd 不影响另一个 fd 指向的 file（f_count 没归零） |
| `fork()` | 父子各一份（内容复制） | **共享 file，共享偏移** | 父子都 close 才释放 file |
| 线程（`CLONE_FILES`） | **共享同一份 files_struct** | 同份（本来就是同一套） | 一个线程 close，所有线程都失去这个 fd |

## 七、一句话总结

> **进程通过 `fd`（整数）→ `files_struct.fdtable[fd]` → `struct file` → `f_op` 这条五跳指针链间接"使用" VFS，自己只认 fd 和系统调用的返回值。`open()` 时 VFS 分配 file + 选定 f_op（从此操作走哪条路永久确定），`read()` / `write()` / `poll()` 靠 `f_op` 分发到正确的文件系统实现，`close()` 靠 `f_count` 引用计数推迟真正的释放——直到最后一个持有者松手。fork 后父子共享 `struct file`（f_count=2）、各自拥有独立的 fd 表。进程从头到尾不知道自己操作的是磁盘 inode 还是 pipe 还是 socket——这就是"一切皆文件"给进程的透明抽象。**

## 八、延伸阅读

- [vfs-overview.md](/concepts/vfs/vfs-overview.md) —— VFS 架构全景：四大对象、多态分发机制
- [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 深度融合：两套 vtable、`read()` 3 层分发
- [../process/task-resources/files-struct.md](/concepts/process/task-resources/files-struct.md) —— 进程 fd 表（`files_struct`）结构详解
- [../process/syscall.md](/concepts/process/syscall.md) —— 系统调用从用户态到内核态的完整流程
- [../process/process-creation.md](/concepts/process/process-creation.md) —— fork 时 fd 表的复制与共享细节
- [../io/read-write-process.md](/concepts/io/read-write-process.md) —— `read()` + `write()` 10 层完整调用链
