﻿# vfs-file-operations.md —— `file_operations` vtable 逐字段详解

> 前一篇 [vfs-overview.md](/concepts/vfs/vfs-overview.md) 讲 VFS 架构全景和四大对象，下一篇 [vfs-and-socket.md](/concepts/vfs/vfs-and-socket.md) 讲 socket 怎么填这张表。本篇聚焦**这张 vtable 本身**：每个函数指针的签名、语义、哪些文件系统填了哪些不填。

## 一、`file_operations` 是什么

```c
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iopoll)(struct kiocb *kiocb, unsigned int flags);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    unsigned long mmap_supported_flags;
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,
                                       unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
    void (*show_fdinfo)(struct seq_file *m, struct file *f);
    // ... kernel 6.x 还有更多
};
```

这是一张 **~30 个函数指针**的表。但具体文件系统**只填它能支持的**——空指针即"不支持此操作"，VFS 返回 `-ENODEV` 或 `-EINVAL`。

## 二、核心字段逐行解析

### 2.1 `llseek` —— 偏移量操作

| 签名 | `loff_t (*llseek)(struct file *, loff_t, int whence)` |
|------|-------------------------------------------------------|
| 来源 | `lseek(fd, offset, SEEK_SET/SEEK_CUR/SEEK_END)` |
| 职责 | 修改 `file->f_pos`（当前读写位置） |
| ext4 | `generic_file_llseek` —— 支持 SEEK_DATA / SEEK_HOLE |
| pipe | `no_llseek` —— 管道是 FIFO，无"位置"概念 |
| socket | `no_llseek` —— 同上，返回 `-ESPIPE` |

### 2.2 `read` / `read_iter` —— 读操作

| 签名 | `ssize_t (*read_iter)(struct kiocb *, struct iov_iter *)` |
|------|-----------------------------------------------------------|
| 来源 | `read(fd, buf, count)` |
| 职责 | 从文件当前位置读取数据到用户缓冲区 |

> **`read` vs `read_iter`**：新版内核优先调 `read_iter`（基于 `iov_iter` 的迭代器风格，支持 vectored I/O 和 O_DIRECT 对齐），只有 `read_iter` 为 NULL 时才回退到 `read`。

| 文件类型 | 实现 | 行为 |
|---------|------|------|
| ext4 | `generic_file_read_iter` | 查 page cache → 命中则拷贝、未命中则触发磁盘读 |
| pipe | `pipe_read`（不设 `read_iter`，用旧 `read`） | 从 pipe 环形缓冲区读，没数据时阻塞或返回 `EAGAIN` |
| socket | `sock_read_iter` | 组装 `msghdr` → `sock->ops->recvmsg` → 协议栈 |

### 2.3 `write` / `write_iter` —— 写操作

| 签名 | `ssize_t (*write_iter)(struct kiocb *, struct iov_iter *)` |
|------|-----------------------------------------------------------|
| 来源 | `write(fd, buf, count)` |
| 职责 | 从用户缓冲区写数据到文件 |
| 文件类型 | 实现 | 行为 |
|---------|------|------|
| ext4 | `generic_file_write_iter` | 写 page cache（writeback 模式）→ 标记脏页 → 后台刷盘 |
| pipe | `pipe_write` | 写到 pipe 缓冲区，满时阻塞 |
| socket | `sock_write_iter` | 组装 `msghdr` → `sock->ops->sendmsg` → 协议栈 → 网卡 |

### 2.4 `poll` —— 事件检测

| 签名 | `__poll_t (*poll)(struct file *, struct poll_table_struct *)` |
|------|---------------------------------------------------------------|
| 来源 | `poll()` / `select()` / `epoll_ctl(EPOLL_CTL_ADD)` |
| 职责 | 返回此文件当前的就绪事件（`EPOLLIN` / `EPOLLOUT` / `EPOLLERR` ...），并在 wait queue 上注册回调 |

这是**事件驱动 I/O 的基石**——`epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev)` 之所以能工作，是因为内核调用 `sockfd` 背后 `file->f_op->poll` = `sock_poll`。`sock_poll` 检查 `sk_receive_queue` 非空（可读）、`sk_wmem_alloc < sk_sndbuf`（可写）、`sk_err`（错误），并注册回调到 `sk_sleep` 等待队列——数据到达时唤醒 epoll。

| 文件类型 | 实现 |
|---------|------|
| ext4 | `ext4_file_poll` → 磁盘文件总是可读可写（除非文件锁） |
| pipe | `pipe_poll` → 缓冲区空 = 不可读，满 = 不可写 |
| socket | `sock_poll` → 检查 receive queue / send buffer / error |
| eventfd | `eventfd_poll` → counter > 0 = 可读 |
| timerfd | `timerfd_poll` → 定时器已触发 = 可读 |

