3. 缓存、I/O 模式与一致性模型
| [中文] | English | 教程目录 |
| 上一篇 | 下一篇 |
1. 三类缓存
1.1 dentry cache
dentry 缓存“父目录 + 名字 -> inode 或不存在”。成功 LOOKUP 的 entry_valid 控制 positive dentry;不存在的名字也可形成 negative dentry。
在有效期内,VFS path walk 可以不询问 daemon。有效期结束不等于立即删除,而是下次需要时重新验证。
1.2 attribute cache
inode 属性由 attr_valid 控制,包含:
- mode、uid、gid;
- size、blocks;
- atime、mtime、ctime;
- link count、设备号等。
属性缓存影响的不只是 stat(2)。陈旧 i_size 会影响 EOF、写扩展、mmap fault 和 truncate 判断;陈旧 mode/uid 可能影响内核侧 permission check。
1.3 data cache
cached I/O 使用 address_space 和 folio:
- read miss 发送 FUSE_READ;
- readahead 批量填充 folio;
- write 修改页缓存;
- writeback 把脏数据变成 FUSE_WRITE;
- mmap 可直接映射页缓存页面。
data cache 没有一个简单的 data_valid 秒数。它由 open flags、INIT flags、属性变化、显式 notification、writeback 状态和 inode invalidation 共同控制。
2. 缓存有效期是 lease,不是所有权转移
daemon 返回 timeout,相当于承诺:在有效期内,内核可以使用这份答案。如果后端可能被其他参与者修改,daemon 应选择以下策略之一:
- 使用短 timeout,接受周期性重新验证;
- 监听后端变化并发送 invalidation notification;
- 确保所有修改都经过 daemon,从源头维持一致性;
- 明确只提供弱一致性。
timeout 越长,元数据性能越好,但外部变化可见性越差。timeout 为 0 会增加往返,却不自动解决已经映射或正在使用的数据一致性。
3. 主动失效
daemon 可发送:
FUSE_NOTIFY_INVAL_ENTRY:失效父目录下的名字;FUSE_NOTIFY_DELETE:表达删除并帮助 dentry/inode 关系更新;FUSE_NOTIFY_INVAL_INODE:失效 inode 属性或数据范围;FUSE_NOTIFY_STORE/RETRIEVE:与页缓存交换数据;FUSE_NOTIFY_POLL:唤醒 poll waiter。
主动失效减少了“必须把 timeout 设得很短”的压力,但引入顺序问题:notification 与普通请求可以并发,daemon 必须定义某次修改、应答和失效消息之间的 happens-before 关系。
4. 四种主要 I/O 模式
4.1 Cached I/O
优点:
- 热数据不需要进入 daemon;
- readahead 和大 folio 可以合并请求;
- mmap 自然复用页缓存;
- 小读写可被内核合并。
代价:
- 外部后端修改可能不可见;
- writeback 错误可能延迟到 fsync/close;
- 数据、属性和后端状态存在多个时间点。
4.2 Direct I/O
FOPEN_DIRECT_IO 让该 open 实例绕过普通页缓存数据路径。它不意味着单个请求或零拷贝,而是 iov_iter 按连接限制拆成一个或多个 FUSE READ/WRITE。
优点是状态直接、适合 daemon 自己管理缓存;代价是每次 I/O 都跨边界,小随机 I/O 容易受调度开销支配。
4.3 Passthrough
passthrough 把数据操作交给 backing file,减少 daemon 数据面处理。控制面仍由 FUSE 管理,因此需要保证:
- backing file 与 FUSE nodeid 指向相同逻辑内容;
- cached 和 passthrough open 之间的模式切换有序;
- truncate、mmap、writeback 和权限不会绕过必要检查。
4.4 virtio-fs DAX
DAX 将文件区间映射到共享 window,使 guest CPU 经页表直接访问。它消除了普通 READ/WRITE 数据请求,但新增:
- window 槽位分配和回收;
FUSE_SETUPMAPPING/FUSE_REMOVEMAPPING;- DAX entry、页表和 GUP pin 生命周期;
- window 容量不足时的等待与抖动。
5. Writeback cache 的语义变化
未启用 writeback cache 时,daemon 更接近数据和属性事实的权威来源。启用后,guest 内核可以先接受写入并在稍后回写,因此:
- daemon 看到 WRITE 的时间晚于应用 write;
- 内核可能基于本地脏页维护 size 和 timestamps;
- READ 可能要看到尚未写到 daemon 后端的数据;
- FLUSH、FSYNC、RELEASE 的错误和顺序更重要;
- daemon 不应把自己的后端 size 无条件覆盖更新更晚的 guest size。
writeback cache 是协议语义变化,不只是性能 flag。
6. open flags 如何影响缓存
常见应答 flag:
| Flag | 含义 |
|---|---|
FOPEN_DIRECT_IO |
该 open 使用 direct-I/O 语义 |
FOPEN_KEEP_CACHE |
open 时保留已有页缓存 |
FOPEN_NONSEEKABLE |
文件不支持正常 seek |
FOPEN_CACHE_DIR |
允许缓存目录内容 |
FOPEN_STREAM |
流式文件语义 |
若没有 KEEP_CACHE,内核通常在 open 时失效旧页,以避免 daemon 外部变化留下陈旧数据。具体行为还取决于 writeback、inode I/O mode 和当前 mmap。
7. 一致性场景分析
场景 A:所有修改都经过一个 daemon
可以使用较长 timeout 和 writeback cache。daemon 仍需正确处理同一对象上的并行请求、rename 与 open handle。
场景 B:后端文件也被 host 进程修改
需要 inotify/fanotify 或后端机制驱动 invalidation,或者使用短 timeout/direct I/O。仅缩短 attr timeout 不能自动失效已经缓存的数据页。
场景 C:多个 guest 共享 virtio-fs 后端
每个 guest 有独立 VFS cache。host daemon 必须向各连接传播失效;DAX 映射还要考虑页表撤销和 pin。
场景 D:生成式或远程 namespace
negative dentry timeout 往往比 positive timeout 更敏感,因为新创建的远程对象可能被旧 negative cache 遮蔽。
8. 模式切换为何困难
同一 inode 上的 cached I/O、direct I/O、passthrough 和 DAX 可能共享:
i_size和 timestamps;- mmap 映射;
- 脏页或 DAX dirty entry;
- file locks;
- 在途异步请求。
切换前通常需要某种组合:等待在途 I/O、写回、失效页缓存、撤销页表映射、取得 inode/invalidate lock。只在某次请求失败时“直接换一条路径”可能破坏可见性或重复部分 I/O。
9. 配置检查表
- 分别定义 entry、negative entry、attribute 的有效期;
- 明确后端外部修改源和 notification 机制;
- 决定是否启用 writeback cache;
- 定义 direct I/O 与 mmap 是否允许共存;
- 为短 I/O、延迟错误和 fsync 错误设计应用语义;
- 记录 cache hit、FUSE request 和后端 request 三层指标;
- 压测随机访问、rename/unlink 并发、内存回收和 daemon 重启。
小结
FUSE 一致性是多个 lease、页缓存状态、open 模式和主动通知的组合。下一篇讨论这些状态在并发、信号、背压和失败下如何保持可控。
| 上一篇 | 下一篇 |