Re: [PATCH v5 0/6] fuse: add io-uring buffer pools and zero-copy

Joanne Koong <[email protected]> Mon, 13 Jul 2026 14:06:46 -0700
Newsgroups dev.linux.lists.fuse-devel
Message-ID <CAJnrk1Y2tiwCecH-HVyaP79kcmBoEYu--mmwRKzp=9uZNaiSUA@mail.gmail.com>
On Tue, Jun 30, 2026 at 2:16 PM Joanne Koong <[email protected]> wrote:
>
> This series adds fuse io-uring buffer pools and zero-copy.
>
> Prior to this series, ents and buffers are tightly coupled where each entry
> has its own dedicated payload buffer, requiring N buffers for N entries where
> each buffer must be large enough to accomodate the maximum payload size. This
> is suboptimal as most request types require vastly less bytes than the maximum
> payload size and some requests do not require payload buffers at all.
>
> This series allows servers to pass in a buffer pool (a contiguous chunk of
> memory) that the kernel will use as it wishes for servicing ents/requests.
> This decoupling reduces the memory usage requirements needed to use
> fuse-io-uring and lets the kernel do any optimizations for assigning payload
> memory to requests.
>
> This series additionally adds zero copy to fuse io-uring. The server can
> directly access client pages or page cache folios without copying data through
> an intermediary buffer. This requires CAP_SYS_ADMIN privileges and using
> buffer pools. The zero copy patch has a dependency on io-uring registered
> bvec changes in [1].
>
> This series is on top of commit 7d87a5a284b with the io-uring bvec changes
> applied.
>
> The throughput improvements from registered buffers and zero-copy depends on
> how much of the server's per-request latency is spent on data copying vs
> backing I/O. When backing I/O dominates, the saved memcpy is a negligible
> fraction of overall latency. Please also note that for the server to
> read/write into the zero-copied pages, the read/write must go through io-uring
> as an IORING_OP_READ_FIXED / IORING_OP_WRITE_FIXED operation.
>
> The throughput improvement from zero-copy depends on how much of the
> per-request latency is spent on data copying vs backing I/O. The gain
> comes from eliminating the payload-buffer memcpy,  but accessing the
> zero-copied pages requires the server to issue the read/write as an
> IORING_OP_READ/WRITE_FIXED operation. The benefit is largest when the
> mempcy is a meaningful fraction of per-request latency while backing i/o
> is still noticable enough that the extra io-uring op's overhead doesn't
> dominate.
>
> Benchmarked with passthrough_hp (--nopassthrough, q_depth=8) on a
> 2-socket Intel Xeon Gold 6138 (40 cores / 80 threads), using fio (sync
> engine, bs=1M, O_DIRECT, numjobs=2, 30s run + 10s ramp, 3 runs) where
> direct-I/O throughput is against a RAM-backed (tmpfs) source (backing
> I/O is not the bottleneck):
>
>                 baseline   registered-buf   zero-copy   (zc vs base)
> direct read     ~5.1 GB/s  ~5.4 GB/s        ~8.9 GB/s   (+75%)
> direct write    ~3.4 GB/s  ~4.8 GB/s        ~5.1 GB/s   (+50%)
>
> On a device-bound NVMe (~2 GB/s reads) the read gain shrinks to ~10-16% (no
> measurable gains for writes), as backing I/O rather than the eliminated copy
> dominates latency. The benefit overall scales with how much of the per-request
> latency is the data copy versus backing I/O.
>
> The benchmark script and results can be found in [2]. The libfuse changes can
> be found in [3]. To test the server, run:
> sudo ~/libfuse/build/example/passthrough_hp ~/src ~/mounts/tmp --nopassthrough
> -o io_uring_zero_copy -o io_uring_q_depth=8
> Once this series is merged, the libfuse changes will be tidied up and
> submitted upstream.
>
> Thanks,
> Joanne
>
> [1] https://lore.kernel.org/io-uring/[email protected]/T/#t
> [2] https://github.com/joannekoong/linux/commits/fuse_zero_copy_benchmarks/
> [3] https://github.com/joannekoong/libfuse/commits/zero_copy_v5
>

