public inbox for io-uring@vger.kernel.org
 help / color / mirror / Atom feed
From: Jens Axboe <axboe@kernel.dk>
To: io-uring@vger.kernel.org
Cc: linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org, tglx@kernel.org, mingo@redhat.com,
	peterz@infradead.org, Jens Axboe <axboe@kernel.dk>
Subject: [PATCH 11/15] io_uring: enable handing submitter identity to an io-wq worker
Date: Fri, 11 Sep 2026 09:41:01 -0600	[thread overview]
Message-ID: <20260911154148.644489-12-axboe@kernel.dk> (raw)
In-Reply-To: <20260911154148.644489-1-axboe@kernel.dk>

If a blockable request issued inline from io_uring_enter() blocks, hand
the submitter's identity to an idle io-wq worker. The worker finishes
the io_uring_enter() call and returns to userspace as the submitter,
while the original task finishes the request and lives on as the
worker.

Requests are still issued with IO_URING_F_NONBLOCK, but any sleep they
hit anyway (page fault, lock, allocation stall) no longer blocks the
submitter.

Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
 include/linux/io_uring.h       |   4 +
 include/linux/io_uring_types.h |  34 ++++
 io_uring/Makefile              |   1 +
 io_uring/handoff.c             | 326 +++++++++++++++++++++++++++++++++
 io_uring/handoff.h             | 102 +++++++++++
 io_uring/io_uring.c            |  84 +++++++--
 io_uring/io_uring.h            |  36 +++-
 io_uring/msg_ring.c            |   2 +-
 io_uring/rw.c                  |   2 +-
 io_uring/tctx.c                |   6 +-
 io_uring/tw.c                  |  14 +-
 io_uring/uring_cmd.c           |   5 +-
 12 files changed, 596 insertions(+), 20 deletions(-)
 create mode 100644 io_uring/handoff.c
 create mode 100644 io_uring/handoff.h

diff --git a/include/linux/io_uring.h b/include/linux/io_uring.h
index 969de22c3d0f..4137276ba8f1 100644
--- a/include/linux/io_uring.h
+++ b/include/linux/io_uring.h
@@ -61,8 +61,12 @@ static inline int io_uring_fork(struct task_struct *tsk)
 #endif
 
 /* called from sched_submit_work() when a PF_IO_HANDOFF task blocks */
+#if defined(CONFIG_IO_URING) && defined(CONFIG_THREAD_HANDOFF)
+void io_uring_task_sleeping(struct task_struct *tsk);
+#else
 static inline void io_uring_task_sleeping(struct task_struct *tsk)
 {
 }
+#endif
 
 #endif
diff --git a/include/linux/io_uring_types.h b/include/linux/io_uring_types.h
index 0b0d73688b8c..81bc4810fcab 100644
--- a/include/linux/io_uring_types.h
+++ b/include/linux/io_uring_types.h
@@ -4,9 +4,11 @@
 #include <linux/blk_plug.h>
 #include <linux/hashtable.h>
 #include <linux/task_work.h>
+#include <linux/thread_handoff.h>
 #include <linux/bitmap.h>
 #include <linux/llist.h>
 #include <linux/uio.h>
+#include <linux/signal_types.h>
 #include <uapi/linux/io_uring.h>
 
 struct iou_loop_params;