### 2.5 `mmap` —— 内存映射

| 签名 | `int (*mmap)(struct file *, struct vm_area_struct *)` |
|------|------------------------------------------------------|
| 来源 | `mmap(addr, len, prot, MAP_SHARED, fd, offset)` |
| 职责 | 把文件内容映射到进程的虚拟地址空间，建立 VMA |

```plantuml
@startuml
skinparam shadowing false
skinparam participant {
  BackgroundColor<<U>> #C8E6C9
  BorderColor<<U>>     #388E3C
  BackgroundColor<<k>> #BBDEFB
  BorderColor<<k>>     #1976D2
}
participant "用户进程\nmmap(fd, ...)" <<U>> as U
participant "ksys_mmap" as MMAP
participant "file->f_op->mmap" as FOP
participant "ext4" as EXT4 #FFF9C4
participant "socket" as SK #FFCDD2
U -> MMAP : mmap(fd, 4096, PROT_READ, MAP_SHARED, fd, 0)
MMAP -> MMAP : 分配 VMA，设置 vm_flags
MMAP -> FOP : file->f_op->mmap(file, vma)
FOP -> EXT4 : ext4_file_mmap(vma)\n设置 vma->vm_ops = &ext4_file_vm_ops\n缺页时走 ext4_read_folio → 读磁盘
FOP -> SK : sock_mmap(vma)\n**socket 不支持映射**\nreturn -ENODEV
note bottom of SK : 没法把网络流映射到内存\n数据是 FIFO 的、可以丢的、有生命周期的\n不是随机可寻址的持久数据
@enduml
```

> **图析**：`mmap` 的签名里有一个 `struct vm_area_struct *vma`——这是 VFS 和内存管理子系统的"握手点"。文件系统在 `mmap` 回调里设置 `vma->vm_ops`（`fault` / `page_mkwrite` / `writable` 等），之后缺页中断发生时内核调用 `vma->vm_ops->fault` 来真正读数据。详情见 [`../elf/mmap.md`](/concepts/elf/mmap.md)。

### 2.6 `ioctl` / `unlocked_ioctl` —— 万能控制接口

| 签名 | `long (*unlocked_ioctl)(struct file *, unsigned int cmd, unsigned long arg)` |
|------|-------------------------------------------------------------------------------|
| 来源 | `ioctl(fd, cmd, arg)` |
| 职责 | 执行设备/文件系统特定的控制命令——VFS 自己不解析 `cmd`，原样传递给驱动 |

这是"万能工具箱"——每个文件系统/驱动定义自己的命令码，VFS 只做转发：

| 文件类型 | 典型 ioctl 命令 |
|---------|----------------|
| socket | `FIONBIO`（设非阻塞）、`SIOCGIFADDR`（取网卡 IP）、`TIOCOUTQ`（取发送队列长度） |
| ext4 | `FS_IOC_GETFLAGS`（取 inode 属性）、`FS_IOC_FSSETXATTR` |
| 终端 (tty) | `TCGETS` / `TCSETS`（termios）、`TIOCGWINSZ`（窗口大小） |
| 磁盘 | `BLKGETSIZE64`（取块设备大小）、`HDIO_GETGEO`（取磁盘几何） |

### 2.7 `open` —— 文件被打开时

| 签名 | `int (*open)(struct inode *, struct file *)` |
|------|----------------------------------------------|
| 来源 | `open(path, flags, mode)` → `do_sys_open` → `path_openat` |
| 职责 | 文件系统在 `struct file` 创建后做私有初始化，比如分配 private_data |

`open` 和 `release` 通常成对出现：

- **`open`**：`struct file` 刚分配好、`inode` 已知、`file->f_op` 已指向该文件系统的 vtable。此时文件系统可以分配自己的私有数据并挂到 `file->private_data`。
- 如果文件系统没设 `open`，VFS 就什么都不做——`open` 不是必须的（普通文件系统通常不设，直接跳过）。

### 2.8 `release` —— 文件被关闭时（引用计数归零）

| 签名 | `int (*release)(struct inode *, struct file *)` |
|------|------------------------------------------------|
| 来源 | `close(fd)` → `__fput`（只有 `file->f_count` 归零时才调） |
| 职责 | 释放 `open` 时分配的资源，做清理 |

**关键语义**：`release` 不等于 `close(fd)`——`close(fd)` 先把 `file->f_count` 减 1，只有减到 0（没有任何进程/线程还在引用这个 file）才调 `release`。fork/dup 之后 `f_count > 1`，父 close 只减计数不触发 release，子 close 才真正释放。

