socket 与 VFS —— socket 为什么是一个"文件"
前两篇 vfs-overview 和 vfs-file-operations 讲了 VFS 的架构和
file_operationsvtable。本篇聚焦一个具体案例:socket 怎么借 VFS 之力实现read()/write()/poll()/close()——这是理解 epoll 统一事件驱动、sendfile 零拷贝、fcntl 非阻塞控制的前提。
socket 能通过 read() / write() / poll() 操作,根源在于 Linux 的 VFS(Virtual File System) 抽象——一切皆文件。VFS 为所有文件类型(ext4、设备文件、pipe、socket)提供统一的系统调用入口,内核通过 函数指针表分发 到各自的实现(完整的 read() 调用链从 VFS → page cache → 块层 → 驱动 → DMA 的 10 层路径详见 ../io/read-write-process)。
一、socket 对应两套 vtable
socket 不是只挂一张 file_operations——它在协议栈层还有第二张 proto_ops:

| vtable | 所属层 | 指向对象 | 职责 |
|---|---|---|---|
file_operations | VFS 层 | socket_file_ops | 文件语义:read / write / poll / mmap / ioctl |
proto_ops | 协议族层 | inet_stream_ops | 协议语义:bind / listen / accept / sendmsg / recvmsg |
分工:VFS 看到的是一个"文件",所以 read() / write() / close() 进 file_operations。但 bind() / listen() / accept() 不是文件语义——普通文件没有"绑定端口"和"监听连接"的概念。这些操作不能塞进 read() / write(),所以 socket 额外有一套 proto_ops 来处理专属语义。
二、socket_file_ops 逐字段解析
| 字段 | 实际指向 | 作用 |
|---|---|---|
.read | sock_read_iter | read() → 组装 msghdr → sock->ops->recvmsg |
.write | sock_write_iter | write() → 组装 msghdr → sock->ops->sendmsg |
.poll | sock_poll | poll() / epoll_ctl() → 检查 sk_receive_queue 和 sk_wq |
.mmap | sock_mmap | mmap() → 协议栈不支持则返回 -ENODEV(文件 mmap 原理见 ../elf/mmap) |
.ioctl | sock_ioctl | 命令分流:FIONBIO → 改 file->f_flags;网络 SIOC* → 进协议栈 |
.llseek | no_llseek | socket 无偏移量概念,直接拒绝 |
.release | sock_close | close() → sock->ops->release → TCP 四次挥手 |
三、一条 read() 的完整分发链

图析:一条
read()经过了 3 层函数指针跳转(file_operations → proto_ops → proto),每一层都往下沉淀一层语义:(1) VFS 层——ksys_read管"这个 fd 是不是一个可读的文件",通过file->f_op->read进入 socket 文件操作;(2) proto_ops 层——inet_recvmsg管"这是 TCP 还是 UDP",做地址族相关处理(checksum / IP 分片重组);(3) proto 层——tcp_recvmsg管"TCP 怎么收数据",从sk_receive_queue取sk_buff,最后copy_to_user把数据拷到用户缓冲区。理解这三层分发是理解 epoll、sendfile、splice 能在 socket 上工作的大前提。
四、close() 的引用计数语义
close(sockfd) 不直接释放 socket——VFS 先走 __fput,file->f_count 减 1,只有归零才调 file->f_op->release = sock_close → inet_release → TCP 状态机推进(ESTABLISHED → FIN_WAIT1 → 四次挥手)。

图析:
close()不等于"关闭 TCP 连接"——它先把引用计数减 1,只有归零才真正触发四次挥手。dup()/fork()后父子共享同一个struct file,f_count = 2,任何一方 close 都不影响连接,双方都 close 才断开。这解释了为什么多进程服务器需要注意 fd 泄露——子进程忘了 close 父进程传过来的 fd,父 close 后连接不会断。
五、"一切皆文件"为 socket 带来的三个好处
好处一:统一事件驱动
epoll / select / poll 监视任何 fd(pipe、socket、eventfd、timerfd、signalfd),不需要为每种 I/O 源写不同的多路复用逻辑。epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, ...) 之所以能工作,就是因为 sockfd 背后有 file->f_op->poll(epoll 原理详见 ../network/epoll,线程模型详见 ../network/thread-models)。
好处二:零拷贝的 fd 语义
sendfile(out_fd, in_fd, ...) 要求两个参数都是 fd——in_fd 是普通文件、out_fd 是 socket,VFS 让它们共享同一套 fd 管理,否则 sendfile 需要为 socket 单独设计接口(零拷贝技术详见 ../network/zero-copy 和 ../network/zero-copy-deep)。
好处三:fcntl 统一控制
fcntl(sockfd, F_SETFL, O_NONBLOCK) 对所有 fd 类型有效——O_NONBLOCK 存在 file->f_flags 里,VFS 层统一管理,协议栈通过 file->f_flags & O_NONBLOCK 判断是否阻塞等待。socket 不需要自己存一份"非阻塞标志"。
六、延伸阅读
- vfs-overview — VFS 架构全景:四大对象、多态分发
- vfs-file-operations —
file_operationsvtable 逐字段详解 - socket-kernel-internals — socket 内核实现:
socket()和bind()如何建立 VFS+socket 双层结构 - epoll — epoll 原理:红黑树 + 就绪队列
- zero-copy / zero-copy-deep — 零拷贝技术
- read-write-process —
read()从用户态到 DMA 的完整 10 层调用链 - files-struct — 进程 fd 表(
files_struct)全景 - mmap — mmap 原理:VFS → VMA → 缺页
总结
VFS 是 socket 的"身份证"——它让 socket 复用 read / write / poll / close / fcntl 的全部文件基础设施,实际网络语义通过两套 vtable(file_operations → proto_ops → proto)的三层分发完成。file->private_data → socket → sock 这条指针链,就是从 VFS 世界进入网络世界的"任意门"。