--- /dev/null
+From 91214661489467f8452d34edbf257488d85176e4 Mon Sep 17 00:00:00 2001
+From: Li Chen <me@linux.beauty>
+Date: Mon, 2 Feb 2026 22:37:54 +0800
+Subject: io_uring: allow io-wq workers to exit when unused
+
+From: Li Chen <me@linux.beauty>
+
+commit 91214661489467f8452d34edbf257488d85176e4 upstream.
+
+io_uring keeps a per-task io-wq around, even when the task no longer has
+any io_uring instances.
+
+If the task previously used io_uring for file I/O, this can leave an
+unrelated iou-wrk-* worker thread behind after the last io_uring
+instance is gone.
+
+When the last io_uring ctx is removed from the task context, mark the
+io-wq exit-on-idle so workers can go away. Clear the flag on subsequent
+io_uring usage.
+
+Signed-off-by: Li Chen <me@linux.beauty>
+Signed-off-by: Jens Axboe <axboe@kernel.dk>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ io_uring/tctx.c | 11 +++++++++++
+ 1 file changed, 11 insertions(+)
+
+--- a/io_uring/tctx.c
++++ b/io_uring/tctx.c
+@@ -122,6 +122,14 @@ int __io_uring_add_tctx_node(struct io_r
+ return ret;
+ }
+ }
++
++ /*
++ * Re-activate io-wq keepalive on any new io_uring usage. The wq may have
++ * been marked for idle-exit when the task temporarily had no active
++ * io_uring instances.
++ */
++ if (tctx->io_wq)
++ io_wq_set_exit_on_idle(tctx->io_wq, false);
+ if (!xa_load(&tctx->xa, (unsigned long)ctx)) {
+ node = kmalloc(sizeof(*node), GFP_KERNEL);
+ if (!node)
+@@ -183,6 +191,9 @@ __cold void io_uring_del_tctx_node(unsig
+ if (tctx->last == node->ctx)
+ tctx->last = NULL;
+ kfree(node);
++
++ if (xa_empty(&tctx->xa) && tctx->io_wq)
++ io_wq_set_exit_on_idle(tctx->io_wq, true);
+ }
+
+ __cold void io_uring_clean_tctx(struct io_uring_task *tctx)
--- /dev/null
+From 38aa434ab9335ce2d178b7538cdf01d60b2014c3 Mon Sep 17 00:00:00 2001
+From: Li Chen <me@linux.beauty>
+Date: Mon, 2 Feb 2026 22:37:53 +0800
+Subject: io_uring/io-wq: add exit-on-idle state
+
+From: Li Chen <me@linux.beauty>
+
+commit 38aa434ab9335ce2d178b7538cdf01d60b2014c3 upstream.
+
+io-wq uses an idle timeout to shrink the pool, but keeps the last worker
+around indefinitely to avoid churn.
+
+For tasks that used io_uring for file I/O and then stop using io_uring,
+this can leave an iou-wrk-* thread behind even after all io_uring
+instances are gone. This is unnecessary overhead and also gets in the
+way of process checkpoint/restore.
+
+Add an exit-on-idle state that makes all io-wq workers exit as soon as
+they become idle, and provide io_wq_set_exit_on_idle() to toggle it.
+
+Signed-off-by: Li Chen <me@linux.beauty>
+Signed-off-by: Jens Axboe <axboe@kernel.dk>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ io_uring/io-wq.c | 27 +++++++++++++++++++++++++--
+ io_uring/io-wq.h | 1 +
+ 2 files changed, 26 insertions(+), 2 deletions(-)
+
+--- a/io_uring/io-wq.c
++++ b/io_uring/io-wq.c
+@@ -34,6 +34,7 @@ enum {
+
+ enum {
+ IO_WQ_BIT_EXIT = 0, /* wq exiting */
++ IO_WQ_BIT_EXIT_ON_IDLE = 1, /* allow all workers to exit on idle */
+ };
+
+ enum {
+@@ -706,9 +707,13 @@ static int io_wq_worker(void *data)
+ raw_spin_lock(&acct->workers_lock);
+ /*
+ * Last sleep timed out. Exit if we're not the last worker,
+- * or if someone modified our affinity.
++ * or if someone modified our affinity. If wq is marked
++ * idle-exit, drop the worker as well. This is used to avoid
++ * keeping io-wq workers around for tasks that no longer have
++ * any active io_uring instances.
+ */
+- if (last_timeout && (exit_mask || acct->nr_workers > 1)) {
++ if ((last_timeout && (exit_mask || acct->nr_workers > 1)) ||
++ test_bit(IO_WQ_BIT_EXIT_ON_IDLE, &wq->state)) {
+ acct->nr_workers--;
+ raw_spin_unlock(&acct->workers_lock);
+ __set_current_state(TASK_RUNNING);
+@@ -963,6 +968,24 @@ static bool io_wq_worker_wake(struct io_
+ return false;
+ }
+
++void io_wq_set_exit_on_idle(struct io_wq *wq, bool enable)
++{
++ if (!wq->task)
++ return;
++
++ if (!enable) {
++ clear_bit(IO_WQ_BIT_EXIT_ON_IDLE, &wq->state);
++ return;
++ }
++
++ if (test_and_set_bit(IO_WQ_BIT_EXIT_ON_IDLE, &wq->state))
++ return;
++
++ rcu_read_lock();
++ io_wq_for_each_worker(wq, io_wq_worker_wake, NULL);
++ rcu_read_unlock();
++}
++
+ static void io_run_cancel(struct io_wq_work *work, struct io_wq *wq)
+ {
+ do {
+--- a/io_uring/io-wq.h
++++ b/io_uring/io-wq.h
+@@ -41,6 +41,7 @@ struct io_wq_data {
+ struct io_wq *io_wq_create(unsigned bounded, struct io_wq_data *data);
+ void io_wq_exit_start(struct io_wq *wq);
+ void io_wq_put_and_exit(struct io_wq *wq);
++void io_wq_set_exit_on_idle(struct io_wq *wq, bool enable);
+
+ void io_wq_enqueue(struct io_wq *wq, struct io_wq_work *work);
+ void io_wq_hash_work(struct io_wq_work *work, void *val);