vfs-file-operations.md —— file_operations vtable 逐字段详解
前一篇 vfs-overview 讲 VFS 架构全景和四大对象,下一篇 vfs-and-socket 讲 socket 怎么填这张表。本篇聚焦这张 vtable 本身:每个函数指针的签名、语义、哪些文件系统填了哪些不填。
一、file_operations 是什么
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) |
| 职责 | 从文件当前位置读取数据到用户缓冲区 |
readvsread_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 |
| pipe | pipe_write |
| socket | sock_write_iter |
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 |

图析:
mmap的签名里有一个struct vm_area_struct *vma——这是 VFS 和内存管理子系统的"握手点"。文件系统在mmap回调里设置vma->vm_ops(fault/page_mkwrite/writable等),之后缺页中断发生时内核调用vma->vm_ops->fault来真正读数据。详情见../elf/mmap。
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 才真正释放。

图析:
release的延迟调用是 VFS 引用计数的核心机制。socket 利用这一点——sock_close→inet_release→ TCP 状态机推进(四次挥手),只有双方都 close 才真正断开连接。详见 vfs-and-socket。
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 对比

四、f_op vs private_data —— 行为与状态分离的设计模式
file_operations 只解决了一个问题:这个 fd 能做什么操作。但操作需要操作的对象——数据在哪、计数器是多少、等待队列挂在哪——这些问题 f_op 一概不答。答案在 file->private_data。
核心类比
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 为例
// 两个独立的 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 还可以"共享"
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。
五、延伸阅读
- vfs-overview — VFS 架构全景:四大对象、多态分发机制
- vfs-and-socket — socket 与 VFS 的深度融合
- fuse — FUSE 用户态文件系统:多挂载点隔离、fuse_conn、三层分层
- read-write-process —
read()从用户态到 DMA 的完整 10 层调用链 - mmap — mmap 原理:VFS → VMA → 缺页
- files-struct — 进程 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 函数代码和数据状态之间的"粘合指针"。