Re: [PATCH v6 4/6] fuse: support registered buffer pools in io-uring

Joanne Koong <[email protected]>
Newsgroups dev.linux.lists.fuse-devel
Message-ID <CAJnrk1ah0+F+VvpGnLJwqmKFRfb1Bo8Vc_V6ucy5=9JQT=G9Wg@mail.gmail.com>
On Tue, Aug 11, 2026 at 3:45 PM Bernd Schubert <[email protected]> wrote:
>
>
>
> On 7/16/26 19:59, Joanne Koong wrote:
> > Allow servers to use a buffer pool that is also registered through
> > io-uring. When the server registers a buffer pool with io-uring, the
> > pages backing the pool are pinned upfront. This eliminates the overhead
> > of pinning/unpinning user pages and translating virtual addresses per
> > i/o request. This also allows servers to use the same registered memory
> > for subsequent backing store I/O (eg read_fixed/write_fixed), keeping
> > data in the same pinned pages without additional pinning or mapping
> > overhead required.
> >
> > To use this, the server needs to set the FUSE_URING_REGISTERED_BUFPOOL
> > flag when adding a bufpool through the FUSE_IO_URING_CMD_ADD_BUFPOOL
> > cmd. For every sqe submitted (including the one for adding the bufpool),
> > it should set sqe->uring_cmd_flags to include IORING_URING_CMD_FIXED,
> > and pass in the index where the registered bufpool resides to
> > sqe->buf_index.
> >
> > 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 buffers
> >   direct read       ~5.1 GB/s     ~5.4 GB/s   (+~5%)
> >   direct write      ~3.4 GB/s     ~4.8 GB/s   (+~45%)
> >
> > Registered buffers bring up the write path speed up closer to speed of
> > reads. There isn't much improvement for reads because it is already fast
> > enough where it's at the copy-bound ceiling (surpassing that requires
> > doing zero-copy). On a device-bound NVMe though, the differences are
> > within noise, as backing I/O dominates per-request latency.
> >
> > Signed-off-by: Joanne Koong <[email protected]>
> > ---
> >  fs/fuse/dev_uring.c       | 122 +++++++++++++++++++++++++++++++-------
> >  fs/fuse/dev_uring_i.h     |   8 +++
> >  include/uapi/linux/fuse.h |   4 ++
> >  3 files changed, 112 insertions(+), 22 deletions(-)
> >
> > diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c
> > index e9da92b46090..ed7e783bd68d 100644
> > --- a/fs/fuse/dev_uring.c
> > +++ b/fs/fuse/dev_uring.c
> > @@ -22,6 +22,8 @@ MODULE_PARM_DESC(enable_uring,
> >  #define FUSE_URING_IOV_HEADERS 0
> >  #define FUSE_URING_IOV_PAYLOAD 1
> >
> > +#define FUSE_URING_ADD_BUFPOOL_FLAGS (FUSE_URING_REGISTERED_BUFPOOL)
> > +
> >  bool fuse_uring_enabled(void)
> >  {
> >       return enable_uring;
> > @@ -47,6 +49,27 @@ static inline bool bufpool_enabled(struct fuse_ring_queue *queue)
> >       return queue->payload_mode == FUSE_PAYLOAD_BUFPOOL;
> >  }
> >
> > +static inline bool bufpool_registered(struct fuse_ring_queue *queue)
> > +{
> > +     return queue->bufpool && queue->bufpool->registered;
> > +}
> > +
> > +/*
> > + * For a registered bufpool, every sqe that drives a payload import (REGISTER,
> > + * COMMIT_AND_FETCH) must carry the registered buffer index of the pool.
> > + * This also must be called from the command's issue handler, where cmd->sqe is
> > + * still valid
> > + */
> > +static inline bool fuse_uring_cmd_index_ok(struct io_uring_cmd *cmd,
> > +                                        struct fuse_ring_queue *queue)
> > +{
> > +     if (!bufpool_registered(queue))
> > +             return true;
> > +
> > +     return (cmd->flags & IORING_URING_CMD_FIXED) &&
> > +            READ_ONCE(cmd->sqe->buf_index) == queue->bufpool->registered_index;
> > +}
> > +
> >  static void uring_cmd_set_ring_ent(struct io_uring_cmd *cmd,
> >                                  struct fuse_ring_ent *ring_ent)
> >  {
> > @@ -653,19 +676,42 @@ static int copy_header_from_ring(struct fuse_ring_ent *ent,
> >       return 0;
> >  }
> >
> > +static int fuse_uring_import_payload(struct fuse_ring_ent *ent, int dir,
> > +                                  struct iov_iter *iter,
> > +                                  unsigned int issue_flags)
> > +{
> > +     void __user *base = ent->payload.iov_base;
> > +     size_t len = ent->payload.iov_len;
> > +     int err = 0;
> > +
> > +     if (!base) {
> > +             memset(iter, 0, sizeof(*iter));
> > +             return 0;
> > +     }
> > +
> > +     if (bufpool_registered(ent->queue))
> > +             err = io_uring_cmd_import_fixed((u64)(uintptr_t)base, len, dir,
> > +                                             iter, ent->cmd, issue_flags);
> > +     else
> > +             err = import_ubuf(dir, base, len, iter);
>
> Hmm ok, just an annotation, I have to admit that I don't like
> issue_flags too much. Especially for the optimization that DDN has by
> avoiding io_uring_cmd_complete_in_task(), IO_URING_F_UNLOCKED is rather
> painful. Additionaly with that optimization the lock might get
> contended. The new addition here doesn't introduce it, but also doesn't
> make it any better.

For what it's worth, the locks aren't taken on these paths (eg sends
go through fuse_uring_dispatch_ent() ->
io_uring_cmd_complete_in_task() and task work runs with
ctx->uring_lock held, and commits run in the command's issue handler,
where the lock is likewise held).

Threading the issue_flags through gets a bit annoying, but I
personally don't see a better way for core io-uring infrastructure to
determine whether a given call path already holds the lock or not.

>
> > +
> > +     if (err)
> > +             pr_info_ratelimited("fuse: Import of user buffer failed\n");
> > +
> > +     return err;
> > +}
> > +
> > @@ -1362,8 +1421,21 @@ static int fuse_uring_add_bufpool(struct io_uring_cmd *cmd,
> >       uintptr_t pool_uaddr;
> >       unsigned int pool_len, nr_bufs;
> >       size_t pool_size, buf_size;
> > +     bool registered = flags & FUSE_URING_REGISTERED_BUFPOOL;
> > +
> > +     if (!ring || qid >= ring->nr_queues)
> > +             return -EINVAL;
> >
> > -     if (!ring || qid >= ring->nr_queues || flags)
> > +     if (flags & ~FUSE_URING_ADD_BUFPOOL_FLAGS)
> > +             return -EINVAL;
> > +
> > +     /*
> > +      * A registered bufpool is reached through an io_uring fixed buffer, so
> > +      * this command must be submitted with IORING_URING_CMD_FIXED and the
> > +      * registered buffer index in sqe->buf_index. That index is recorded
> > +      * below and enforced on every REGISTER / COMMIT_AND_FETCH sqe.
> > +      */
> > +     if (registered && !(cmd->flags & IORING_URING_CMD_FIXED))
> >               return -EINVAL;
>
> Hmm, why do we actually need FUSE_URING_REGISTERED_BUFPOOL if the
> decision can be made by IORING_URING_CMD_FIXED?

It's not strictly needed, but imo it makes uapi more explicit and the
contract more clear. I don't feel strongly about this though, I'm fine
with either approach.

>
> >
> > diff --git a/fs/fuse/dev_uring_i.h b/fs/fuse/dev_uring_i.h
> > index cdf56f8b38b5..e142cae43022 100644
> > --- a/fs/fuse/dev_uring_i.h
> > +++ b/fs/fuse/dev_uring_i.h
> > @@ -49,6 +49,14 @@ enum fuse_queue_payload_mode {
> >  };
> >
> >  struct fuse_bufpool {
> > +     bool registered;
> > +
> > +     /*
> > +      * io_uring registered buffer table index for this pool, bound at
> > +      * ADD_BUFPOOL time. Only valid if the bufpool is registered
> > +      */
> > +     u16 registered_index;
>
> Dunno, could this go into the previous commit or be reserved in the
> previous commit? This addition make bisect hard due to ABI change?

This is an addition to a kernel-internal struct, not an uapi-defined
one, so I don't think this affects ABI. The previous commit doesn't
have any notion of registered buffers, so I think moving these
definitions to the previous commit would be confusing.

Thanks,
Joanne
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.