2.3 open、read、write 与 writeback
| 中文 | English | 目录 |
第二章:FUSE 代码实现分析 · 第 3/6 篇
本文沿 fs/fuse/file.c 的 VFS 操作表分析文件打开、数据路径选择、缓存读写、direct I/O、writeback、fsync 和 release。关键不是背诵函数,而是弄清路径选择发生在哪一层,以及部分完成如何向上返回。
1. 从 fuse_file_operations 进入
普通文件的主要入口为:
.open = fuse_open
.read_iter = fuse_file_read_iter
.write_iter = fuse_file_write_iter
.mmap = fuse_file_mmap
.fsync = fuse_fsync
.flush = fuse_flush
.release = fuse_release
fuse_file_aops 则负责页缓存的 read folio、readahead、writepages、dirty/writeback 协作。系统调用入口和后台页缓存入口会在不同时间发出相同类型的协议 I/O。
2. OPEN 和 struct fuse_file
fuse_open() 最终构造 FUSE_OPEN,daemon 返回 fuse_open_out,其中最重要的是 fh 与 open flags。内核为这次打开建立 struct fuse_file,挂到 VFS struct file 私有数据上。
fuse_file 通常承载:
- daemon
fh; - open flags 与 poll 状态;
- writeback/异步 I/O 需要的引用;
- release 参数,保证最后能关闭 daemon handle;
- 与 passthrough 或其他扩展路径相关的状态。
一个 inode 可以对应多个 fuse_file,因此 per-open 决策不能塞进 inode 全局状态。
3. open flags 如何影响数据路径
daemon 可以在 OPEN 响应中请求 direct I/O、keep cache 等行为。内核还会结合连接能力、inode 是否 DAX、是否建立 passthrough backing file 等条件做最终选择。
路径选择概念上为:
read_iter/write_iter
-> inode 使用 DAX? -> fuse_dax_*_iter
-> 本次 open 为 direct I/O? -> fuse_direct_*_iter
-> 可用 passthrough? -> backing file I/O
-> 否则 -> page-cache path
实际判断还包含 mmap、并行写、同步标志和连接 feature,阅读时以 fuse_file_read_iter()、fuse_file_write_iter() 中的分支顺序为准。
4. 缓存读路径
fuse_cache_read_iter() 进入通用 filemap 读路径。已有 folio 可直接拷贝;缺页触发 fuse_file_aops,再通过 fuse_send_read() 形成 FUSE_READ 请求。
read(2)
-> fuse_file_read_iter
-> fuse_cache_read_iter
-> filemap_read
-> cache hit:复制到用户缓冲区
-> cache miss:read_folio/readahead
-> FUSE_READ
-> 填充 folio
readahead 可以使 daemon 收到的范围大于应用当前请求;这属于正常行为。
5. 缓存写路径
fuse_cache_write_iter() 把数据写入页缓存,处理 inode size、时间戳、写入串行化和 direct-I/O 冲突。启用 writeback cache 时,系统调用成功不意味着 daemon 已收到写请求。
随后 writepages 可能:
- 合并多个小写;
- 把一个大写拆成多次
FUSE_WRITE; - 在内存回收或周期回写线程中执行;
- 以与原 syscall 不同的时间和线程上下文运行;
- 把错误延迟到
fsync、close 或映射错误报告。
daemon 必须按文件偏移处理请求,不能依赖收到请求的顺序等于应用写入顺序。
6. Direct I/O 主循环
fuse_direct_io() 是 direct I/O 的核心循环。它根据 max_read/max_write、页数和迭代器剩余长度切分请求,pin 用户页或组织参数,然后调用发送读写的帮助函数。
伪代码可简化为:
while (iov_iter_count(iter)) {
nbytes = choose_request_size(iter, limits);
nres = send_one_read_or_write(pos, nbytes);
if (nres < 0)
break;
total += nres;
pos += nres;
if (nres != nbytes)
break; /* short I/O */
}
return total ? total : error;
真实代码还处理异步完成、页释放、脏页标记、append、共享锁和并行 direct write。
7. 部分完成优先于后续错误
Linux 迭代式 I/O 的常见约定是:前几个子请求已传输数据、后一个子请求失败时,向用户返回正的已完成字节数。错误只有在零字节完成时才直接返回。
例如 8 MiB 读被拆成四个 2 MiB 子操作:
第 1 个:2 MiB 成功
第 2 个:2 MiB 成功
第 3 个:-ETIMEDOUT
系统调用结果:4 MiB,而不是 -ETIMEDOUT
这不是丢错误,而是 POSIX 风格的 short I/O:调用者应该处理 4 MiB,再次调用读取余下部分。它也意味着“看到 timeout 才把原请求整体改走另一条路径”通常不可实现,因为 timeout 可能已被正的部分结果遮蔽。
8. 读写的 short I/O 差异
对读而言,短读可能是 EOF,也可能是 daemon 返回少于请求的数据;上层需要根据当前位置和文件大小判断。对写而言,正的短写表示前缀已经产生副作用,重试必须从剩余后缀开始。
因此 fallback 代码必须同时保存:
- 原始位置;
iov_iter已消费量;- 实际提交与实际完成量;
- 是否存在尚未确定结果的异步请求;
- inode size、mtime 与 dirty 状态是否已经改变。
9. FLUSH、FSYNC 与 RELEASE
三者语义不同:
FLUSH与某次 file descriptor close 相关,可能执行多次;FSYNC要等待相关脏数据并向 daemon 请求持久化;RELEASE对应 daemon open handle 生命周期结束,通常只在最后引用消失时发送。
fuse_release_common() 要与未完成写入、异步请求和 handle 引用协调。不能因为 VFS release 已进入,就立即释放后续完成回调仍会访问的状态。
10. writeback 错误传播
后台写失败时,原始 write() 可能早已返回。错误会记录到 mapping/error sequence,并在后续 fsync() 或相关同步点报告。daemon 应返回稳定、可诊断的 errno;内核调用者则不能假设 close 一定是唯一错误出口。
11. 阅读与调试方法
分析一次 I/O 时建议同时记录:
- VFS 入口和
kiocbflags; - inode/open 上的 DAX、direct、passthrough、cache 标志;
iov_iter初始与最终 count;- 每个 FUSE 子请求的 offset、size、unique 和结果;
- 系统调用最终返回值;
- 是否还有 writeback 或异步完成在后台运行。
只观察一个 FUSE_READ/WRITE 无法重建完整 syscall。
12. 本文小结
file.c 在 DAX、direct I/O、passthrough 和页缓存之间分派。缓存路径受 readahead/writeback 驱动;direct path 自己切分请求。只要前缀已完成,后续 errno 通常会被正的字节数遮蔽,这一点是设计 timeout fallback 时最重要的返回值约束。
| 上一篇:路径名与元数据操作 | 下一篇:请求生命周期与 /dev/fuse 传输 |