mirror of
https://github.com/libfuse/libfuse.git
synced 2026-07-07 14:47:41 +08:00
1615ff35af
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>
1043 lines
26 KiB
C
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, ¶ms);
|
|
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, ¶m);
|
|
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);
|
|
}
|