All of lore.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.