4. 并发、背压、安全与失败模型
| [中文] | English | 教程目录 |
| 上一篇 | 进入第二章 |
1. FUSE 天生是并发协议
多个进程可以同时操作同一挂载点;readahead、writeback、异步 I/O、FORGET 和 notification 又会产生系统调用之外的并发。unique 允许 daemon 区分和乱序完成请求。
daemon 不应假设:
- 请求按 syscall 顺序到达;
- 同一 inode 的请求只由一个线程处理;
- RELEASE 一定晚于所有可见协议消息;
- FORGET 意味着没有在途操作;
- INTERRUPT 后原请求必然不会完成。
2. 同步请求与后台请求
2.1 同步请求
调用线程构造请求并等待应答。等待过程中可能遇到:
- daemon 正常回复;
- 普通信号触发 INTERRUPT;
- fatal signal 尝试移除尚未发送的请求;
- channel abort;
- daemon 永久不回复。
一旦请求已经交给 daemon,内核不能简单释放其参数,因为 daemon 仍可能写回。请求引用计数、状态位和 waitqueue 共同保证生命周期。
2.2 后台请求
后台请求用于:
- readahead;
- writeback;
- async direct I/O;
- RELEASE 等不适合阻塞当前线程的生命周期消息。
后台请求通过 completion callback 归还 folio、file reference 和 I/O context。任何错误路径都必须最终调用 completion,否则会泄漏引用或永久保持 writeback 状态。
3. 背压
如果 daemon 消费速度低于生产速度,系统必须限制积压:
num_background < max_background
-> 可激活更多后台请求
num_background == max_background
-> channel blocked,后续请求留在 bg_queue
completion
-> 降低计数,flush 更多请求
congestion_threshold 让 VM/writeback/readahead 感知 FUSE 拥塞。它与 max_background 的区别是:前者用于提前降速,后者是硬的后台在途上限。
过大配置会增加内存、尾延迟和 abort 成本;过小会让 daemon/后端吃不满。应结合请求大小、daemon 线程数和后端并发能力调节。
4. 主要锁域
FUSE 不用一把全局锁保护全部状态。常见锁域包括:
| 锁/同步原语 | 保护对象 |
|---|---|
inode i_rwsem |
文件大小、写入、truncate、目录操作的 VFS 串行化 |
fiq->lock |
pending、interrupt、forget 和 input queue 状态 |
fpq->lock |
io 与 processing queue、传输复制状态 |
| channel background lock | background 计数、bg_queue、blocked |
fuse_inode 内部锁 |
attr version、write files、状态位 |
| mapping invalidate lock | page cache/DAX mapping 与回收、truncate |
| request waitqueue/refcount | 单个请求完成和释放 |
锁顺序比单把锁的功能更重要。daemon 自身若在处理请求时访问同一 FUSE mount,可能形成递归依赖;这也是部分 RELEASE 选择后台执行、writeback 标记潜在 reclaim deadlock 的原因。
5. INTERRUPT 不是强制取消
线程收到信号时有三种典型情况:
- 请求仍在 pending:内核可以从队列移除并结束;
- daemon 已读取:内核发送
FUSE_INTERRUPT(original_unique); - daemon 已完成但应答正在竞争:原应答或 interrupt 结果之一先改变状态。
daemon 对 INTERRUPT 可以:
- 成功取消并让原请求返回
-EINTR; - 返回
-EAGAIN,让内核稍后重试 interrupt; - 返回
-ENOSYS,内核记住不支持 interrupt; - 原操作已完成,正常返回原应答。
因此 application 收到信号不意味着后端操作一定未发生。涉及创建、写入等副作用时,daemon 和应用都要考虑“不确定完成”。
6. FORGET、删除与打开文件
Unix 允许文件 unlink 后继续通过已打开 fd 访问。对应到 FUSE:
- UNLINK 移除 namespace 名字;
- nodeid 可能仍有 lookup reference;
fuse_file和 fh 仍保持 open reference;- READ/WRITE 可继续使用 fh;
- 最后由 FORGET 和 RELEASE 分别归还两类引用。
daemon 必须把 namespace lifetime 和 open lifetime 分开,否则容易过早释放后端对象。
7. 安全模型
7.1 谁能访问挂载点
默认情况下,FUSE 内核 client 对非挂载所有者施加限制;allow_other 可放宽,但通常还受 /etc/fuse.conf 控制。它是 guest VFS 入口策略,不替代 daemon 后端权限。
7.2 请求凭据
请求头携带 uid/gid/pid。当前内核还支持:
- user namespace 映射;
- idmapped mount 协商;
- supplementary groups 扩展;
- LSM security context 扩展。
daemon 应验证字段和协商能力,并明确后端操作使用 daemon 凭据、请求者凭据还是映射凭据。
7.3 不可信 daemon
daemon 能返回恶意长度、非法属性、错误 nodeid 或不回复。内核需要验证结构长度、属性范围、目录项对齐和 error 值。FUSE mount 不会让 daemon 获得任意内核内存访问权,但 daemon 可以让依赖该挂载的进程阻塞或收到错误。
8. 连接失败与恢复
daemon 退出
关闭最后一个 /dev/fuse fd 或 transport 断开后,连接变为 disconnected。pending/processing 请求以 -ENODEV、-ECONNABORTED 等错误结束,等待者被唤醒。
管理员 abort
fusectl 的 abort 用于强制结束卡死连接。它是破坏性恢复:在途写可能处于不确定状态,应用需要重新验证。
请求超时
通用 FUSE 长期依赖信号和 abort;新协议能力可以提供 request timeout,但 timeout 的定义必须明确:
- 是等待 daemon 取请求,还是等待完整应答?
- 已产生部分 I/O 时返回字节数还是错误?
- timeout 后 daemon 是否仍可能完成副作用?
- 是否发送 INTERRUPT,是否 abort 整个连接?
对有副作用的操作,盲目重试可能重复执行。
9. 常见死锁模式
- daemon 的后端路径递归访问自己的 FUSE mount;
- 内存回收等待 FUSE writeback,而 daemon 分配内存又依赖同一回收;
- 持 inode lock 等 daemon,daemon 反向操作需要同一 inode lock;
- DAX range 回收等待长期 GUP pin;
- 同步 RELEASE 由 daemon 线程间接触发,而完成需要该 daemon 线程。
设计时应减少 daemon 对自身挂载的依赖,预留内存,使用规定的锁顺序和异步完成路径,并为 abort 提供可观测性。
10. 可靠性检查表
- 每个请求是否恰好完成一次?
- 所有错误分支是否释放 req、folio、file 和 lookup reference?
- daemon 能否处理同一 inode 的乱序并行请求?
- 副作用操作在 interrupt/timeout 后是否可判定或幂等?
- 背压是否覆盖 writeback 和 async direct I/O?
- daemon 退出后所有等待者是否会醒?
- notification 与普通应答是否有清晰顺序?
- 安全上下文是否按 INIT 协商解析?
第一章总结
第一章建立了 FUSE 的设计模型:分层边界、四种身份、三类缓存、多种 I/O 模式、并发队列和失败语义。第二章将这些概念逐一定位到 Linux 7.2-rc6 的具体源码。
| 上一篇 | 进入第二章 |