# socket 与 VFS —— socket 为什么是一个"文件"

> 前两篇 [vfs-overview.md](/concepts/vfs/vfs-overview.md) 和 [vfs-file-operations.md](/concepts/vfs/vfs-file-operations.md) 讲了 VFS 的架构和 `file_operations` vtable。本篇聚焦一个具体案例：**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.md`](/concepts/io/read-write-process.md)）。

## 一、socket 对应两套 vtable

socket 不是只挂一张 `file_operations`——它在协议栈层还有第二张 `proto_ops`：

```plantuml
@startuml
left to right direction
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<vfs>> #BBDEFB
  BorderColor<<vfs>>     #1976D2
  BackgroundColor<<proto>> #FFECB3
  BorderColor<<proto>>   #F9A825
}
rectangle "**file_operations**\n(VFS 层)\n─────────────────\n.read  = sock_read_iter\n.write = sock_write_iter\n.poll  = sock_poll\n.mmap  = sock_mmap\n.ioctl = sock_ioctl\n.llseek = no_llseek\n.release = sock_close\n.open  = sock_no_open" <<vfs>> as FOPS
rectangle "**proto_ops**\n(协议族层)\n─────────────────\n.bind    = inet_bind\n.listen  = inet_listen\n.accept  = inet_accept\n.connect = inet_stream_connect\n.sendmsg = inet_sendmsg\n.recvmsg = inet_recvmsg\n.shutdown = inet_shutdown\n.setsockopt = sock_common_setsockopt" <<proto>> as POPS
FOPS -right-> POPS : file_operations 内部\n调用 sock->ops->xxx()
@enduml
```

| 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.md`](/concepts/elf/mmap.md)） |
| `.ioctl` | `sock_ioctl` | 命令分流：`FIONBIO` → 改 `file->f_flags`；网络 `SIOC*` → 进协议栈 |
| `.llseek` | `no_llseek` | socket 无偏移量概念，直接拒绝 |
| `.release` | `sock_close` | `close()` → `sock->ops->release` → TCP 四次挥手 |

## 三、一条 `read()` 的完整分发链

```plantuml
@startuml
skinparam shadowing false
skinparam participant {
  BackgroundColor<<u>> #C8E6C9
  BorderColor<<u>>     #388E3C
  BackgroundColor<<fo>> #BBDEFB
  BorderColor<<fo>>    #1976D2
  BackgroundColor<<po>> #FFECB3
  BorderColor<<po>>    #F9A825
  BackgroundColor<<pr>> #E1BEE7
  BorderColor<<pr>>    #7B1FA2
}
participant "用户进程" <<u>> as U
participant "VFS 层\nksys_read" <<fo>> as VFS
participant "socket_file_ops\nsock_read_iter" <<fo>> as FOPS
participant "inet_stream_ops\ninet_recvmsg" <<po>> as POPS
participant "tcp_prot\ntcp_recvmsg" <<pr>> as PROTO
U -> VFS : **read(sockfd, buf, len)**
VFS -> VFS : fdget(fd) → struct file\n检查 f_mode & FMODE_READ
VFS -> FOPS : **file->f_op->read**\n= sock_read_iter()
FOPS -> FOPS : 从 file->private_data\n拿到 struct socket
FOPS -> FOPS : 组装 msghdr (含用户 buf 地址)
FOPS -> POPS : **sock->ops->recvmsg**\n= inet_recvmsg()
POPS -> POPS : 地址族相关处理\n(checksum / IP 分片重组)
POPS -> PROTO : **sk->sk_prot->recvmsg**\n= tcp_recvmsg()
PROTO -> PROTO : 从 sk_receive_queue 取 sk_buff
PROTO -> PROTO : copy_to_user(buf, ..., len)
PROTO --> POPS : return 已拷贝字节数
POPS --> FOPS : return
FOPS --> VFS : return
VFS --> U : return
note bottom of PROTO : 3 层函数指针跳转\nfile_operations → proto_ops → proto\n每一层沉淀一层语义
@enduml
```