@@ -139,12 +141,42 @@ struct io_br_sel {
  */
 #define IO_RINGFD_REG_MAX 16
 
+/* handoff state of a submitter that blocked inline, see io_uring/handoff.c */
+struct io_handoff {
+	/* the request being issued, while a handoff is possible */
+	struct io_kiocb			*req;
+	/* its ring and io-wq pool, @req is the demoted task's after that */
+	struct io_ring_ctx		*ctx;
+	bool				bound;
+	/* the submitter's signal mask while blocking issues run without */
+	sigset_t			sigmask;
+	bool				sigsaved;
+	/* the task the identity came from, and the task refs it held */
+	struct task_struct		*src;
+	unsigned int			src_refs;
+	struct thread_handoff_stats	stats;
+	/* io_uring_enter() arguments, to resume the syscall */
+	struct file			*file;
+	u32				to_submit;
+	/* SQEs consumed so far by this syscall, across handoffs */
+	u32				consumed;
+	u32				min_complete;
+	u32				flags;
+	const void __user		*argp;
+	size_t				argsz;
+};
+
 struct io_uring_task {
 	/* submission side */
 	int				cached_refs;
 	const struct io_ring_ctx 	*last;
 	struct task_struct		*task;
+	/* serializes ->task changes against off-task reference puts */
+	raw_spinlock_t			task_ref_lock;
 	struct io_wq			*io_wq;
+#ifdef CONFIG_THREAD_HANDOFF
+	struct io_handoff		handoff;
+#endif
 	/*
 	 * Consumer cursor for ->task_list. Only popped by the task itself,
 	 * or by ->fallback_work once the task can no longer run task_work.
@@ -298,6 +330,8 @@ struct io_submit_state {
 	bool			need_plug;
 	bool			cq_flush;
 	unsigned short		submit_nr;
+	/* cached SQ head at the start of the batch */
+	unsigned int		sq_head;
 	/* the submitting task's plug, lives on its stack */
 	struct blk_plug		*plug;
 };
diff --git a/io_uring/Makefile b/io_uring/Makefile
index c54e328d1410..9fd99118d26a 100644
--- a/io_uring/Makefile
+++ b/io_uring/Makefile
@@ -18,6 +18,7 @@ obj-$(CONFIG_IO_URING)		+= io_uring.o opdef.o kbuf.o rsrc.o notif.o \
 
 obj-$(CONFIG_IO_URING_ZCRX)	+= zcrx.o
 obj-$(CONFIG_IO_WQ)		+= io-wq.o
+obj-$(CONFIG_THREAD_HANDOFF)	+= handoff.o
 obj-$(CONFIG_FUTEX)		+= futex.o
 obj-$(CONFIG_EPOLL)		+= epoll.o
 obj-$(CONFIG_NET_RX_BUSY_POLL)	+= napi.o
diff --git a/io_uring/handoff.c b/io_uring/handoff.c
new file mode 100644
index 000000000000..25e9e06812a7
--- /dev/null
+++ b/io_uring/handoff.c
@@ -0,0 +1,326 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Inline issue handoff: if an inline issue blocks, the submitter's identity
+ * moves to an idle io-wq worker which returns to userspace as the submitter,
+ * while the submitter finishes the request as the worker.
+ *
+ * Copyright (C) 2026 Jens Axboe
+ */
+#include <linux/kernel.h>
+#include <linux/errno.h>
+#include <linux/sched.h>
+#include <linux/sched/task.h>
+#include <linux/sched/signal.h>
+#include <linux/sched/task_stack.h>
+#include <linux/signal.h>
+#include <linux/task_work.h>
+#include <linux/thread_handoff.h>
+#include <linux/io_uring.h>
+#include <linux/blkdev.h>
+#include <linux/fs.h>
+#include <linux/file.h>
+#include <asm/syscall.h>
+
+#include "io_uring.h"
+#include "io-wq.h"
+#include "opdef.h"
+#include "tctx.h"
+#include "handoff.h"
+
+int sysctl_io_uring_handoff __read_mostly = 1;
+
+static long io_handoff_resume(void);
+
+/* fork a spare worker upfront, so the first blockable issue has a target */
+void io_handoff_prime(struct io_uring_task *tctx, struct io_ring_ctx *ctx)
+{
+	if (!sysctl_io_uring_handoff || !tctx->io_wq)
+		return;
+	/* handoffs are never done for these, see __io_handoff_begin() */
+	if (ctx->flags & (IORING_SETUP_IOPOLL | IORING_SETUP_SQPOLL |
+			  IORING_SETUP_SQ_REWIND))
+		return;
+	io_wq_handoff_spare(tctx->io_wq, true, true);
+}
+
+/*
+ * Blocking inline issues run with an io-wq worker's signal mask, a request
+ * shouldn't fail with -EINTR because the submitter has a timer.
+ */
+static void io_handoff_block_signals(struct io_handoff *ho)
+{
+	sigset_t mask;
+
+	if (ho->sigsaved)
+		return;
+	ho->sigsaved = true;
+	ho->sigmask = current->blocked;
+	siginitsetinv(&mask, sigmask(SIGKILL) | sigmask(SIGSTOP));
+	set_current_blocked(&mask);
+}
+
+void __io_handoff_restore_signals(struct io_handoff *ho)
+{
+	ho->sigsaved = false;
+	set_current_blocked(&ho->sigmask);
+}
+
+/*
+ * Arm a handoff for the inline issue of @req. Until io_handoff_end() the issue
+ * runs like on io-wq, neither normal signals nor task_work interrupt it.
+ */
+bool __io_handoff_begin(struct io_kiocb *req)
+{
+	struct io_ring_ctx *ctx = req->ctx;
+	struct io_uring_task *tctx = current->io_uring;
+	struct io_handoff *ho = &tctx->handoff;
+
+	if (!sysctl_io_uring_handoff)
+		return false;
+	/* nonblocking semantics were asked for, -EAGAIN is the answer */
+	if (req->flags & REQ_F_NOWAIT)
+		return false;
+	/* IOPOLL/SQPOLL issue differently, SQ_REWIND can't resume mid-batch */
+	if (ctx->flags & (IORING_SETUP_IOPOLL | IORING_SETUP_SQPOLL |
+			  IORING_SETUP_SQ_REWIND))
+		return false;
+	/* pollable files keep the nonblocking issue + poll retry path */
+	if (io_file_can_poll(req))
+		return false;
+	if (!tctx->io_wq)
+		return false;
+	if (!thread_handoff_allowed(current))
+		return false;
+	/* the SQ head is published while we may still be running */
+	if (io_req_sqe_copy(req, IO_URING_F_INLINE))
+		return false;
+	/* have a worker ready to take over */
+	if (!io_wq_handoff_spare(tctx->io_wq, !io_req_unbound(req), false))
+		return false;
+	/* would interrupt the issue right away, and can't be handled here */
+	if (signal_pending(current))
+		return false;
+
+	ho->req = req;
+	io_handoff_block_signals(ho);
+	current->flags |= PF_IO_HANDOFF;
+	return true;
+}
+
+/* Returns true if the identity got handed off during the issue */
+bool io_handoff_end(void)
+{
+	bool handed_off = current->flags & PF_IO_WORKER;
+
+	current->flags &= ~PF_IO_HANDOFF;
+	/* pairs with io_wq_task_work_add(), notify for work queued meanwhile */
+	smp_mb();
+	if (task_work_pending(current))
+		set_notify_signal(current);
+
+	/* once handed off the tctx isn't ours anymore */
+	if (likely(!handed_off))
+		current->io_uring->handoff.req = NULL;
+	return handed_off;
+}
+
+/* close our part of the batch and drop uring_lock for the promoted task */
+static void io_handoff_release_ring(struct io_ring_ctx *ctx,
+				    struct io_handoff *ho)
+	__releases(&ctx->uring_lock)
+{
+	lockdep_assert_held(&ctx->uring_lock);
+
+	ho->consumed += io_submit_sqes_abandon(ctx);
+	mutex_unlock(&ctx->uring_lock);
+}
+
+/* move the outstanding tctx task refs from @src to @dst, see io_put_task() */
+static void io_handoff_task_refs(struct io_uring_task *tctx,
+				 struct task_struct *src,
+				 struct task_struct *dst)
+{
+	unsigned int nr;
+
+	raw_spin_lock(&tctx->task_ref_lock);
+	nr = percpu_counter_sum(&tctx->inflight);
+	refcount_add(nr, &dst->usage);
+	WRITE_ONCE(tctx->task, dst);
+	raw_spin_unlock(&tctx->task_ref_lock);
+
+	/* dropped by the promoted task once it's done taking over */
+	tctx->handoff.src_refs = nr;
+}
+
+/* move tctx task_work queued on @task along to the tctx's new task */
+void io_handoff_tw_moved(struct io_uring_task *tctx, struct task_struct *task)
+{
+	struct task_struct *cur;
+
+	while (task_work_cancel(task, &tctx->task_work)) {
+		cur = READ_ONCE(tctx->task);
+		if (WARN_ON_ONCE(task_work_add(cur, &tctx->task_work, TWA_SIGNAL)))
+			break;
+		if (READ_ONCE(tctx->task) == cur)
+			break;
+		task = cur;
+	}
+}
+
+/* Move the io_uring task state from @src to @dst */
+static void io_handoff_move_tctx(struct io_uring_task *tctx,
+				 struct task_struct *src,
+				 struct task_struct *dst)
+{
+	struct io_tctx_node *node;
+
+	io_handoff_task_refs(tctx, src, dst);
+
+	dst->io_uring = tctx;
+	src->io_uring = NULL;
+	dst->io_uring_restrict = src->io_uring_restrict;
+	src->io_uring_restrict = NULL;
+
+	list_for_each_entry(node, &tctx->node_list, tctx_link) {
+		struct io_ring_ctx *ctx = node->ctx;
+
+		node->task = dst;
+		if (READ_ONCE(ctx->submitter_task) == src) {
+			get_task_struct(dst);
+			WRITE_ONCE(ctx->submitter_task, dst);
+			put_task_struct(src);
+		}
+	}
+
+	io_handoff_tw_moved(tctx, src);
+}
+
+/*
+ * Called from sched_submit_work() when a task blocks inside an inline issue,
+ * hand our identity to an idle worker. Can't block, uring_lock is held.
+ */
+void io_uring_task_sleeping(struct task_struct *tsk)
+{
+	struct io_uring_task *tctx = tsk->io_uring;
+	struct io_handoff *ho = &tctx->handoff;
+	struct io_kiocb *req = ho->req;
+	struct io_ring_ctx *ctx = req->ctx;
+	struct task_struct *dst;
+	bool bound;
+
+	WARN_ON_ONCE(tsk != current);
+
+	/* the issue path is touching state that needs the ring lock held */
+	if (ctx->submit_lock_depth)
+		return;
+	if (!thread_handoff_prepare(tsk))
+		return;
+
+	/* don't let the woken worker preempt us before we've committed */
+	preempt_disable();
+	bound = !io_req_unbound(req);
+	dst = io_wq_handoff_claim(tctx->io_wq, bound, io_handoff_resume);
+	if (!dst) {
+		preempt_enable();
+		return;
+	}
+
+	/* committed, @req is ours as the worker from here on */
+	ho->src = tsk;
+	ho->ctx = ctx;
+	ho->bound = bound;
+	thread_handoff_stats_take(&ho->stats);
+
+	io_handoff_release_ring(ctx, ho);
+	io_handoff_move_tctx(tctx, tsk, dst);
+	io_wq_handoff_commit(dst);
+
+	/* do what sched_submit_work() would have done for an io-wq worker */
+	io_wq_worker_sleeping(tsk);
+	preempt_enable();
+}
+
+/* the issue of @req blocked and we're a worker now, finish it like io-wq */
+int io_handoff_complete(struct io_kiocb *req, int ret)
+{
+	WARN_ON_ONCE(!io_wq_current_is_worker());
+
+	if (ret == IOU_COMPLETE) {
+		req->io_task_work.func = io_req_task_complete;
+		io_req_task_work_add(req);
+	} else if (ret == IOU_ISSUE_SKIP_COMPLETE) {
+		/* completes on its own */
+	} else if ((ret == -EAGAIN && !(req->flags & REQ_F_NOWAIT)) ||
+		   io_issue_wants_restart(ret)) {
+		/* wants a blocking retry, or got interrupted, io-wq does that */
+		io_queue_iowq(req);
+	} else {
+		io_req_task_queue_fail(req, ret);
+	}
+
+	return -EIOCBQUEUED;
+}
+
+/* runs on the promoted task, finishes io_uring_enter() for the submitter */
+static long io_handoff_resume(void)
+{
+	struct io_uring_task *tctx = current->io_uring;
+	struct io_handoff *ho = &tctx->handoff;
+	struct task_struct *src = ho->src;
+	struct io_ring_ctx *ctx = ho->ctx;
+	bool bound = ho->bound;
+	long ret;
+
+	if (WARN_ON_ONCE(thread_handoff_finish(src, &ho->stats)))
+		force_sig(SIGKILL);
+	io_wq_handoff_finished(src);
+	put_task_struct_many(src, ho->src_refs);
+	ho->src_refs = 0;
+	ho->src = NULL;
+	ho->req = NULL;
+	ho->ctx = NULL;
+
+	/* flush what the blocked batch left behind, then submit the rest */
+	io_run_task_work();
+	mutex_lock(&ctx->uring_lock);
+	io_submit_flush_completions(ctx);
+	if (ho->consumed < ho->to_submit) {
+		ret = io_submit_sqes(ctx, ho->to_submit - ho->consumed);
+		if (ret == -EIOCBQUEUED)
+			return ret;
+		if (ret > 0)
+			ho->consumed += ret;
+	}
+	/* the identity came with the mask the blocking issue ran under */
+	io_handoff_submit_end();
+	ret = ho->consumed;
+	if (ret != ho->to_submit) {
+		mutex_unlock(&ctx->uring_lock);
+	} else {
+		ret = io_uring_enter_finish(ctx, ret, ho->min_complete,
+					    ho->flags, ho->argp, ho->argsz);
+	}
+	if (!(ho->flags & IORING_ENTER_REGISTERED_RING))
+		fput(ho->file);
+
+	/* we took a worker, top the spare pool back up now the work is done */
+	io_wq_handoff_spare(tctx->io_wq, bound, true);
+
+	syscall_set_return_value(current, task_pt_regs(current),
+				 ret < 0 ? ret : 0, ret);
+	return ret;
+}
+
+/* run the worker loop after a demotion, returns once handed an identity */
+long io_uring_handoff_worker(void)
+{
+	io_wq_handoff_fn *fn;
+	long ret;
+
+	do {
+		fn = io_wq_handoff_worker();
+		ret = fn();
+	} while (ret == -EIOCBQUEUED);
+
+	return ret;
+}
diff --git a/io_uring/handoff.h b/io_uring/handoff.h
new file mode 100644
index 000000000000..833c6314d3b7
--- /dev/null
+++ b/io_uring/handoff.h
@@ -0,0 +1,102 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef IOU_HANDOFF_H
+#define IOU_HANDOFF_H
+
+#include <linux/io_uring_types.h>
+#include "opdef.h"
+#include "tw.h"
+
+/* a blocking issue got interrupted, retry on io-wq rather than restart */
+static inline bool io_issue_wants_restart(int ret)
+{
+	return ret == -ERESTARTSYS || ret == -ERESTARTNOINTR ||
+	       ret == -ERESTARTNOHAND || ret == -ERESTART_RESTARTBLOCK;
+}
+
+#ifdef CONFIG_THREAD_HANDOFF
+extern int sysctl_io_uring_handoff;
+
+bool __io_handoff_begin(struct io_kiocb *req);
+void io_handoff_prime(struct io_uring_task *tctx, struct io_ring_ctx *ctx);
+bool io_handoff_end(void);
+void __io_handoff_restore_signals(struct io_handoff *ho);
+int io_handoff_complete(struct io_kiocb *req, int ret);
+long io_uring_handoff_worker(void);
+void io_handoff_tw_moved(struct io_uring_task *tctx, struct task_struct *task);
+
+/*
+ * Stash the io_uring_enter() arguments so a promoted task can resume it, and
+ * run pending task_work so it doesn't interrupt a blocking issue later.
+ */
+static inline void io_handoff_enter(struct file *file, u32 to_submit,
+				    u32 min_complete, u32 flags,
+				    const void __user *argp, size_t argsz)
+{
+	struct io_handoff *ho = &current->io_uring->handoff;
+
+	io_run_task_work();
+	ho->file = file;
+	ho->to_submit = to_submit;
+	ho->consumed = 0;
+	ho->min_complete = min_complete;
+	ho->flags = flags;
+	ho->argp = argp;
+	ho->argsz = argsz;
+}
+
+/* a submit call is done issuing, restore the signal mask if we changed it */
+static inline void io_handoff_submit_end(void)
+{
+	struct io_handoff *ho = &current->io_uring->handoff;
+
+	if (unlikely(ho->sigsaved))
+		__io_handoff_restore_signals(ho);
+}
+
+/* if true, @req gets a blocking inline issue. Pair with io_handoff_end() */
+static inline bool io_handoff_begin(struct io_kiocb *req,
+				    const struct io_issue_def *def,
+				    unsigned int issue_flags)
+{
+	if (!(issue_flags & IO_URING_F_INLINE) || !def->blockable)
+		return false;
+	return __io_handoff_begin(req);
+}
+#else
+static inline void io_handoff_enter(struct file *file, u32 to_submit,
+				    u32 min_complete, u32 flags,
+				    const void __user *argp, size_t argsz)
+{
+}
+static inline bool io_handoff_begin(struct io_kiocb *req,
+				    const struct io_issue_def *def,
+				    unsigned int issue_flags)
+{
+	return false;
+}
+static inline void io_handoff_submit_end(void)
+{
+}
+static inline bool io_handoff_end(void)
+{
+	return false;
+}
+static inline int io_handoff_complete(struct io_kiocb *req, int ret)
+{
+	return -EFAULT;
+}
+static inline long io_uring_handoff_worker(void)
+{
+	return -EFAULT;
+}
+static inline void io_handoff_tw_moved(struct io_uring_task *tctx,
+				       struct task_struct *task)
+{
+}
+static inline void io_handoff_prime(struct io_uring_task *tctx,
+				    struct io_ring_ctx *ctx)
+{
+}
+#endif
+
+#endif
diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index 100ade1eee3e..289e9ddc8c24 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -98,6 +98,7 @@
 #include "wait.h"
 #include "bpf_filter.h"
 #include "loop.h"
+#include "handoff.h"
 
 #define SQE_COMMON_FLAGS (IOSQE_FIXED_FILE | IOSQE_IO_LINK | \
 			  IOSQE_IO_HARDLINK | IOSQE_ASYNC)
