Linux-Kernel-Notes

Thinking in linux kernel

View on GitHub

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:

The classic device channel binds that description to struct fuse_req, adding transport-lifetime state:

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:

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.

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:

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:

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