Linux-Kernel-Notes

Thinking in linux kernel

View on GitHub

2.4 请求生命周期与 /dev/fuse 传输

中文 English 目录

第二章:FUSE 代码实现分析 · 第 4/6 篇

本文从上层 fuse_args 跟到 fs/fuse/req.cfs/fuse/dev.cfs/fuse/fuse_dev_i.h,解释一次同步请求如何排队、被 daemon 读取、按 unique 匹配响应并唤醒调用者,也覆盖后台请求、FORGET、中断和断连竞态。

1. 两层请求表示

上层操作先构造 struct fuse_args,描述:

经典 device channel 再把它绑定到 struct fuse_req。后者增加传输生命周期状态:

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 队列中找到请求。

需要防御:

unique 只解决“回复属于哪个请求”,不解决操作是否被后端执行一次。

6. request_wait_answer() 与信号

同步调用者在 request_wait_answer() 等待。等待逻辑必须区分请求尚在 pending、已被 daemon 取走、已锁定复制以及已完成等状态。

收到信号时:

所以用户看到 EINTR 不一定证明远端写操作没有副作用。需要幂等性的上层协议应自行提供请求 ID 或事务语义。

7. 后台请求与背压

后台请求先受到 connection 的 background 额度控制,再进入发送队列。达到 max_background 时,新请求需要等待;超过 congestion_threshold 会影响 VFS/BDI 的拥塞判断。

完成 fuse_request_bg_finish() 时要:

  1. 执行对应完成回调;
  2. 归还 background slot;
  3. 唤醒等待额度的生产者;
  4. 在需要时继续派发已排队请求;
  5. 最后释放 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 时,内核需要同时处理:

所有路径最终必须汇入一次且仅一次的完成逻辑,并用连接错误唤醒 waiters。

11. 内存排序与请求所有权

dev.c 在 daemon 取走请求和等待线程检查状态之间使用锁及内存屏障。阅读注释中成对的 barrier 很重要:它们保证一方看到“已被读取”的状态时,也能看到请求复制/锁定状态,从而正确决定是本地移除还是发送 interrupt。

不要把这类 barrier 当成可有可无的性能细节;它们是避免双完成和丢中断的协议实现。

12. 一次请求的观测字段

排查延迟建议记录:

由此可以把延迟拆成 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