> **图析**：一条 `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` → 四次挥手）。

```plantuml
@startuml
skinparam shadowing false
skinparam participant {
  BackgroundColor<<u>> #C8E6C9
  BorderColor<<u>>     #388E3C
}
participant "父进程\nclose(sockfd)" <<u>> as PARENT
participant "子进程\nclose(sockfd)" <<u>> as CHILD
participant "VFS\n__fput" as FPUT
participant "TCP 状态机" as TCP
PARENT -> FPUT : close() → __fput\nf_count: 2 → 1
FPUT -> FPUT : f_count > 0, 不调 release
FPUT --> PARENT : 返回（TCP 连接仍存活）
CHILD -> FPUT : close() → __fput\nf_count: 1 → 0
FPUT -> FPUT : f_count == 0 !
FPUT -> TCP : file->f_op->release = sock_close\n→ inet_release → FIN_WAIT1
TCP -> TCP : 四次挥手...
FPUT --> CHILD : 返回
note bottom of TCP
  fork() / dup() 后父子共享同一个 file
  f_count = 2 → 双方都 close 才断开
  这就是 struct file 引用计数的价值
@end note
@enduml
```

> **图析**：`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.md`](/concepts/network/epoll.md)，线程模型详见 [`../network/thread-models.md`](/concepts/network/thread-models.md)）。

### 好处二：零拷贝的 fd 语义

`sendfile(out_fd, in_fd, ...)` 要求两个参数都是 fd——`in_fd` 是普通文件、`out_fd` 是 socket，VFS 让它们共享同一套 fd 管理，否则 sendfile 需要为 socket 单独设计接口（零拷贝技术详见 [`../network/zero-copy.md`](/concepts/network/zero-copy.md) 和 [`../network/zero-copy-deep.md`](/concepts/network/zero-copy-deep.md)）。

### 好处三：`fcntl` 统一控制

`fcntl(sockfd, F_SETFL, O_NONBLOCK)` 对所有 fd 类型有效——`O_NONBLOCK` 存在 `file->f_flags` 里，VFS 层统一管理，协议栈通过 `file->f_flags & O_NONBLOCK` 判断是否阻塞等待。socket 不需要自己存一份"非阻塞标志"。

## 六、延伸阅读

- [vfs-overview.md](/concepts/vfs/vfs-overview.md) — VFS 架构全景：四大对象、多态分发
- [vfs-file-operations.md](/concepts/vfs/vfs-file-operations.md) — `file_operations` vtable 逐字段详解
- [../network/socket-kernel-internals.md](/concepts/network/socket-kernel-internals.md) — socket 内核实现：`socket()` 和 `bind()` 如何建立 VFS+socket 双层结构
- [../network/epoll.md](/concepts/network/epoll.md) — epoll 原理：红黑树 + 就绪队列
- [../network/zero-copy.md](/concepts/network/zero-copy.md) / [../network/zero-copy-deep.md](/concepts/network/zero-copy-deep.md) — 零拷贝技术
- [../io/read-write-process.md](/concepts/io/read-write-process.md) — `read()` 从用户态到 DMA 的完整 10 层调用链
- [../process/task-resources/files-struct.md](/concepts/process/task-resources/files-struct.md) — 进程 fd 表（`files_struct`）全景
- [../elf/mmap.md](/concepts/elf/mmap.md) — mmap 原理：VFS → VMA → 缺页

## 一句话总结

**VFS 是 socket 的"身份证"——它让 socket 复用 `read` / `write` / `poll` / `close` / `fcntl` 的全部文件基础设施，实际网络语义通过两套 vtable（`file_operations` → `proto_ops` → `proto`）的三层分发完成。`file->private_data → socket → sock` 这条指针链，就是从 VFS 世界进入网络世界的"任意门"。**
