Linux-Kernel-Notes

Thinking in linux kernel

View on GitHub

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. OPENstruct fuse_file

fuse_open() 最终构造 FUSE_OPEN,daemon 返回 fuse_open_out,其中最重要的是 fh 与 open flags。内核为这次打开建立 struct fuse_file,挂到 VFS struct file 私有数据上。

fuse_file 通常承载:

一个 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 可能:

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 代码必须同时保存:

9. FLUSHFSYNCRELEASE

三者语义不同:

fuse_release_common() 要与未完成写入、异步请求和 handle 引用协调。不能因为 VFS release 已进入,就立即释放后续完成回调仍会访问的状态。

10. writeback 错误传播

后台写失败时,原始 write() 可能早已返回。错误会记录到 mapping/error sequence,并在后续 fsync() 或相关同步点报告。daemon 应返回稳定、可诊断的 errno;内核调用者则不能假设 close 一定是唯一错误出口。

11. 阅读与调试方法

分析一次 I/O 时建议同时记录:

  1. VFS 入口和 kiocb flags;
  2. inode/open 上的 DAX、direct、passthrough、cache 标志;
  3. iov_iter 初始与最终 count;
  4. 每个 FUSE 子请求的 offset、size、unique 和结果;
  5. 系统调用最终返回值;
  6. 是否还有 writeback 或异步完成在后台运行。

只观察一个 FUSE_READ/WRITE 无法重建完整 syscall。

12. 本文小结

file.c 在 DAX、direct I/O、passthrough 和页缓存之间分派。缓存路径受 readahead/writeback 驱动;direct path 自己切分请求。只要前缀已完成,后续 errno 通常会被正的字节数遮蔽,这一点是设计 timeout fallback 时最重要的返回值约束。

上一篇:路径名与元数据操作 下一篇:请求生命周期与 /dev/fuse 传输