```plantuml
@startuml
skinparam shadowing false
skinparam participant {
  BackgroundColor<<U>> #C8E6C9
  BorderColor<<U>>     #388E3C
  BackgroundColor<<k>> #BBDEFB
  BorderColor<<k>>     #1976D2
}
participant "close(fd)" <<U>> as C
participant "__close_fd" as CLOSE
participant "__fput" as FPUT
participant "release" as REL
C -> CLOSE : close(fd)
CLOSE -> CLOSE : 从 files_struct 中移除 fd
CLOSE -> FPUT : f_count 减 1
FPUT -> FPUT : f_count == 0 ?
FPUT -> REL : **是** ⇒ file->f_op->release(inode, file)
FPUT -> FPUT : f_count > 0 ?
FPUT -> C : **否** ⇒ 直接返回，不调 release
note right of FPUT
  fork() 后父子共享 file
  f_count = 2 → 只有双方都 close
  才触发真正的 release
end note
@enduml
```

> **图析**：`release` 的延迟调用是 VFS 引用计数的核心机制。socket 利用这一点——`sock_close` → `inet_release` → TCP 状态机推进（四次挥手），只有双方都 close 才真正断开连接。详见 [vfs-and-socket.md](/concepts/vfs/vfs-and-socket.md)。

### 2.9 其他重要字段

| 字段 | 来源 | 语义 |
|------|------|------|
| `fsync` | `fsync(fd)` / `fdatasync(fd)` | 把脏页刷到磁盘，socket/pipe 通常不设（返回 `-EINVAL`） |
| `fasync` | `fcntl(fd, F_SETFL, O_ASYNC)` | 异步 I/O 通知（SIGIO 信号），socket 用 `sock_fasync` |
| `lock` / `flock` | `flock()` / `fcntl(F_SETLK)` | 文件锁，通常只有磁盘文件系统实现 |
| `splice_read` / `splice_write` | `splice()` / `sendfile()` | 零拷贝——数据不经过用户态，pipe→socket 或 file→socket 直接在内核传递 |
| `sendpage` | `sendfile()` 内部 | 把 page cache 的页直接发给 socket，零拷贝核心路径 |
| `fallocate` | `fallocate()` / `posix_fallocate()` | 预分配磁盘空间 |
| `iterate` / `iterate_shared` | `getdents64()` → `ls` / `readdir` | 读取目录项，只有目录文件才设，普通文件和 pipe/socket 不设 |
| `iopoll` | `io_uring` IORING_SETUP_IOPOLL | 用户态轮询 I/O 完成，避免 IRQ 开销（仅 NVMe 等支持） |

## 三、各文件系统的 `file_operations` 对比

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<yes>> #C8E6C9
  BorderColor<<yes>>     #388E3C
  BackgroundColor<<no>>  #FFCDD2
  BorderColor<<no>>      #C62828
  BackgroundColor<<hdr>> #E1BEE7
  BorderColor<<hdr>>     #7B1FA2
}
rectangle "字段" <<hdr>> as HDR
rectangle "ext4" <<hdr>> as H_EXT4
rectangle "pipe" <<hdr>> as H_PIPE
rectangle "socket" <<hdr>> as H_SK
rectangle "procfs" <<hdr>> as H_PROC
rectangle "eventfd" <<hdr>> as H_EFD
rectangle "read_iter / read" as R1
rectangle " ✓" <<yes>> as R2
rectangle " ✓ (pipe_read)" <<yes>> as R3
rectangle " ✓ (sock_read_iter)" <<yes>> as R4
rectangle " ✓ (seq_read)" <<yes>> as R5
rectangle " ✓" <<yes>> as R6
rectangle "write_iter / write" as W1
rectangle " ✓" <<yes>> as W2
rectangle " ✓ (pipe_write)" <<yes>> as W3
rectangle " ✓ (sock_write_iter)" <<yes>> as W4
rectangle " ✓ (seq_write)" <<yes>> as W5
rectangle " ✓" <<yes>> as W6
rectangle "poll" as P1
rectangle " ✓" <<yes>> as P2
rectangle " ✓ (pipe_poll)" <<yes>> as P3
rectangle " ✓ (sock_poll)" <<yes>> as P4
rectangle " ✓" <<yes>> as P5
rectangle " ✓ (eventfd_poll)" <<yes>> as P6
rectangle "mmap" as M1
rectangle " ✓" <<yes>> as M2
rectangle " ✗" <<no>> as M3
rectangle " ✗" <<no>> as M4
rectangle " ✗" <<no>> as M5
rectangle " ✗" <<no>> as M6
rectangle "llseek" as L1
rectangle " ✓" <<yes>> as L2
rectangle " ✗" <<no>> as L3
rectangle " ✗" <<no>> as L4
rectangle " ✓" <<yes>> as L5
rectangle " ✗" <<no>> as L6
rectangle "ioctl" as I1
rectangle " ✓" <<yes>> as I2
rectangle " ✗" <<no>> as I3
rectangle " ✓ (sock_ioctl)" <<yes>> as I4
rectangle " ✗" <<no>> as I5
rectangle " ✗" <<no>> as I6
note bottom of M4
  不支持的操作用返回 -ENODEV 或 -ESPIPE
  VFS 不崩溃，用户态收到 errno
