2.4 Request Lifecycle and /dev/fuse Transport
| 中文 | English | Contents |
Chapter 2: FUSE Implementation Analysis · Article 4 of 6
This article follows an operation from fuse_args through fs/fuse/req.c, fs/fuse/dev.c, and fs/fuse/fuse_dev_i.h. It covers synchronous queueing, daemon read/write, unique-based reply matching, background work, FORGET, interruption, and disconnect races.
1. Two request representations
An operation first builds struct fuse_args, which describes:
- opcode and node ID;
- input segments and lengths;
- output segments and capacities;
- reply and variable-output rules;
- completion and background behavior.
The classic device channel binds that description to struct fuse_req, adding transport-lifetime state:
- unique ID and channel;
- pending/processing list nodes;
- references and completion state;
- wait queue;
- interrupted, background, and copy-lock flags.
fuse_args says what to do; fuse_req says where this transaction currently lives.
2. Channel abstraction
fs/fuse/req.c exposes stable send functions. A synchronous operation reaches fuse_chan_send(), background work reaches fuse_chan_send_bg(), and a reply to a daemon notification uses a dedicated path. The channel determines whether delivery uses classic device I/O, io_uring, or another transport.
Directory and file code therefore need not know how daemon buffers are supplied.
3. Synchronous request chain
VFS/FUSE operation
-> fuse_simple_request(fm, args)
-> fuse_chan_send(channel, args)
-> allocate fuse_req and bind args
-> assign unique
-> enqueue on fuse_iqueue.pending
-> wake daemon reader
-> request_wait_answer(req)
daemon read(/dev/fuse)
-> fuse_dev_do_read()
-> select pending request
-> copy request header and payload
-> move reply-bearing request to fuse_pqueue.processing
daemon write(/dev/fuse)
-> fuse_dev_do_write()
-> parse fuse_out_header.unique
-> fuse_request_find(processing, unique)
-> validate and copy output
-> fuse_request_end(req)
-> wake original caller
4. Input and processing queues
struct fuse_iqueue holds requests not yet delivered, interrupts, and forget messages, together with daemon-reader wait state and transport callbacks. struct fuse_pqueue holds requests already dequeued by a daemon endpoint and waiting for replies.
A simplified state machine is:
allocated -> pending -> copy/io -> processing -> finished -> freed
\-> no-reply finished
\-> locally removed or aborted
Copying has additional ownership and lock states, so list membership alone is not a complete state variable.
5. Unique allocation and reply validation
An ordinary request receives a connection-scoped unique before queueing. The daemon copies it into the output header, and fuse_dev_do_write() uses it to locate the request.
The implementation defends against:
- unknown or stale unique IDs;
- replies after local completion;
- truncated or oversized fixed output;
- variable output beyond its buffer;
- confusion between notifications and ordinary replies;
- completing the same request twice.
Unique identifies the transaction; it does not make a backend mutation exactly-once.
6. request_wait_answer() and signals
A synchronous caller waits in request_wait_answer(). Signal handling depends on whether the request is still pending, is being copied, or is already processing.
- A still-pending request may be removed locally.
- A dequeued request generally requires
FUSE_INTERRUPT. - The interrupt races with an ordinary reply.
- The daemon’s backend may no longer be cancellable.
Consequently, EINTR does not prove that a remote mutation had no side effect. Higher layers that need idempotency require transaction identifiers or backend support.
7. Background queue and backpressure
Background sends consume a bounded connection slot. At max_background, producers wait; crossing congestion_threshold influences congestion behavior.
Background completion must execute the callback, return the slot, wake blocked producers, dispatch newly eligible work, and release references in safe order. Losing a slot leaves the connection permanently saturated; freeing the request before its callback or accounting completes creates use-after-free risk.
8. FORGET’s special path
FORGET and BATCH_FORGET usually have no reply and can be generated heavily by reclaim. The input queue maintains dedicated forget state and can batch messages for a daemon read. Once copied, the kernel-side message is finished rather than inserted into the processing queue.
A FORGET on the normal reply-bearing path would wait forever.
9. Notifications reverse the direction
Ordinary traffic is kernel request → daemon reply. A notification is daemon-initiated, commonly invalidating an inode, entry, or data range. Device-write code classifies it before normal reply matching.
Notification payloads remain untrusted: range overflow, invalid node IDs, and excessive name lengths must be rejected.
10. Abort and disconnect
Connection abort must reconcile:
- pending requests not yet read;
- processing requests awaiting replies;
- background slot accounting;
- sleeping application callers and daemon readers;
- late daemon writes;
- initialization and teardown waiters.
All paths need to converge on exactly one request completion and a deterministic connection error.
11. Memory ordering and ownership
dev.c uses locks and paired barriers between daemon dequeue and the waiting task’s state checks. They ensure that observing “request was read” also observes its copy/lock state, so interruption chooses local removal or interrupt queueing correctly.
These barriers implement protocol ownership; removing them as apparent micro-optimizations can introduce lost interrupts and double completion.
12. Observing one request
For latency analysis record:
- opcode, node ID, and unique;
- enqueue, daemon dequeue, reply, and request-end timestamps;
- foreground/background classification;
- input/output lengths;
- daemon worker and backend request identity;
- interrupt, abort, and final errno.
This separates kernel queueing, transport/daemon queueing, backend service, and completion latency.
13. Summary
fuse_args represents protocol intent, while fuse_req owns transport lifetime. Classic /dev/fuse moves a request from pending to processing and matches its reply by unique. Signals, background work, FORGET, notifications, and abort extend the basic state machine and demand unambiguous single-completion ownership.
| Previous: Open, Read, Write, and Writeback | Next: mmap, virtio-fs, and DAX |