Sashiko had some comments [1]. These are the replies to Sashiko's comments:

Patch 1/6:
1) Will add missing smp_rmb() in v6. Fix for pre-existing missing
smp_rmb() is submitted in [2].

2) Pre-existing issue, fix is submitted in [3].

Patch 2/6:
1) Pre-existing issue, fix is submitted in [3].

Patch 3/6:
1) ->uring_cmd() cancellation happens when a task exits or when a ring
is torn down. In both cases, the buffer is not marked as available
again in the bitmap, but this doesn't matter since the task/ring is
torn down. There's no buffer leak anywhere - the buffer was allocated
by userspace.

2) This introduces no additional head-of-line blocking than what
currently exists when there's no ent/buffer available for a request.
If a server wants to preserve memory and allocate fewer buffers than
entries, then requests have to wait until there are available buffers.
With ents and buffers decoupled under buffer pools, this also opens an
optimization where non-payload requests which require no buffer can
still be serviced when entries are free, but that can be a future
patch.
Buffer recycling does not need to check for stalled requests or idle
entries. The buffer is returned back when an in-flight request
completes, and each completion (commit-and-fetch) automatically
assigns the free buffer to any request waiting to be serviced.

3) It is safe (and expected) to read directly from cmd->sqe. uring_cmd
has different SQE-stability semantics than non-cmd io-uring ops.

4) A payload carrying request is added to the tail of the queue and
does not jump ahead of other payload carrying requests. Only
payloadless requests would get serviced first in the case of
bufferpool exhaustion, which is safe since the requests that can be
reordered are independent.

Patch 4/6:
1) It is safe (and expected) to read directly from cmd->sqe. uring_cmd
has different SQE-stability semantics than non-cmd io-uring ops.

2) This race is not reachable on a correct server. For a
buggy/malicious server that fires COMMIT_AND_FETCH concurrently with
ADD_BUFPOOL, this is safe since this returns an error regardless.
There's no regression introduced, but the barrier pairing should be
added anyways for robustness. I'll add this in for v6.

3) No, this is not necessary. See answer to 1)

Patch 5/6:

1) I'll drop the WARN_ON_ONCE. I don't think loopback mount can
trigger the warning, but I think other cases that go through the
kernel_read()/kernel_write() path on a direct-io zero-copy-enabled
fuse file potentially could.

2) This compiles, it is just currently flagged as broken because it
requires the io-uring bvec changes queued up in Jens's tree being
available [4].

3) same answer as above for 2)

4) same answer as the answer for 2) in patch 4.

5) There is no permanent leak - all resources are released when the
ring is torn down. This scenario is only possible if a server
deliberately tries to manufacture it by crossing rings. The worst case
that happens here is that the index in the buffer table of the
original ring remains reserved still and the folios are pinned, until
the ring is torn down. There's no use after free, as io-uring's node
refcount protects against this. Given that zero-copy is already gated
only to privileged servers and all resources are cleaned up when the
ring is torn down, this is not worth fixing, as the server would have
to deliberately self-inflict this.

6) No, this analysis is wrong. io_uring's teardown logic keeps
retrying cancellation until the ctx's references are all dropped (see
io_ring_exit_work()). This is the same in the task exit path as well
(io_uring_cancel_generic()).

Patch 6/6:
1) Will fix the typo for v6.

2) This is fine. It is safe (and expected) to read directly from
cmd->sqe. uring_cmd has different SQE-stability semantics than non-cmd
io-uring ops.

Thanks,
Joanne

[1] https://sashiko.dev/#/patchset/20260630211436.2062816-1-joannelkoong%40gmail.com
[2] https://lore.kernel.org/fuse-devel/[email protected]/T/#mab904e0a63035e094c18184930284727399174be
[3] https://lore.kernel.org/fuse-devel/[email protected]/T/#m00c29b80ce418d05a791e330be7a9b9530e4ba99
[4] https://lore.kernel.org/io-uring/[email protected]/T/#t