mirror of
https://github.com/torvalds/linux.git
synced 2026-10-26 20:46:02 +01:00
Implement zero-copy in fuse io-uring to eliminate memory copies between
the application, kernel, and server for read/write operations. The
server can directly access client pages or page cache folios without
copying data through an intermediary buffer. When a fuse request arrives,
the kernel registers the relevant pages into a sparse slot in the
server's io_uring registered buffer table. The server can then operate
on these pages directly using io-uring fixed buffer operations (eg
read_fixed/write_fixed) and the kernel unregisters these pages when the
request completes. Non-page-backed args (eg op out headers) will go
through the payload buffer as normal. The server can specify which open
files should have their reads/writes go through zero-copy, by setting
the FOPEN_IO_URING_ZERO_COPY flag when servicing opens.
This requires CAP_SYS_ADMIN and bufpools. This is gated behind
CAP_SYS_ADMIN because zero-copy allows the server direct access to the
client's underlying pages, rather than operating on an intermediary
buffer that the contents of the client's pages were copied into or on
page cache folios.
The request flow for the zero-copy direct-io write path (client writes
data, server reads it) is as follows:
=======================================================================
| Kernel | FUSE server
| |
| "write(fd, buf, 1MB)" |
| |
| >sys_write() |
| >fuse_file_write_iter() |
| >fuse_send_one() |
| [req->args->in_pages = true] |
| [folios hold client write data] |
| |
| >fuse_uring_copy_to_ring() |
| >copy_header_to_ring(IN_OUT) |
| [memcpy fuse_in_header] |
| >copy_header_to_ring(OP) |
| [memcpy write_in header] |
| |
| >fuse_uring_args_to_ring() |
| >setup_fuse_copy_state() |
| [skip_folio_copy = true] |
| |
| >fuse_uring_set_up_zero_copy() |
| [folio_get for each client folio] |
| [build bio_vec array from folios] |
| >io_buffer_register_bvec() |
| [register pages at
ent->zero_copy_index] |
| [ent->zero_copied = true] |
| |
| >fuse_copy_args() |
| [skip_folio_copy => return 0 |
| for page arg, skip data copy] |
| |
| >copy_header_to_ring(RING_ENT) |
| [memcpy ent_in_out] |
| >io_uring_cmd_done() |
| |
| | [CQE received]
| |
| | [issue io_uring READ at
| | ent->zero_copy_index]
| | [reads directly from
| |client's pages (ZERO_COPY)]
| |
| | [write data to backing
| | store]
| | [submit COMMIT AND FETCH]
| |
| >fuse_uring_commit_fetch() |
| >fuse_uring_commit() |
| >fuse_uring_copy_from_ring() |
| >fuse_uring_req_end() |
| >io_buffer_unregister(ent->zero_copy_index) |
| [unregister pages from index] |
| >fuse_zero_copy_release() |
| [folio_put for each folio] |
| [ent->zero_copied = false] |
| >fuse_request_end() |
| [wake up client] |
The zero-copy read path is analogous.
Some requests may have both page-backed args and non-page-backed args.
For these requests, the page-backed args are zero-copied while the
non-page-backed args are copied to the buffer selected from the buffer
pool:
zero-copy: pages registered via io_buffer_register_bvec()
non-page-backed: copied to payload buffer via fuse_copy_args()
For a request whose payload is zero-copied, the
registration/unregistration path looks like:
register: fuse_uring_set_up_zero_copy()
folio_get() for each folio
io_buffer_register_bvec(ent->zero_copy_index)
unregister: fuse_uring_req_end()
io_buffer_unregister(ent->zero_copy_index)
-> fuse_zero_copy_release() callback
folio_put() for each folio
Please note that on abort for in-flight zero-copied requests that have
been sent to userspace, the registered bvec slot remains occupied and
its folios remain pinned until the io-uring ring is destroyed, at which
point io-uring unregisters all buffers and the fuse_zero_copy_release()
callback drops the folio references. Unregistering at teardown would
require operating on the ring context directly, whose validity is hard
to ascertain; this is deemed not worth the complexity for the abort
race, since everything is freed when the ring is torn down.
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%)
Reads end up higher than writes because the backing store reads faster
than it writes (the baseline shows the same read>write gap, and the raw
device does too). On a device-bound NVMe (~2 GB/s reads) the read gain
shrinks to ~10-16% (and 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.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
Reviewed-by: Bernd Schubert <bernd@bsbernd.com>
Signed-off-by: Miklos Szeredi <mszeredi@redhat.com>
257 lines
5.5 KiB
C
257 lines
5.5 KiB
C
/* SPDX-License-Identifier: GPL-2.0
|
|
*
|
|
* FUSE: Filesystem in Userspace
|
|
* Copyright (c) 2023-2024 DataDirect Networks.
|
|
*/
|
|
|
|
#ifndef _FS_FUSE_DEV_URING_I_H
|
|
#define _FS_FUSE_DEV_URING_I_H
|
|
|
|
#include <linux/uio.h>
|
|
|
|
#include "fuse_dev_i.h"
|
|
|
|
#ifdef CONFIG_FUSE_IO_URING
|
|
|
|
#define FUSE_URING_TEARDOWN_TIMEOUT (5 * HZ)
|
|
#define FUSE_URING_TEARDOWN_INTERVAL (HZ/20)
|
|
|
|
enum fuse_ring_req_state {
|
|
FRRS_INVALID = 0,
|
|
|
|
/* The ring entry received from userspace and it is being processed */
|
|
FRRS_COMMIT,
|
|
|
|
/* The ring entry is waiting for new fuse requests */
|
|
FRRS_AVAILABLE,
|
|
|
|
/* The ring entry got assigned a fuse req */
|
|
FRRS_FUSE_REQ,
|
|
|
|
/* The ring entry is in or on the way to user space */
|
|
FRRS_USERSPACE,
|
|
|
|
/* The ring entry is in teardown */
|
|
FRRS_TEARDOWN,
|
|
|
|
/* The ring entry is released, but not freed yet */
|
|
FRRS_RELEASED,
|
|
};
|
|
|
|
/* how a queue's payload buffers are provided */
|
|
enum fuse_queue_payload_mode {
|
|
/* not yet committed (a bufpool may still be added) */
|
|
FUSE_PAYLOAD_UNSET = 0,
|
|
/* each entry registers its own payload buffer */
|
|
FUSE_PAYLOAD_PER_ENT,
|
|
/* each entry's payload buffer is assigned from a bufpool */
|
|
FUSE_PAYLOAD_BUFPOOL,
|
|
};
|
|
|
|
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;
|
|
|
|
/* starting uaddr of the bufpool */
|
|
uintptr_t base_uaddr;
|
|
|
|
/* size of each buffer in the pool */
|
|
size_t buf_size;
|
|
|
|
/* total number of buffers in the pool */
|
|
unsigned int nr_bufs;
|
|
|
|
/* bitmap tracking which buffers are free */
|
|
unsigned long free_map[];
|
|
};
|
|
|
|
/** A fuse ring entry, part of the ring queue */
|
|
struct fuse_ring_ent {
|
|
/* userspace buffer */
|
|
struct fuse_uring_req_header __user *headers;
|
|
struct iovec payload;
|
|
|
|
/* buffer id in the pool, if bufpools are used. ignored otherwise */
|
|
unsigned int buf_id;
|
|
|
|
/* true if the request's pages are being zero-copied */
|
|
bool zero_copied;
|
|
unsigned int zero_copy_index;
|
|
|
|
/* the ring queue that owns the request */
|
|
struct fuse_ring_queue *queue;
|
|
|
|
/* fields below are protected by queue->lock */
|
|
|
|
struct io_uring_cmd *cmd;
|
|
|
|
struct list_head list;
|
|
|
|
enum fuse_ring_req_state state;
|
|
|
|
struct fuse_req *fuse_req;
|
|
};
|
|
|
|
struct fuse_ring_queue {
|
|
/*
|
|
* back pointer to the main fuse uring structure that holds this
|
|
* queue
|
|
*/
|
|
struct fuse_ring *ring;
|
|
|
|
/* queue id, corresponds to the cpu core */
|
|
unsigned int qid;
|
|
|
|
/*
|
|
* queue lock, taken when any value in the queue changes _and_ also
|
|
* a ring entry state changes.
|
|
*/
|
|
spinlock_t lock;
|
|
|
|
/* available ring entries (struct fuse_ring_ent) */
|
|
struct list_head ent_avail_queue;
|
|
|
|
/*
|
|
* entries in the process of being committed or in the process
|
|
* to be sent to userspace
|
|
*/
|
|
struct list_head ent_w_req_queue;
|
|
struct list_head ent_commit_queue;
|
|
|
|
/* entries in userspace */
|
|
struct list_head ent_in_userspace;
|
|
|
|
/* entries that are released */
|
|
struct list_head ent_released;
|
|
|
|
/* fuse requests waiting for an entry slot */
|
|
struct list_head fuse_req_queue;
|
|
|
|
/* background fuse requests */
|
|
struct list_head fuse_req_bg_queue;
|
|
|
|
struct fuse_pqueue fpq;
|
|
|
|
unsigned int active_background;
|
|
|
|
bool stopped;
|
|
|
|
/* how this queue's payload buffers are provided */
|
|
enum fuse_queue_payload_mode payload_mode;
|
|
|
|
/* only allocated when payload_mode == FUSE_PAYLOAD_BUFPOOL */
|
|
struct fuse_bufpool *bufpool;
|
|
|
|
bool zero_copy;
|
|
};
|
|
|
|
/*
|
|
* Describes if uring is for communication and holds alls the data needed
|
|
* for uring communication
|
|
*/
|
|
struct fuse_ring {
|
|
/* back pointer */
|
|
struct fuse_chan *chan;
|
|
|
|
/* number of ring queues */
|
|
size_t nr_queues;
|
|
|
|
/* maximum payload/arg size */
|
|
size_t max_payload_sz;
|
|
|
|
struct fuse_ring_queue **queues;
|
|
|
|
/*
|
|
* Log ring entry states on stop when entries cannot be released
|
|
*/
|
|
unsigned int stop_debug_log : 1;
|
|
|
|
wait_queue_head_t stop_waitq;
|
|
|
|
/* async tear down */
|
|
struct delayed_work async_teardown_work;
|
|
|
|
/* log */
|
|
unsigned long teardown_time;
|
|
|
|
atomic_t queue_refs;
|
|
|
|
bool ready;
|
|
};
|
|
|
|
void fuse_uring_conn_init(struct fuse_chan *fch);
|
|
void fuse_uring_stop_queues(struct fuse_ring *ring);
|
|
void fuse_uring_abort_end_requests(struct fuse_ring *ring);
|
|
int fuse_uring_cmd(struct io_uring_cmd *cmd, unsigned int issue_flags);
|
|
void fuse_uring_queue_fuse_req(struct fuse_iqueue *fiq, struct fuse_req *req);
|
|
bool fuse_uring_queue_bq_req(struct fuse_req *req);
|
|
bool fuse_uring_remove_pending_req(struct fuse_req *req);
|
|
bool fuse_uring_request_expired(struct fuse_chan *fch);
|
|
|
|
static inline void fuse_uring_abort(struct fuse_chan *fch)
|
|
{
|
|
struct fuse_ring *ring = fch->ring;
|
|
|
|
if (ring == NULL)
|
|
return;
|
|
|
|
fuse_uring_abort_end_requests(ring);
|
|
|
|
if (atomic_read(&ring->queue_refs) > 0)
|
|
fuse_uring_stop_queues(ring);
|
|
}
|
|
|
|
static inline void fuse_uring_wait_stopped_queues(struct fuse_chan *fch)
|
|
{
|
|
struct fuse_ring *ring = fch->ring;
|
|
|
|
if (ring)
|
|
wait_event(ring->stop_waitq,
|
|
atomic_read(&ring->queue_refs) == 0);
|
|
}
|
|
|
|
static inline bool fuse_uring_ready(struct fuse_chan *fch)
|
|
{
|
|
struct fuse_ring *ring = READ_ONCE(fch->ring);
|
|
|
|
return ring && smp_load_acquire(&ring->ready);
|
|
}
|
|
|
|
#else /* CONFIG_FUSE_IO_URING */
|
|
|
|
static inline void fuse_uring_conn_init(struct fuse_chan *fch)
|
|
{
|
|
}
|
|
|
|
static inline void fuse_uring_abort(struct fuse_chan *fch)
|
|
{
|
|
}
|
|
|
|
static inline void fuse_uring_wait_stopped_queues(struct fuse_chan *fch)
|
|
{
|
|
}
|
|
|
|
static inline bool fuse_uring_ready(struct fuse_chan *fch)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
static inline bool fuse_uring_remove_pending_req(struct fuse_req *req)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
static inline bool fuse_uring_request_expired(struct fuse_chan *fch)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#endif /* CONFIG_FUSE_IO_URING */
|
|
|
|
#endif /* _FS_FUSE_DEV_URING_I_H */
|