Files
libfuse/lib/fuse_uring.c
Bernd Schubert 1615ff35af fuse_uring: register the ring fd for single-issuer queues
For single-issuer queues only fuse_uring_thread() ever issues
io_uring_enter() on the ring, so the per-thread registered ring-fd index
is always valid. Registering it lets io_uring skip the fd grab/put on
every enter, which is measurable with libfuse's shared, threaded file
table.

A ring shared between separate submit and reap threads cannot use this
(each issuing thread would need its own index), so it stays gated on
io_uring_single_issuer. Registration needs kernel >= 5.18 and is
non-fatal: on failure the queue simply keeps using the normal ring fd.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-29 00:21:20 +02:00

1043 lines
26 KiB
C

/*
* FUSE: Filesystem in Userspace
* Copyright (C) 2025 Bernd Schubert <bschubert@ddn.com>
*
* Implementation of (most of) FUSE-over-io-uring.
*
* This program can be distributed under the terms of the GNU LGPLv2.
* See the file LGPL2.txt
*/
#define _GNU_SOURCE
#include "fuse_i.h"
#include "fuse_kernel.h"
#include "fuse_uring_i.h"
#include <stdlib.h>
#include <liburing.h>
#include <sys/sysinfo.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <linux/mempolicy.h>
#include <pthread.h>
#include <stdio.h>
#include <linux/sched.h>
#include <poll.h>
#include <sys/eventfd.h>
/* Size of command data area in SQE when IORING_SETUP_SQE128 is used */
#define FUSE_URING_MAX_SQE128_CMD_DATA 80
struct fuse_ring_ent {
struct fuse_ring_queue *ring_queue; /* back pointer */
struct fuse_req req;
struct fuse_uring_req_header *req_header;
void *op_payload;
size_t req_payload_sz;
/* commit id of a fuse request */
uint64_t req_commit_id;
enum fuse_uring_cmd last_cmd;
/* header and payload */
struct iovec iov[2];
};
struct fuse_ring_queue {
/* back pointer */
struct fuse_ring_pool *ring_pool;
int qid;
pthread_t tid;
int eventfd;
size_t req_header_sz;
struct io_uring ring;
pthread_mutex_t ring_lock;
/* batched inline replies across cqe handling; flushed by the loop */
_Atomic bool cqe_processing;
/* size depends on queue depth */
struct fuse_ring_ent ent[];
};
/**
* Main fuse_ring structure, holds all fuse-ring data
*/
struct fuse_ring_pool {
struct fuse_session *se;
/* mirror of se->conn.io_uring_single_issuer, fixed at ring creation */
bool single_issuer;
/* number of queues */
size_t nr_queues;
/* number of per queue entries */
size_t queue_depth;
/* max payload size for fuse requests*/
size_t max_req_payload_sz;
/* size of a single queue */
size_t queue_mem_size;
unsigned int started_threads;
unsigned int failed_threads;
/* Avoid sending queue entries before FUSE_INIT reply*/
sem_t init_sem;
pthread_cond_t thread_start_cond;
pthread_mutex_t thread_start_mutex;
/* pointer to the first queue */
struct fuse_ring_queue *queues;
};
static size_t
fuse_ring_queue_size(const size_t q_depth)
{
const size_t req_size = sizeof(struct fuse_ring_ent) * q_depth;
return sizeof(struct fuse_ring_queue) + req_size;
}
static struct fuse_ring_queue *
fuse_uring_get_queue(struct fuse_ring_pool *fuse_ring, int qid)
{
void *ptr =
((char *)fuse_ring->queues) + (qid * fuse_ring->queue_mem_size);
return ptr;
}
/**
* return a pointer to the 80B area
*/
static void *fuse_uring_get_sqe_cmd(struct io_uring_sqe *sqe)
{
return (void *)&sqe->cmd[0];
}
static void fuse_uring_sqe_set_req_data(struct fuse_uring_cmd_req *req,
const unsigned int qid,
const uint64_t commit_id)
{
req->qid = qid;
req->commit_id = commit_id;
req->flags = 0;
}
static void
fuse_uring_sqe_prepare(struct io_uring_sqe *sqe, struct fuse_ring_ent *req,
__u32 cmd_op)
{
/* These fields should be written once, never change */
sqe->opcode = IORING_OP_URING_CMD;
/*
* IOSQE_FIXED_FILE: fd is the index to the fd *array*
* given to io_uring_register_files()
*/
sqe->flags = IOSQE_FIXED_FILE;
sqe->fd = 0;
sqe->rw_flags = 0;
sqe->ioprio = 0;
sqe->off = 0;
io_uring_sqe_set_data(sqe, req);
sqe->cmd_op = cmd_op;
sqe->__pad1 = 0;
}
static int fuse_uring_commit_sqe(struct fuse_ring_pool *ring_pool,
struct fuse_ring_queue *queue,
struct fuse_ring_ent *ring_ent)
{
const bool locked = !ring_pool->single_issuer;
struct fuse_session *se = ring_pool->se;
struct fuse_uring_req_header *rrh = ring_ent->req_header;
struct fuse_out_header *out = (struct fuse_out_header *)&rrh->in_out;
struct fuse_uring_ent_in_out *ent_in_out =
(struct fuse_uring_ent_in_out *)&rrh->ring_ent_in_out;
struct io_uring_sqe *sqe;
/*
* Multi-issuer: serialise every submission-side SQ access under
* ring_lock. Single-issuer: only the uring thread submits, so skip the
* lock and batch inline replies (cqe_processing), flushed by the next
* submit_and_wait().
*/
if (locked)
pthread_mutex_lock(&queue->ring_lock);
sqe = io_uring_get_sqe(&queue->ring);
if (sqe == NULL) {
/* This is an impossible condition, unless there is a bug.
* The kernel sent back an SQEs, which is assigned to a request.
* There is no way to get out of SQEs, as the number of
* SQEs matches the number tof requests.
*/
if (locked)
pthread_mutex_unlock(&queue->ring_lock);
se->error = -EIO;
fuse_log(FUSE_LOG_ERR, "Failed to get a ring SQEs\n");
return -EIO;
}
ring_ent->last_cmd = FUSE_IO_URING_CMD_COMMIT_AND_FETCH;
fuse_uring_sqe_prepare(sqe, ring_ent, ring_ent->last_cmd);
fuse_uring_sqe_set_req_data(fuse_uring_get_sqe_cmd(sqe), queue->qid,
ring_ent->req_commit_id);
if (se->debug) {
fuse_log(FUSE_LOG_DEBUG, " unique: %" PRIu64 ", result=%d\n",
out->unique, ent_in_out->payload_sz);
}
if (!atomic_load_explicit(&queue->cqe_processing, memory_order_relaxed))
io_uring_submit(&queue->ring);
if (locked)
pthread_mutex_unlock(&queue->ring_lock);
return 0;
}
int fuse_req_get_payload(fuse_req_t req, char **payload, size_t *payload_sz,
void **mr)
{
struct fuse_ring_ent *ring_ent;
/* Not possible without io-uring interface */
if (!req->flags.is_uring)
return -EINVAL;
ring_ent = container_of(req, struct fuse_ring_ent, req);
*payload = ring_ent->op_payload;
*payload_sz = ring_ent->req_payload_sz;
/*
* For now unused, but will be used later when the application can
* allocate the buffers itself and register them for rdma.
*/
if (mr)
*mr = NULL;
return 0;
}
int send_reply_uring(fuse_req_t req, int error, const void *arg, size_t argsize)
{
int res;
struct fuse_ring_ent *ring_ent =
container_of(req, struct fuse_ring_ent, req);
struct fuse_uring_req_header *rrh = ring_ent->req_header;
struct fuse_out_header *out = (struct fuse_out_header *)&rrh->in_out;
struct fuse_uring_ent_in_out *ent_in_out =
(struct fuse_uring_ent_in_out *)&rrh->ring_ent_in_out;
struct fuse_ring_queue *queue = ring_ent->ring_queue;
struct fuse_ring_pool *ring_pool = queue->ring_pool;
size_t max_payload_sz = ring_pool->max_req_payload_sz;
if (argsize > max_payload_sz) {
fuse_log(FUSE_LOG_ERR, "argsize %zu exceeds buffer size %zu",
argsize, max_payload_sz);
error = -EINVAL;
} else if (argsize) {
if (arg != ring_ent->op_payload)
memcpy(ring_ent->op_payload, arg, argsize);
}
ent_in_out->payload_sz = argsize;
out->error = error;
out->unique = req->unique;
res = fuse_uring_commit_sqe(ring_pool, queue, ring_ent);
fuse_free_req(req);
return res;
}
int fuse_reply_data_uring(fuse_req_t req, struct fuse_bufvec *bufv,
enum fuse_buf_copy_flags flags)
{
struct fuse_ring_ent *ring_ent =
container_of(req, struct fuse_ring_ent, req);
struct fuse_ring_queue *queue = ring_ent->ring_queue;
struct fuse_ring_pool *ring_pool = queue->ring_pool;
struct fuse_uring_req_header *rrh = ring_ent->req_header;
struct fuse_out_header *out = (struct fuse_out_header *)&rrh->in_out;
struct fuse_uring_ent_in_out *ent_in_out =
(struct fuse_uring_ent_in_out *)&rrh->ring_ent_in_out;
size_t max_payload_sz = ring_ent->req_payload_sz;
struct fuse_bufvec dest_vec = FUSE_BUFVEC_INIT(max_payload_sz);
int res;
dest_vec.buf[0].mem = ring_ent->op_payload;
dest_vec.buf[0].size = max_payload_sz;
res = fuse_buf_copy(&dest_vec, bufv, flags);
out->error = res < 0 ? res : 0;
out->unique = req->unique;
ent_in_out->payload_sz = res > 0 ? res : 0;
res = fuse_uring_commit_sqe(ring_pool, queue, ring_ent);
fuse_free_req(req);
return res;
}
/**
* Copy the iov into the ring buffer and submit and commit/fetch sqe
*/
int fuse_send_msg_uring(fuse_req_t req, struct iovec *iov, int count)
{
struct fuse_ring_ent *ring_ent =
container_of(req, struct fuse_ring_ent, req);
struct fuse_ring_queue *queue = ring_ent->ring_queue;
struct fuse_ring_pool *ring_pool = queue->ring_pool;
struct fuse_uring_req_header *rrh = ring_ent->req_header;
struct fuse_out_header *out = (struct fuse_out_header *)&rrh->in_out;
struct fuse_uring_ent_in_out *ent_in_out =
(struct fuse_uring_ent_in_out *)&rrh->ring_ent_in_out;
size_t max_buf = ring_pool->max_req_payload_sz;
size_t len = 0;
int res = 0;
/* copy iov into the payload, idx=0 is the header section */
for (int idx = 1; idx < count; idx++) {
const struct iovec *cur = &iov[idx];
if (len + cur->iov_len > max_buf) {
fuse_log(FUSE_LOG_ERR,
"iov[%d] exceeds buffer size %zu",
idx, max_buf);
res = -EINVAL; /* Gracefully handle this? */
break;
}
memcpy((char *)ring_ent->op_payload + len, cur->iov_base, cur->iov_len);
len += cur->iov_len;
}
ent_in_out->payload_sz = len;
out->error = res;
out->unique = req->unique;
out->len = len;
return fuse_uring_commit_sqe(ring_pool, queue, ring_ent);
}
static int fuse_queue_setup_io_uring(struct io_uring *ring, size_t qid,
size_t depth, int fd, int evfd,
bool single_issuer)
{
int rc;
struct io_uring_params params = {0};
int files[2] = { fd, evfd };
depth += 1; /* for the eventfd poll SQE */
params.flags = IORING_SETUP_SQE128;
/* Replies are batched and flushed in one io_uring_enter; don't let a
* single failing commit SQE stall submission of the rest of the batch.
*/
params.flags |= IORING_SETUP_SUBMIT_ALL;
/* Avoid cq overflow */
params.flags |= IORING_SETUP_CQSIZE;
params.cq_entries = depth * 2;
/* These flags should help to increase performance, but actually
* make it a bit slower - reason should get investigated.
*/
if (0) {
/* Has the main slow down effect */
params.flags |= IORING_SETUP_SINGLE_ISSUER;
// params.flags |= IORING_SETUP_DEFER_TASKRUN;
params.flags |= IORING_SETUP_TASKRUN_FLAG;
/* Second main effect to make it slower */
params.flags |= IORING_SETUP_COOP_TASKRUN;
}
rc = io_uring_queue_init_params(depth, ring, &params);
if (rc != 0) {
fuse_log(FUSE_LOG_ERR, "Failed to setup qid %zu: %d (%s)\n",
qid, rc, strerror(-rc));
return rc;
}
rc = io_uring_register_files(ring, files, 1);
if (rc != 0) {
rc = -errno;
fuse_log(FUSE_LOG_ERR,
"Failed to register files for ring idx %zu: %s",
qid, strerror(errno));
return rc;
}
if (single_issuer) {
/*
* Only fuse_uring_thread() issues io_uring_enter() on this
* ring, so the registered ring-fd index is valid. Non-fatal -
* older kernels just keep using the normal ring fd.
*/
rc = io_uring_register_ring_fd(ring);
if (rc < 0)
fuse_log(FUSE_LOG_DEBUG,
"qid=%zu register_ring_fd failed: %s\n",
qid, strerror(-rc));
}
return 0;
}
static void fuse_session_destruct_uring(struct fuse_ring_pool *fuse_ring)
{
for (size_t qid = 0; qid < fuse_ring->nr_queues; qid++) {
struct fuse_ring_queue *queue =
fuse_uring_get_queue(fuse_ring, qid);
if (queue->tid != 0) {
uint64_t value = 1ULL;
int rc;
rc = write(queue->eventfd, &value, sizeof(value));
if (rc != sizeof(value))
fprintf(stderr,
"Wrote to eventfd=%d err=%s: rc=%d\n",
queue->eventfd, strerror(errno), rc);
pthread_cancel(queue->tid);
pthread_join(queue->tid, NULL);
queue->tid = 0;
}
if (queue->eventfd >= 0) {
close(queue->eventfd);
queue->eventfd = -1;
}
if (queue->ring.ring_fd != -1)
io_uring_queue_exit(&queue->ring);
for (size_t idx = 0; idx < fuse_ring->queue_depth; idx++) {
struct fuse_ring_ent *ent = &queue->ent[idx];
munmap(ent->op_payload, ent->req_payload_sz);
munmap(ent->req_header, queue->req_header_sz);
}
pthread_mutex_destroy(&queue->ring_lock);
}
free(fuse_ring->queues);
pthread_cond_destroy(&fuse_ring->thread_start_cond);
pthread_mutex_destroy(&fuse_ring->thread_start_mutex);
free(fuse_ring);
}
static int fuse_uring_register_ent(struct fuse_ring_queue *queue,
struct fuse_ring_ent *ent)
{
struct io_uring_sqe *sqe;
sqe = io_uring_get_sqe(&queue->ring);
if (sqe == NULL) {
/*
* All SQEs are idle here - no good reason this
* could fail
*/
fuse_log(FUSE_LOG_ERR, "Failed to get all ring SQEs");
return -EIO;
}
ent->last_cmd = FUSE_IO_URING_CMD_REGISTER;
fuse_uring_sqe_prepare(sqe, ent, ent->last_cmd);
/* only needed for fetch */
ent->iov[0].iov_base = ent->req_header;
ent->iov[0].iov_len = queue->req_header_sz;
ent->iov[1].iov_base = ent->op_payload;
ent->iov[1].iov_len = ent->req_payload_sz;
sqe->addr = (uint64_t)(ent->iov);
sqe->len = 2;
/* this is a fetch, kernel does not read commit id */
fuse_uring_sqe_set_req_data(fuse_uring_get_sqe_cmd(sqe), queue->qid, 0);
return 0;
}
static int fuse_uring_register_queue(struct fuse_ring_queue *queue)
{
struct fuse_ring_pool *ring_pool = queue->ring_pool;
unsigned int sq_ready;
struct io_uring_sqe *sqe;
for (size_t idx = 0; idx < ring_pool->queue_depth; idx++) {
struct fuse_ring_ent *ent = &queue->ent[idx];
int res;
res = fuse_uring_register_ent(queue, ent);
if (res != 0)
return res;
}
sq_ready = io_uring_sq_ready(&queue->ring);
if (sq_ready != ring_pool->queue_depth) {
fuse_log(FUSE_LOG_ERR,
"SQE ready mismatch, expected %zu got %u\n",
ring_pool->queue_depth, sq_ready);
return -EINVAL;
}
/* Poll SQE for the eventfd to wake up on teardown */
sqe = io_uring_get_sqe(&queue->ring);
if (sqe == NULL) {
fuse_log(FUSE_LOG_ERR, "Failed to get eventfd SQE");
return -EIO;
}
io_uring_prep_poll_add(sqe, queue->eventfd, POLLIN);
io_uring_sqe_set_data(sqe, (void *)(uintptr_t)queue->eventfd);
/* Only preparation until here, no submission yet */
return 0;
}
static struct fuse_ring_pool *fuse_create_ring(struct fuse_session *se)
{
struct fuse_ring_pool *fuse_ring = NULL;
const size_t nr_queues = get_nprocs_conf();
size_t payload_sz = se->bufsize - FUSE_BUFFER_HEADER_SIZE;
size_t queue_sz;
if (se->debug)
fuse_log(FUSE_LOG_DEBUG, "starting io-uring q-depth=%d\n",
se->uring.q_depth);
fuse_ring = calloc(1, sizeof(*fuse_ring));
if (fuse_ring == NULL) {
fuse_log(FUSE_LOG_ERR, "Allocating the ring failed\n");
goto err;
}
queue_sz = fuse_ring_queue_size(se->uring.q_depth);
fuse_ring->queues = calloc(1, queue_sz * nr_queues);
if (fuse_ring->queues == NULL) {
fuse_log(FUSE_LOG_ERR, "Allocating the queues failed\n");
goto err;
}
fuse_ring->se = se;
fuse_ring->nr_queues = nr_queues;
fuse_ring->queue_depth = se->uring.q_depth;
fuse_ring->max_req_payload_sz = payload_sz;
fuse_ring->queue_mem_size = queue_sz;
fuse_ring->single_issuer = se->conn.io_uring_single_issuer;
/*
* very basic queue initialization, that cannot fail and will
* allow easy cleanup if something (like mmap) fails in the middle
* below
*/
for (size_t qid = 0; qid < nr_queues; qid++) {
struct fuse_ring_queue *queue =
fuse_uring_get_queue(fuse_ring, qid);
queue->ring.ring_fd = -1;
queue->qid = qid;
queue->ring_pool = fuse_ring;
queue->eventfd = -1;
pthread_mutex_init(&queue->ring_lock, NULL);
}
pthread_cond_init(&fuse_ring->thread_start_cond, NULL);
pthread_mutex_init(&fuse_ring->thread_start_mutex, NULL);
sem_init(&fuse_ring->init_sem, 0, 0);
return fuse_ring;
err:
if (fuse_ring)
fuse_session_destruct_uring(fuse_ring);
return NULL;
}
static void fuse_uring_resubmit(struct fuse_ring_queue *queue,
struct fuse_ring_ent *ent)
{
const bool locked = !queue->ring_pool->single_issuer;
struct io_uring_sqe *sqe;
if (locked)
pthread_mutex_lock(&queue->ring_lock);
sqe = io_uring_get_sqe(&queue->ring);
if (sqe == NULL) {
/* This is an impossible condition, unless there is a bug.
* The kernel sent back an SQEs, which is assigned to a request.
* There is no way to get out of SQEs, as the number of
* SQEs matches the number tof requests.
*/
if (locked)
pthread_mutex_unlock(&queue->ring_lock);
queue->ring_pool->se->error = -EIO;
fuse_log(FUSE_LOG_ERR, "Failed to get a ring SQEs\n");
return;
}
fuse_uring_sqe_prepare(sqe, ent, ent->last_cmd);
switch (ent->last_cmd) {
case FUSE_IO_URING_CMD_REGISTER:
sqe->addr = (uint64_t)(ent->iov);
sqe->len = 2;
fuse_uring_sqe_set_req_data(fuse_uring_get_sqe_cmd(sqe),
queue->qid, 0);
break;
case FUSE_IO_URING_CMD_COMMIT_AND_FETCH:
fuse_uring_sqe_set_req_data(fuse_uring_get_sqe_cmd(sqe),
queue->qid, ent->req_commit_id);
break;
default:
fuse_log(FUSE_LOG_ERR, "Unknown command type: %d\n",
ent->last_cmd);
queue->ring_pool->se->error = -EINVAL;
break;
}
if (!atomic_load_explicit(&queue->cqe_processing, memory_order_relaxed))
io_uring_submit(&queue->ring);
if (locked)
pthread_mutex_unlock(&queue->ring_lock);
}
static void fuse_uring_handle_cqe(struct fuse_ring_queue *queue,
struct io_uring_cqe *cqe)
{
struct fuse_ring_ent *ent = io_uring_cqe_get_data(cqe);
if (!ent) {
fuse_log(FUSE_LOG_ERR,
"cqe=%p io_uring_cqe_get_data returned NULL\n", cqe);
return;
}
struct fuse_req *req = &ent->req;
struct fuse_ring_pool *fuse_ring = queue->ring_pool;
struct fuse_uring_req_header *rrh = ent->req_header;
struct fuse_in_header *in = (struct fuse_in_header *)&rrh->in_out;
const struct fuse_uring_ent_in_out *ent_in_out = &rrh->ring_ent_in_out;
ent->req_commit_id = ent_in_out->commit_id;
if (unlikely(ent->req_commit_id == 0)) {
/*
* If this happens kernel will not find the response - it will
* be stuck forever - better to abort immediately.
*/
fuse_log(FUSE_LOG_ERR, "Received invalid commit_id=0\n");
abort();
}
memset(&req->flags, 0, sizeof(req->flags));
memset(&req->u, 0, sizeof(req->u));
req->flags.is_uring = 1;
req->ref_cnt++;
req->ch = NULL; /* not needed for uring */
req->interrupted = 0;
list_init_req(req);
fuse_session_process_uring_cqe(fuse_ring->se, req, in, &rrh->op_in,
ent->op_payload, ent_in_out->payload_sz);
}
static int fuse_uring_queue_handle_cqes(struct fuse_ring_queue *queue)
{
struct fuse_ring_pool *ring_pool = queue->ring_pool;
struct fuse_session *se = ring_pool->se;
size_t num_completed = 0;
struct io_uring_cqe *cqe;
unsigned int head;
struct fuse_ring_ent *ent;
int ret = 0;
io_uring_for_each_cqe(&queue->ring, head, cqe) {
int err = 0;
num_completed++;
err = cqe->res;
if (unlikely(err != 0)) {
if (err > 0 && ((uintptr_t)io_uring_cqe_get_data(cqe) ==
(unsigned int)queue->eventfd)) {
/* teardown from eventfd */
return -ENOTCONN;
}
switch (err) {
case -EAGAIN:
fallthrough;
case -EINTR:
ent = io_uring_cqe_get_data(cqe);
fuse_uring_resubmit(queue, ent);
continue;
default:
break;
}
/* -ENOTCONN is ok on umount */
if (err != -ENOTCONN) {
se->error = cqe->res;
/* return first error */
if (ret == 0)
ret = err;
}
} else {
fuse_uring_handle_cqe(queue, cqe);
}
}
if (num_completed)
io_uring_cq_advance(&queue->ring, num_completed);
return ret == 0 ? 0 : num_completed;
}
/**
* In per-core-queue configuration we have thread per core - the thread
* to that core
*/
static void fuse_uring_set_thread_core(int qid)
{
cpu_set_t mask;
int rc;
CPU_ZERO(&mask);
CPU_SET(qid, &mask);
rc = sched_setaffinity(0, sizeof(cpu_set_t), &mask);
if (rc != 0)
fuse_log(FUSE_LOG_ERR, "Failed to bind qid=%d to its core: %s\n",
qid, strerror(errno));
if (0) {
const int policy = SCHED_IDLE;
const struct sched_param param = {
.sched_priority = sched_get_priority_min(policy),
};
/* Set the lowest possible priority, so that the application
* submitting requests is not moved away from the current core.
*/
rc = sched_setscheduler(0, policy, &param);
if (rc != 0)
fuse_log(FUSE_LOG_ERR, "Failed to set scheduler: %s\n",
strerror(errno));
}
}
static void *alloc_local(size_t size)
{
void *p = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED)
return NULL;
syscall(SYS_mbind, p, size, MPOL_LOCAL, NULL, 0, 0);
if (madvise(p, size, MADV_POPULATE_WRITE) < 0)
return NULL;
return p;
}
/*
* @return negative error code or io-uring file descriptor
*/
static int fuse_uring_init_queue(struct fuse_ring_queue *queue)
{
struct fuse_ring_pool *ring = queue->ring_pool;
struct fuse_session *se = ring->se;
int res;
size_t page_sz = sysconf(_SC_PAGESIZE);
queue->eventfd = eventfd(0, EFD_CLOEXEC);
if (queue->eventfd < 0) {
res = -errno;
fuse_log(FUSE_LOG_ERR,
"Failed to create eventfd for qid %d: %s\n",
queue->qid, strerror(errno));
return res;
}
res = fuse_queue_setup_io_uring(&queue->ring, queue->qid,
ring->queue_depth, se->fd,
queue->eventfd, ring->single_issuer);
if (res != 0) {
fuse_log(FUSE_LOG_ERR, "qid=%d io_uring init failed\n",
queue->qid);
return res;
}
queue->req_header_sz = ROUND_UP(sizeof(struct fuse_uring_req_header),
page_sz);
for (size_t idx = 0; idx < ring->queue_depth; idx++) {
struct fuse_ring_ent *ring_ent = &queue->ent[idx];
struct fuse_req *req = &ring_ent->req;
ring_ent->ring_queue = queue;
/*
* Also allocate the header to have it page aligned, which
* is a requirement for page pinning
*/
ring_ent->req_header = alloc_local(queue->req_header_sz);
if (!ring_ent->req_header)
return -ENOMEM;
ring_ent->req_payload_sz = ring->max_req_payload_sz;
ring_ent->op_payload = alloc_local(ring_ent->req_payload_sz);
if (!ring_ent->op_payload)
return -ENOMEM;
req->se = se;
pthread_mutex_init(&req->lock, NULL);
req->flags.is_uring = 1;
req->ref_cnt = 1; /* extra ref to avoid destruction */
list_init_req(req);
}
res = fuse_uring_register_queue(queue);
if (res != 0) {
fuse_log(
FUSE_LOG_ERR,
"Grave fuse-uring error on preparing SQEs, aborting\n");
se->error = -EIO;
fuse_session_exit(se);
return res;
}
return queue->ring.ring_fd;
}
static void *fuse_uring_thread(void *arg)
{
struct fuse_ring_queue *queue = arg;
struct fuse_ring_pool *ring_pool = queue->ring_pool;
struct fuse_session *se = ring_pool->se;
const bool single_issuer = ring_pool->single_issuer;
int err;
char thread_name[16] = { 0 };
snprintf(thread_name, 16, "fuse-ring-%d", queue->qid);
thread_name[15] = '\0';
fuse_set_thread_name(thread_name);
fuse_uring_set_thread_core(queue->qid);
err = fuse_uring_init_queue(queue);
pthread_mutex_lock(&ring_pool->thread_start_mutex);
if (err < 0)
ring_pool->failed_threads++;
ring_pool->started_threads++;
pthread_cond_broadcast(&ring_pool->thread_start_cond);
pthread_mutex_unlock(&ring_pool->thread_start_mutex);
if (err < 0) {
fuse_log(FUSE_LOG_ERR, "qid=%d queue setup failed\n",
queue->qid);
goto err_non_fatal;
}
sem_wait(&ring_pool->init_sem);
/*
* Multi-issuer flushes the registration SQEs here - safe without
* ring_lock, no request can reach this queue yet. Single-issuer's
* first submit_and_wait() below flushes them instead.
*/
if (!single_issuer)
io_uring_submit(&queue->ring);
/* Not using fuse_session_exited(se), as that cannot be inlined */
while (!atomic_load_explicit(&se->mt_exited, memory_order_relaxed)) {
/*
* Single-issuer: one combined submit_and_wait() flushes the
* previous iteration's batched replies and waits. Multi-issuer:
* split it - wait only here (no ring_lock) so off-thread
* repliers keep submitting; the batched inline replies are
* flushed below. The returned cqe is ignored; handle_cqes()
* re-scans and advances the CQ.
*/
if (single_issuer) {
io_uring_submit_and_wait(&queue->ring, 1);
} else {
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&queue->ring, &cqe);
}
/*
* Batch inline replies (commit_sqe()/resubmit()) across cqe
* handling. Lock-free: the flag only gates who submits, while
* the SQ stays serialised by ring_lock, so a reply that batched
* here is always flushed by the submit below before the next
* wait - it is never stranded.
*/
atomic_store_explicit(&queue->cqe_processing, true,
memory_order_relaxed);
err = fuse_uring_queue_handle_cqes(queue);
if (err < 0)
goto err;
atomic_store_explicit(&queue->cqe_processing, false,
memory_order_relaxed);
/*
* Multi-issuer does not use io_uring_submit_and_wait(),
* but io_uring_wait_cqe() and locked io_uring_submit().
*/
if (!single_issuer) {
pthread_mutex_lock(&queue->ring_lock);
io_uring_submit(&queue->ring);
pthread_mutex_unlock(&queue->ring_lock);
}
}
return NULL;
err:
fuse_session_exit(se);
err_non_fatal:
return NULL;
}
static int fuse_uring_start_ring_threads(struct fuse_ring_pool *ring)
{
int rc = 0;
for (size_t qid = 0; qid < ring->nr_queues; qid++) {
struct fuse_ring_queue *queue = fuse_uring_get_queue(ring, qid);
rc = pthread_create(&queue->tid, NULL, fuse_uring_thread, queue);
if (rc != 0)
break;
}
return rc;
}
static int fuse_uring_sanity_check(const struct fuse_session *se)
{
if (se->uring.q_depth == 0) {
fuse_log(FUSE_LOG_ERR, "io-uring queue depth must be > 0\n");
return -EINVAL;
}
_Static_assert(sizeof(struct fuse_uring_cmd_req) <=
FUSE_URING_MAX_SQE128_CMD_DATA,
"SQE128_CMD_DATA has 80B cmd data");
return 0;
}
int fuse_uring_start(struct fuse_session *se)
{
int err = 0;
struct fuse_ring_pool *fuse_ring;
fuse_uring_sanity_check(se);
fuse_ring = fuse_create_ring(se);
if (fuse_ring == NULL) {
err = -EADDRNOTAVAIL;
goto err;
}
se->uring.pool = fuse_ring;
err = fuse_uring_start_ring_threads(fuse_ring);
if (err)
goto err;
/*
* Wait for all threads to start or to fail
*/
pthread_mutex_lock(&fuse_ring->thread_start_mutex);
while (fuse_ring->started_threads < fuse_ring->nr_queues)
pthread_cond_wait(&fuse_ring->thread_start_cond,
&fuse_ring->thread_start_mutex);
if (fuse_ring->failed_threads != 0)
err = -EADDRNOTAVAIL;
pthread_mutex_unlock(&fuse_ring->thread_start_mutex);
err:
if (err) {
/* Note all threads need to have been started */
if (fuse_ring)
fuse_session_destruct_uring(fuse_ring);
se->uring.pool = NULL;
}
return err;
}
int fuse_uring_stop(struct fuse_session *se)
{
struct fuse_ring_pool *ring = se->uring.pool;
if (ring == NULL)
return 0;
fuse_session_destruct_uring(ring);
se->uring.pool = NULL;
return 0;
}
void fuse_uring_wake_ring_threads(struct fuse_session *se)
{
struct fuse_ring_pool *ring = se->uring.pool;
/* Wake up the threads to let them send SQEs */
for (size_t qid = 0; qid < ring->nr_queues; qid++)
sem_post(&ring->init_sem);
}