@@ -119,7 +120,7 @@
 /* requests with any of those set should undergo io_disarm_next() */
 #define IO_DISARM_MASK (REQ_F_ARM_LTIMEOUT | REQ_F_LINK_TIMEOUT | REQ_F_FAIL)
 
-static void io_queue_sqe(struct io_kiocb *req, unsigned int extra_flags);
+static int io_queue_sqe(struct io_kiocb *req, unsigned int extra_flags);
 static void __io_req_caches_free(struct io_ring_ctx *ctx);
 
 static __read_mostly DEFINE_STATIC_KEY_DEFERRED_FALSE(io_key_has_sqarray, HZ);
@@ -132,6 +133,17 @@ static int __read_mostly sysctl_io_uring_group = -1;
 
 #ifdef CONFIG_SYSCTL
 static const struct ctl_table kernel_io_uring_disabled_table[] = {
+#ifdef CONFIG_THREAD_HANDOFF
+	{
+		.procname	= "io_uring_handoff",
+		.data		= &sysctl_io_uring_handoff,
+		.maxlen		= sizeof(sysctl_io_uring_handoff),
+		.mode		= 0644,
+		.proc_handler	= proc_dointvec_minmax,
+		.extra1		= SYSCTL_ZERO,
+		.extra2		= SYSCTL_ONE,
+	},
+#endif
 	{
 		.procname	= "io_uring_disabled",
 		.data		= &sysctl_io_uring_disabled,
@@ -384,9 +396,8 @@ static void io_prep_async_work(struct io_kiocb *req)
 			should_hash = false;
 		if (should_hash || (req->flags & REQ_F_IOPOLL))
 			io_wq_hash_work(&req->work, file_inode(req->file));
-	} else if (!req->file || !S_ISBLK(file_inode(req->file)->i_mode)) {
-		if (def->unbound_nonreg_file)
-			atomic_or(IO_WQ_WORK_UNBOUND, &req->work.flags);
+	} else if (io_req_unbound(req)) {
+		atomic_or(IO_WQ_WORK_UNBOUND, &req->work.flags);
 	}
 }
 