end note
R1 -[hidden]right-> R2
R2 -[hidden]right-> R3
R3 -[hidden]right-> R4
R4 -[hidden]right-> R5
R5 -[hidden]right-> R6
@enduml
```

## 四、`f_op` vs `private_data` —— 行为与状态分离的设计模式

`file_operations` 只解决了一个问题：**这个 fd 能做什么操作**。但操作需要操作的对象——数据在哪、计数器是多少、等待队列挂在哪——这些问题 `f_op` 一概不答。答案在 `file->private_data`。

### 核心类比

```bash
file_operations  ←→  类的方法（member functions），一类文件共用一套
private_data     ←→  实例的成员变量（member variables），每个 fd 独立一份
```

| | `f_op`（file_operations） | `private_data` |
|---|---|---|
| **粒度** | 类型级别——所有 eventfd 共享同一份 | 实例级别——每个 fd 独立一份 |
| **内容** | 函数指针：read/write/poll/release/... | 运行时状态：计数器、缓冲区、等待队列、红黑树 |
| **生命周期** | 文件类型注册时确定，全局不变 | open 时分配，release 时释放 |
| **共享性** | 同类型全部 fd 共用 | 每个 fd 独享（pipe 读/写两端指向同一份是例外） |

### 以 eventfd 为例

```c
// 两个独立的 eventfd，f_op 完全相同
fd1 = eventfd(0, 0);  // file1->f_op = &eventfd_fops（全局唯一）
fd2 = eventfd(0, 0);  // file2->f_op = &eventfd_fops（同一个结构体！）
// 但是状态独立——靠 private_data 区分
file1->private_data → ctx1：计数器 = 10
file2->private_data → ctx2：计数器 = 0
// read(fd1)：同一套 f_op->read 代码
//           通过 file->private_data 拿到 ctx1 → 读到 10
// read(fd2)：同一套代码
//           通过 file->private_data 拿到 ctx2 → 读到 0
```

### 以 pipe 为例——private_data 还可以"共享"

```c
int pipefd[2];
pipe(pipefd);
file_read->f_op  = &pipe_read_fops;   // 读端操作集
file_write->f_op = &pipe_write_fops;  // 写端操作集（和读端不同！）
// 但两者的 private_data 指向同一个 pipe_inode_info（环形缓冲区）
file_read->private_data == file_write->private_data  // 同一块！
```

**这里体现了两层语义**：
- `f_op` 区分"这个 fd 是读端还是写端"（允许读还是允许写）
- `private_data` 保证"读端和写端共享同一块环形缓冲区"

> **这也就是为什么所有文件操作函数的第一行代码都是 `xxx_ctx *ctx = file->private_data;`**——`f_op` 只负责路由到正确的函数，`private_data` 负责提供这个 fd 独有的状态。函数代码全局共用，数据每个 fd 隔离。FUSE、socket、字符设备全部遵循这个模式。详见 [fuse.md](/concepts/vfs/fuse.md)。

## 五、延伸阅读

- [vfs-overview.md](/concepts/vfs/vfs-overview.md) — VFS 架构全景：四大对象、多态分发机制
- [vfs-and-socket.md](/concepts/vfs/vfs-and-socket.md) — socket 与 VFS 的深度融合
- [fuse.md](/concepts/vfs/fuse.md) — FUSE 用户态文件系统：多挂载点隔离、fuse_conn、三层分层
- [../io/read-write-process.md](/concepts/io/read-write-process.md) — `read()` 从用户态到 DMA 的完整 10 层调用链
- [../elf/mmap.md](/concepts/elf/mmap.md) — mmap 原理：VFS → VMA → 缺页
- [../process/task-resources/files-struct.md](/concepts/process/task-resources/files-struct.md) — 进程 fd 表结构

## 一句话总结

**`file_operations` 是 VFS 的"多态接口表"——约 30 个函数指针定义了一套文件操作协议，ext4/pipe/socket/procfs/eventfd 等各自填自己能支持的，不填的就是不支持；`read()`/`write()`/`poll()`/`mmap()`/`ioctl()` 等系统调用全部通过 `file->f_op->xxx()` 一跳分发，VFS 不关心底层是什么。`private_data` 则负责存放每个 fd 独立的运行时状态，是 f_op 函数代码和数据状态之间的"粘合指针"。**

