进程如何感知 VFS —— 从 task_struct 到 file_operations 的全链路
上一篇 vfs-overview 讲 VFS 本身的多态分发机制,vfs-file-operations 拆解
file_operationsvtable 逐字段。本篇切换视角:站在进程的角度,看它怎么通过 fd 和系统调用与 VFS 交互——进程不"知道" VFS,但它的每一次文件操作都穿过 VFS。
一、一句话:进程看到的 vs 内核实际发生的
进程眼里文件操作很简单——一个整数 fd + 几个系统调用:
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 有五跳——每一跳都是通过一个指针字段完成的:

图析:五跳链路——
task_struct.files(指向打开文件表)→files_struct.fdt(指向 fd 数组)→fdtable.fd[3](数组第 3 项,即 fd=3)→struct file(打开文件实例,存偏移和标志)→file->f_op(指向file_operationsvtable)。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 没有任何关系。

图析:
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) |
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 在背后穿过五跳指针链到达真正的数据。

图析:
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 的引用计数意味着:"真正的清理"可能要等很久。

图析:
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 的连接拓扑。

图析:
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 —— VFS 架构全景:四大对象、多态分发机制
- vfs-file-operations ——
file_operationsvtable 逐字段拆解 - vfs-and-socket —— socket 与 VFS 深度融合:两套 vtable、
read()3 层分发 - files-struct —— 进程 fd 表(
files_struct)结构详解 - syscall —— 系统调用从用户态到内核态的完整流程
- process-creation —— fork 时 fd 表的复制与共享细节
- read-write-process ——
read()+write()10 层完整调用链