@@ -595,10 +606,16 @@ static inline void io_put_task(struct io_kiocb *req)
 	if (likely(tctx->task == current)) {
 		tctx->cached_refs++;
 	} else {
+		struct task_struct *task;
+
+		/* ->task can change under us, see io_handoff_task_refs() */
+		raw_spin_lock(&tctx->task_ref_lock);
 		percpu_counter_sub(&tctx->inflight, 1);
+		task = tctx->task;
+		raw_spin_unlock(&tctx->task_ref_lock);
 		if (unlikely(atomic_read(&tctx->in_cancel)))
 			wake_up(&tctx->wait);
-		put_task_struct(tctx->task);
+		put_task_struct(task);
 	}
 }
 
@@ -1401,12 +1418,16 @@ static inline int __io_issue_sqe(struct io_kiocb *req,
 static int io_issue_sqe(struct io_kiocb *req, unsigned int issue_flags)
 {
 	const struct io_issue_def *def = &io_issue_defs[req->opcode];
+	bool handoff;
 	int ret;
 
 	if (unlikely(!io_assign_file(req, def, issue_flags)))
 		return -EBADF;
 
+	handoff = io_handoff_begin(req, def, issue_flags);
 	ret = __io_issue_sqe(req, issue_flags, def);
+	if (handoff && unlikely(io_handoff_end()))
+		return io_handoff_complete(req, ret);
 
 	if (ret == IOU_COMPLETE) {
 		if (issue_flags & IO_URING_F_COMPLETE_DEFER)
@@ -1590,7 +1611,7 @@ struct file *io_file_get_normal(struct io_kiocb *req, int fd)
 	return file;
 }
 
-static int io_req_sqe_copy(struct io_kiocb *req, unsigned int issue_flags)
+int io_req_sqe_copy(struct io_kiocb *req, unsigned int issue_flags)
 {
 	const struct io_cold_def *def = &io_cold_defs[req->opcode];
 
@@ -1630,7 +1651,8 @@ static void io_queue_async(struct io_kiocb *req, unsigned int issue_flags, int r
 	}
 }
 
-static inline void io_queue_sqe(struct io_kiocb *req, unsigned int extra_flags)
+/* returns -EIOCBQUEUED if the identity got handed off, we're a worker now */
+static inline int io_queue_sqe(struct io_kiocb *req, unsigned int extra_flags)
 	__must_hold(&req->ctx->uring_lock)
 {
 	unsigned int issue_flags = IO_URING_F_NONBLOCK |
@@ -1638,6 +1660,8 @@ static inline void io_queue_sqe(struct io_kiocb *req, unsigned int extra_flags)
 	int ret;
 
 	ret = io_issue_sqe(req, issue_flags);
+	if (unlikely(ret == -EIOCBQUEUED))
+		return ret;
 
 	/*
 	 * We async punt it if the file wasn't marked NOWAIT, or if the file
@@ -1645,6 +1669,7 @@ static inline void io_queue_sqe(struct io_kiocb *req, unsigned int extra_flags)
 	 */
 	if (unlikely(ret))
 		io_queue_async(req, issue_flags, ret);
+	return 0;
 }
 
 static void io_queue_sqe_fallback(struct io_kiocb *req)
@@ -1922,8 +1947,7 @@ static inline int io_submit_sqe(struct io_ring_ctx *ctx, struct io_kiocb *req,
 		return 0;
 	}
 
-	io_queue_sqe(req, IO_URING_F_INLINE);
-	return 0;
+	return io_queue_sqe(req, IO_URING_F_INLINE);
 }
 
 /*
@@ -2033,6 +2057,25 @@ static int io_submit_sqes_end(struct io_ring_ctx *ctx, unsigned int entries,
 	return ret;
 }
 
+#ifdef CONFIG_THREAD_HANDOFF
+/*
+ * The submitter blocked mid-batch, return the task refs for what it won't
+ * submit and publish the SQ head. Returns the number of entries consumed.
+ */
+unsigned int io_submit_sqes_abandon(struct io_ring_ctx *ctx)
+	__must_hold(&ctx->uring_lock)
+{
+	struct io_submit_state *state = &ctx->submit_state;
+	unsigned int consumed = ctx->cached_sq_head - state->sq_head;
+
+	WARN_ON_ONCE(state->link.head);
+	current->io_uring->cached_refs += state->submit_nr - consumed;
+	state->plug_started = false;
+	io_commit_sqring(ctx);
+	return consumed;
+}
+#endif
+
 int io_submit_sqes(struct io_ring_ctx *ctx, unsigned int nr)
 	__must_hold(&ctx->uring_lock)
 {
@@ -2052,10 +2095,12 @@ int io_submit_sqes(struct io_ring_ctx *ctx, unsigned int nr)
 	left = entries;
 	io_get_task_refs(left);
 	io_submit_state_start(&ctx->submit_state, &plug, left);
+	ctx->submit_state.sq_head = ctx->cached_sq_head;
 
 	do {
 		const struct io_uring_sqe *sqe;
 		struct io_kiocb *req;
+		int ret;
 
 		if (unlikely(!io_alloc_req(ctx, &req)))
 			break;
@@ -2064,17 +2109,24 @@ int io_submit_sqes(struct io_ring_ctx *ctx, unsigned int nr)
 			break;
 		}
 
+		ret = io_submit_sqe(ctx, req, sqe, &left);
+		/* handed off, the promoted task finishes the batch */
+		if (unlikely(ret == -EIOCBQUEUED)) {
+			if (current->plug == &plug)
+				blk_finish_plug(&plug);
+			return ret;
+		}
 		/*
 		 * Continue submitting even for sqe failure if the
 		 * ring was setup with IORING_SETUP_SUBMIT_ALL
 		 */
-		if (unlikely(io_submit_sqe(ctx, req, sqe, &left)) &&
-		    !(ctx->flags & IORING_SETUP_SUBMIT_ALL)) {
+		if (unlikely(ret) && !(ctx->flags & IORING_SETUP_SUBMIT_ALL)) {
 			left--;
 			break;
 		}
 	} while (--left);
 
+	io_handoff_submit_end();
 	return io_submit_sqes_end(ctx, entries, left);
 }
 
@@ -2656,7 +2708,7 @@ static int io_uring_getevents(struct io_ring_ctx *ctx, int ret,
 }
 
 /* Finish an io_uring_enter() call that submitted and holds the uring_lock */
-static int io_uring_enter_finish(struct io_ring_ctx *ctx, int ret, u32 min_complete,
+int io_uring_enter_finish(struct io_ring_ctx *ctx, int ret, u32 min_complete,
 			  u32 flags, const void __user *argp, size_t argsz)
 {
 	int ret2;
@@ -2733,8 +2785,16 @@ SYSCALL_DEFINE6(io_uring_enter, unsigned int, fd, u32, to_submit,
 		if (unlikely(ret))
 			goto out;
 
+		io_handoff_enter(file, to_submit, min_complete, flags, argp,
+				 argsz);
 		mutex_lock(&ctx->uring_lock);
 		ret = io_submit_sqes(ctx, to_submit);
+		/* handed off, the promoted task finishes the syscall */
+		if (unlikely(ret == -EIOCBQUEUED)) {
+			/* uring_lock was dropped for the promoted task */
+			__release(&ctx->uring_lock);
+			return io_uring_handoff_worker();
+		}
 		if (ret != to_submit) {
 			mutex_unlock(&ctx->uring_lock);
 			goto out;
diff --git a/io_uring/io_uring.h b/io_uring/io_uring.h
index 870bb4dcc415..79db0a8b9cc8 100644
--- a/io_uring/io_uring.h
+++ b/io_uring/io_uring.h
@@ -211,6 +211,10 @@ void io_free_req(struct io_kiocb *req);
 void io_queue_next(struct io_kiocb *req);
 void io_task_refs_refill(struct io_uring_task *tctx);
 bool __io_alloc_req_refill(struct io_ring_ctx *ctx);
+int io_req_sqe_copy(struct io_kiocb *req, unsigned int issue_flags);
+unsigned int io_submit_sqes_abandon(struct io_ring_ctx *ctx);
+int io_uring_enter_finish(struct io_ring_ctx *ctx, int ret, u32 min_complete,
+			  u32 flags, const void __user *argp, size_t argsz);
 
 void io_activate_pollwq(struct io_ring_ctx *ctx);
 void io_restriction_clone(struct io_restriction *dst, struct io_restriction *src);
@@ -389,13 +393,41 @@ static inline void io_put_file(struct io_kiocb *req)
 		fput(req->file);
 }
 
+/* a handed off issue keeps its issue_flags but no longer holds uring_lock */
+static inline bool io_issue_handed_off(unsigned int issue_flags)
+{
+	return !(issue_flags & IO_URING_F_UNLOCKED) &&
+		(current->flags & (PF_IO_HANDOFF | PF_IO_WORKER)) ==
+			(PF_IO_HANDOFF | PF_IO_WORKER);
+}
+
+/* which io-wq pool a request belongs in, bound unless a non-reg file op */
+static inline bool io_req_unbound(struct io_kiocb *req)
+{
+	if (!io_issue_defs[req->opcode].unbound_nonreg_file)
+		return false;
+	if (req->file) {
+		umode_t mode = file_inode(req->file)->i_mode;
+
+		if (S_ISREG(mode) || S_ISBLK(mode))
+			return false;
+	}
+	return true;
+}
+
+static inline bool io_issue_needs_lock(unsigned int issue_flags)
+{
+	return (issue_flags & IO_URING_F_UNLOCKED) ||
+		io_issue_handed_off(issue_flags);
+}
+
 static inline void io_ring_submit_unlock(struct io_ring_ctx *ctx,
 					 unsigned issue_flags)
 {
 	lockdep_assert_held(&ctx->uring_lock);
 	lockdep_assert(ctx->submit_lock_depth > 0);
 	ctx->submit_lock_depth--;
-	if (unlikely(issue_flags & IO_URING_F_UNLOCKED))
+	if (unlikely(io_issue_needs_lock(issue_flags)))
 		mutex_unlock(&ctx->uring_lock);
 }
 
@@ -408,7 +440,7 @@ static inline void io_ring_submit_lock(struct io_ring_ctx *ctx,
 	 * The only exception is when we've detached the request and issue it
 	 * from an async worker thread, grab the lock for that case.
 	 */
-	if (unlikely(issue_flags & IO_URING_F_UNLOCKED))
+	if (unlikely(io_issue_needs_lock(issue_flags)))
 		mutex_lock(&ctx->uring_lock);
 	lockdep_assert_held(&ctx->uring_lock);
 	ctx->submit_lock_depth++;
diff --git a/io_uring/msg_ring.c b/io_uring/msg_ring.c
index 3067c9343991..04f2ffe4381d 100644
--- a/io_uring/msg_ring.c
+++ b/io_uring/msg_ring.c
@@ -245,7 +245,7 @@ static int io_msg_fd_remote(struct io_kiocb *req)
 	struct task_struct *task = ctx->submitter_task;
 
 	init_task_work(&msg->tw, io_msg_tw_fd_complete);
-	if (task_work_add(task, &msg->tw, TWA_SIGNAL))
+	if (io_wq_task_work_add(task, &msg->tw, TWA_SIGNAL))
 		return -EOWNERDEAD;
 
 	return IOU_ISSUE_SKIP_COMPLETE;
diff --git a/io_uring/rw.c b/io_uring/rw.c
index 95106dd1d7eb..23b078fe2991 100644
--- a/io_uring/rw.c
+++ b/io_uring/rw.c
@@ -134,7 +134,7 @@ static bool io_rw_recycle(struct io_kiocb *req, unsigned int issue_flags)
 {
 	struct io_async_rw *rw = req->async_data;
 
-	if (unlikely(issue_flags & IO_URING_F_UNLOCKED))
+	if (unlikely(io_issue_needs_lock(issue_flags)))
 		return false;
 
 	io_alloc_cache_vec_kasan(&rw->vec);
diff --git a/io_uring/tctx.c b/io_uring/tctx.c
index 737dfad4a976..f86aca489d15 100644
--- a/io_uring/tctx.c
+++ b/io_uring/tctx.c
@@ -10,6 +10,7 @@
 #include <uapi/linux/io_uring.h>
 
 #include "io_uring.h"
+#include "handoff.h"
 #include "tctx.h"
 #include "bpf_filter.h"
 
@@ -104,6 +105,7 @@ __cold struct io_uring_task *io_uring_alloc_task_context(struct task_struct *tas
 	}
 
 	tctx->task = task;
+	raw_spin_lock_init(&tctx->task_ref_lock);
 	xa_init(&tctx->xa);
 	INIT_LIST_HEAD(&tctx->node_list);
 	init_waitqueue_head(&tctx->wait);
@@ -175,8 +177,10 @@ int __io_uring_add_tctx_node(struct io_ring_ctx *ctx)
 	 * been marked for idle-exit when the task temporarily had no active
 	 * io_uring instances.
 	 */
-	if (tctx->io_wq)
+	if (tctx->io_wq) {
 		io_wq_set_exit_on_idle(tctx->io_wq, false);
+		io_handoff_prime(tctx, ctx);
+	}
 
 	if (new_tctx)
 		current->io_uring = tctx;
diff --git a/io_uring/tw.c b/io_uring/tw.c
index f573bcc3af6a..f26faeb5d21c 100644
--- a/io_uring/tw.c
+++ b/io_uring/tw.c
@@ -9,6 +9,7 @@
 #include <linux/indirect_call_wrapper.h>
 
 #include "io_uring.h"
+#include "handoff.h"
 #include "tctx.h"
 #include "poll.h"
 #include "rw.h"
@@ -130,6 +131,10 @@ void tctx_task_work(struct callback_head *cb)
 	unsigned int count = 0;
 
 	tctx = container_of(cb, struct io_uring_task, task_work);
+	/* the tctx may have moved while queued, run it there if so */
+	if (unlikely(READ_ONCE(tctx->task) != current) &&
+	    !task_work_add(tctx->task, cb, TWA_SIGNAL))
+		return;
 	tctx_task_work_run(tctx, UINT_MAX, &count);
 }
 
@@ -209,6 +214,7 @@ void io_req_normal_work_add(struct io_kiocb *req)
 {
 	struct io_uring_task *tctx = req->tctx;
 	struct io_ring_ctx *ctx = req->ctx;
+	struct task_struct *task;
 
 	/* tw run already pending, nothing else to do */
 	if (!mpscq_push(&tctx->task_list, &req->io_task_work.node))
@@ -227,8 +233,14 @@ void io_req_normal_work_add(struct io_kiocb *req)
 		return;
 	}
 
-	if (likely(!task_work_add(tctx->task, &tctx->task_work, ctx->notify_method)))
+	task = READ_ONCE(tctx->task);
+	if (likely(!io_wq_task_work_add(task, &tctx->task_work,
+					 ctx->notify_method))) {
+		/* the tctx moved while we were adding, move the work along */
+		if (unlikely(READ_ONCE(tctx->task) != task))
+			io_handoff_tw_moved(tctx, task);
 		return;
+	}
 
 	io_fallback_tw(tctx);
 }
diff --git a/io_uring/uring_cmd.c b/io_uring/uring_cmd.c
index 726a659f38c3..ba36a555db98 100644
--- a/io_uring/uring_cmd.c
+++ b/io_uring/uring_cmd.c
@@ -28,7 +28,7 @@ static void io_req_uring_cleanup(struct io_kiocb *req, unsigned int issue_flags)
 	struct io_uring_cmd *ioucmd = io_kiocb_to_cmd(req, struct io_uring_cmd);
 	struct io_async_cmd *ac = req->async_data;
 
-	if (issue_flags & IO_URING_F_UNLOCKED)
+	if (io_issue_needs_lock(issue_flags))
 		return;
 
 	io_alloc_cache_vec_kasan(&ac->vec);
@@ -172,7 +172,8 @@ void __io_uring_cmd_done(struct io_uring_cmd *ioucmd, s32 ret, u64 res2,
 	if (req->flags & REQ_F_IOPOLL) {
 		/* order with io_do_iopoll() checking ->iopoll_completed */
 		smp_store_release(&req->iopoll_completed, 1);
-	} else if (issue_flags & IO_URING_F_COMPLETE_DEFER) {
+	} else if ((issue_flags & IO_URING_F_COMPLETE_DEFER) &&
+		   !io_issue_handed_off(issue_flags)) {
 		if (WARN_ON_ONCE(issue_flags & IO_URING_F_UNLOCKED))
 			return;
 		io_req_complete_defer(req);
-- 
2.55.0


  parent reply	other threads:[~2026-09-11 15:42 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-11 15:40 [RFC PATCH 00/15] io_uring: thread identity handoff for blocking inline issue Jens Axboe
2026-09-11 15:40 ` [PATCH 01/15] kernel: add thread identity handoff Jens Axboe
2026-09-11 15:40 ` [PATCH 02/15] sched: call into io_uring when a PF_IO_HANDOFF task blocks Jens Axboe
2026-09-11 15:40 ` [PATCH 03/15] arm64: implement thread identity handoff Jens Axboe
2026-09-11 15:40 ` [PATCH 04/15] x86: " Jens Axboe
2026-09-11 15:40 ` [PATCH 05/15] io_uring/kbuf: use io_ring_submit_unlock() helper Jens Axboe
2026-09-11 15:40 ` [PATCH 06/15] io_uring: keep the tctx nodes on a list Jens Axboe
2026-09-11 15:40 ` [PATCH 07/15] io_uring: add uring_lock section depth tracking and blockable opdef flag Jens Axboe
2026-09-11 15:40 ` [PATCH 08/15] io_uring: split io_uring_enter() and io_submit_sqes() into helpers Jens Axboe
2026-09-11 15:40 ` [PATCH 09/15] io_uring: keep the submission plug on the io_submit_sqes() stack Jens Axboe
2026-09-11 15:41 ` [PATCH 10/15] io-wq: support handing a task identity to an idle worker Jens Axboe
2026-09-11 15:41 ` Jens Axboe [this message]
2026-09-11 15:41 ` [PATCH 12/15] io_uring: defer the identity migration to the end of the submission Jens Axboe
2026-09-11 15:41 ` [PATCH 13/15] io_uring: issue blockable requests inline in blocking mode Jens Axboe
2026-09-11 15:41 ` [PATCH 14/15] io_uring: add tracepoints for the handoff operation Jens Axboe
2026-09-11 15:41 ` [PATCH 15/15] io_uring: issue IOSQE_ASYNC requests inline when a handoff is possible Jens Axboe
2026-09-11 17:33 ` [RFC PATCH 00/15] io_uring: thread identity handoff for blocking inline issue Gabriel Krisman Bertazi
2026-09-11 17:51   ` Jens Axboe

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260911154148.644489-12-axboe@kernel.dk \
    --to=axboe@kernel.dk \
    --cc=io-uring@vger.kernel.org \
    --cc=linux-arm-kernel@lists.infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=mingo@redhat.com \
    --cc=peterz@infradead.org \
    --cc=tglx@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox