2.4 请求生命周期与 /dev/fuse 传输
| 中文 | English | 目录 |
第二章:FUSE 代码实现分析 · 第 4/6 篇
本文从上层 fuse_args 跟到 fs/fuse/req.c、fs/fuse/dev.c 和 fs/fuse/fuse_dev_i.h,解释一次同步请求如何排队、被 daemon 读取、按 unique 匹配响应并唤醒调用者,也覆盖后台请求、FORGET、中断和断连竞态。
1. 两层请求表示
上层操作先构造 struct fuse_args,描述:
- opcode 和 node ID;
- 输入参数段与长度;
- 输出参数段与最大长度;
- 是否期待回复、是否允许可变长度输出;
- 完成回调和 background 等属性。
经典 device channel 再把它绑定到 struct fuse_req。后者增加传输生命周期状态:
- unique;
- 所属 channel;
- pending/processing 链表节点;
- 引用计数与完成状态;
- wait queue;
- interrupted、background、locked 等 flags。
fuse_args 表示“做什么”,fuse_req 表示“这次消息现在处于哪里”。
2. channel 抽象
fs/fuse/req.c 提供较稳定的发送接口:同步请求调用 fuse_chan_send(),后台请求调用 fuse_chan_send_bg(),通知响应使用专门接口。具体 channel 决定如何把请求交给 /dev/fuse、io_uring 或其他 transport。
这层分离使目录和文件逻辑无需理解 daemon 是通过 read/write 还是 ring buffer 收发。
3. 经典同步请求全链路
VFS/FUSE 操作
-> fuse_simple_request(fm, args)
-> fuse_chan_send(channel, args)
-> 分配 fuse_req,复制 args 描述
-> 分配 unique
-> 加入 fuse_iqueue.pending
-> 唤醒读取 /dev/fuse 的 daemon
-> request_wait_answer(req)
daemon read(/dev/fuse)
-> fuse_dev_do_read()
-> pending 中选出请求
-> 复制 header 与输入 payload
-> 需要 reply 的请求加入 fuse_pqueue.processing
daemon write(/dev/fuse)
-> fuse_dev_do_write()
-> 解析 fuse_out_header.unique
-> fuse_request_find(processing, unique)
-> 复制输出并校验长度
-> fuse_request_end(req)
-> 唤醒原调用者
4. 输入队列与处理队列
struct fuse_iqueue 保存尚未交给 daemon 的请求、interrupt 和 forget,并提供等待队列以及 transport 回调。struct fuse_pqueue 保存已经被某个 daemon reader 取走、等待 reply 的请求。
状态机可简化为:
allocated -> pending -> io copy -> processing -> finished -> freed
\-> no-reply finished
\-> removed/aborted
请求在复制期间还有中间状态和锁保护,因此真实代码不能只靠链表是否为空判断所有权。
5. unique 的分配和响应匹配
每个普通请求在入队时获得连接范围内的 unique。daemon 必须把该值原样写入响应 header。fuse_dev_do_write() 按 unique 在 processing 队列中找到请求。
需要防御:
- unknown unique;
- 已完成请求的迟到 reply;
- reply 长度小于固定输出结构;
- fixed-size 输出却返回额外或缺少字节;
- daemon 把普通 reply 写成 notification;
- 一个请求被完成两次。
unique 只解决“回复属于哪个请求”,不解决操作是否被后端执行一次。
6. request_wait_answer() 与信号
同步调用者在 request_wait_answer() 等待。等待逻辑必须区分请求尚在 pending、已被 daemon 取走、已锁定复制以及已完成等状态。
收到信号时:
- 若请求还在 pending,内核可能直接从队列移除并结束;
- 若 daemon 已取走,内核通常只能排队一个
FUSE_INTERRUPT; - normal reply 可能与 interrupt 竞争;
- daemon 可能无法取消已经提交给后端的操作。
所以用户看到 EINTR 不一定证明远端写操作没有副作用。需要幂等性的上层协议应自行提供请求 ID 或事务语义。
7. 后台请求与背压
后台请求先受到 connection 的 background 额度控制,再进入发送队列。达到 max_background 时,新请求需要等待;超过 congestion_threshold 会影响 VFS/BDI 的拥塞判断。
完成 fuse_request_bg_finish() 时要:
- 执行对应完成回调;
- 归还 background slot;
- 唤醒等待额度的生产者;
- 在需要时继续派发已排队请求;
- 最后释放 request 引用。
如果先释放对象再归还额度,容易出现 use-after-free;如果忘记归还额度,连接会永久看似饱和。
8. FORGET 为什么走特殊路径
FORGET/BATCH_FORGET 通常没有 reply,且在内存回收中可能大量产生。fuse_iqueue 为其维护专门队列和批处理逻辑。daemon read 时可以优先或批量取出,完成复制后直接释放内核侧消息。
把 FORGET 塞进普通 processing 队列会等待永远不会来的 response。
9. notification 的反向方向
普通模式是 kernel request、daemon reply;notification 则由 daemon 主动通过 device write 向内核发送,例如失效 inode、entry 或 data range。其 header/unique 约定与普通 reply 不同,fuse_dev_do_write() 必须先分类,再进入相应通知处理函数。
通知输入同样不可信:范围溢出、非法 node ID 和越界名称长度都必须拒绝。
10. abort 和断连
连接 abort 时,内核需要同时处理:
- pending 中尚未被读取的请求;
- processing 中等待 reply 的请求;
- background 额度;
- wait queue 中的 daemon reader 和应用 caller;
- 仍可能到来的迟到 write;
- connection 初始化或销毁等待者。
所有路径最终必须汇入一次且仅一次的完成逻辑,并用连接错误唤醒 waiters。
11. 内存排序与请求所有权
dev.c 在 daemon 取走请求和等待线程检查状态之间使用锁及内存屏障。阅读注释中成对的 barrier 很重要:它们保证一方看到“已被读取”的状态时,也能看到请求复制/锁定状态,从而正确决定是本地移除还是发送 interrupt。
不要把这类 barrier 当成可有可无的性能细节;它们是避免双完成和丢中断的协议实现。
12. 一次请求的观测字段
排查延迟建议记录:
- opcode、node ID、unique;
- enqueue、daemon dequeue、reply、request end 四个时间点;
- foreground/background;
- 输入输出长度;
- daemon worker 与后端请求 ID;
- interrupt、abort 和最终 errno。
由此可以把延迟拆成 kernel pending、daemon/transport、backend 和 completion 四段。
13. 本文小结
fuse_args 描述协议操作,fuse_req 承担传输生命周期。经典 /dev/fuse 路径让请求从 input pending 进入 processing,再由 unique 匹配 reply。信号、background、FORGET、notification 和 abort 都会改变普通状态机,正确性依赖清楚的所有权与单次完成规则。
| 上一篇:open、read、write 与 writeback | 下一篇:mmap、virtio-fs 与 DAX |