* [PATCHSET RFC v2 0/5] Cancel and wait for all requests on exit @ 2025-03-21 19:24 Jens Axboe 2025-03-21 19:24 ` [PATCH 1/5] fs: gate final fput task_work on PF_NO_TASKWORK Jens Axboe ` (4 more replies) 0 siblings, 5 replies; 10+ messages in thread From: Jens Axboe @ 2025-03-21 19:24 UTC (permalink / raw) To: io-uring; +Cc: asml.silence Hi, Currently, when a ring is being shut down, some cancelations may happen out-of-line. This means that an application cannot rely on the ring exit meaning that any IO has fully completed, or someone else waiting on an application (which has a ring with pending IO) being terminated will mean that all requests are done. This has also manifested itself as various testing sometimes finding a mount point busy after a test has exited, because it may take a brief period of time for things to quiesce and be fully done. This patchset makes the task wait on the cancelations, if any, when the io_uring file fd is being put. That has the effect of ensuring that pending IO has fully completed, and files closed, before the ring exit returns. I did post a previous version of this - fundamentally this one is the same, with the main difference being that rather than invent our own type of references for the ring, a basic atomic_long_t is used. io_uring batches the reference gets and puts on the ring, so this should not be noticeable. The only potential outlier is setting up a ring without DEFER_TASKRUN, where running task_work will result in an atomic dec and inc per ring in running the task_work. We can probably do something about that, but I don't consider it pressing. The switch away from percpu reference counts is done mostly because exiting those references will cost us an RCU grace period. That will noticeably slow down the ring tear down. The changes can also be found here: https://git.kernel.dk/cgit/linux/log/?h=io_uring-exit-cancel.2 fs/file_table.c | 2 +- include/linux/io_uring_types.h | 4 +- include/linux/sched.h | 2 +- io_uring/io_uring.c | 79 +++++++++++++++++++++++----------- io_uring/io_uring.h | 3 +- io_uring/msg_ring.c | 4 +- io_uring/refs.h | 43 ++++++++++++++++++ io_uring/register.c | 2 +- io_uring/rw.c | 2 +- io_uring/sqpoll.c | 2 +- io_uring/zcrx.c | 4 +- kernel/fork.c | 2 +- 12 files changed, 111 insertions(+), 38 deletions(-) -- Jens Axboe ^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH 1/5] fs: gate final fput task_work on PF_NO_TASKWORK 2025-03-21 19:24 [PATCHSET RFC v2 0/5] Cancel and wait for all requests on exit Jens Axboe @ 2025-03-21 19:24 ` Jens Axboe 2025-03-21 19:24 ` [PATCH 2/5] io_uring: mark exit side kworkers as task_work capable Jens Axboe ` (3 subsequent siblings) 4 siblings, 0 replies; 10+ messages in thread From: Jens Axboe @ 2025-03-21 19:24 UTC (permalink / raw) To: io-uring; +Cc: asml.silence, Jens Axboe Rather than hardwire this to kernel threads, add a task flag that tells us whether the task in question runs task_work or not. At fork time, this flag is set for kernel threads. This is in preparation for allowing kernel threads to signal that they will run deferred task_work. No functional changes in this patch. Signed-off-by: Jens Axboe <axboe@kernel.dk> --- fs/file_table.c | 2 +- include/linux/sched.h | 2 +- kernel/fork.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/file_table.c b/fs/file_table.c index 5c00dc38558d..d824f1330d6e 100644 --- a/fs/file_table.c +++ b/fs/file_table.c @@ -520,7 +520,7 @@ void fput(struct file *file) file_free(file); return; } - if (likely(!in_interrupt() && !(task->flags & PF_KTHREAD))) { + if (likely(!in_interrupt() && !(task->flags & PF_NO_TASKWORK))) { init_task_work(&file->f_task_work, ____fput); if (!task_work_add(task, &file->f_task_work, TWA_RESUME)) return; diff --git a/include/linux/sched.h b/include/linux/sched.h index 9c15365a30c0..301f5dda6a06 100644 --- a/include/linux/sched.h +++ b/include/linux/sched.h @@ -1709,7 +1709,7 @@ extern struct pid *cad_pid; * I am cleaning dirty pages from some other bdi. */ #define PF_KTHREAD 0x00200000 /* I am a kernel thread */ #define PF_RANDOMIZE 0x00400000 /* Randomize virtual address space */ -#define PF__HOLE__00800000 0x00800000 +#define PF_NO_TASKWORK 0x00800000 /* task doesn't run task_work */ #define PF__HOLE__01000000 0x01000000 #define PF__HOLE__02000000 0x02000000 #define PF_NO_SETAFFINITY 0x04000000 /* Userland is not allowed to meddle with cpus_mask */ diff --git a/kernel/fork.c b/kernel/fork.c index 735405a9c5f3..3745407624c7 100644 --- a/kernel/fork.c +++ b/kernel/fork.c @@ -2235,7 +2235,7 @@ __latent_entropy struct task_struct *copy_process( goto fork_out; p->flags &= ~PF_KTHREAD; if (args->kthread) - p->flags |= PF_KTHREAD; + p->flags |= PF_KTHREAD | PF_NO_TASKWORK; if (args->user_worker) { /* * Mark us a user worker, and block any signal that isn't -- 2.49.0 ^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 2/5] io_uring: mark exit side kworkers as task_work capable 2025-03-21 19:24 [PATCHSET RFC v2 0/5] Cancel and wait for all requests on exit Jens Axboe 2025-03-21 19:24 ` [PATCH 1/5] fs: gate final fput task_work on PF_NO_TASKWORK Jens Axboe @ 2025-03-21 19:24 ` Jens Axboe 2025-03-21 19:24 ` [PATCH 3/5] io_uring: consider ring dead once the ref is marked dying Jens Axboe ` (2 subsequent siblings) 4 siblings, 0 replies; 10+ messages in thread From: Jens Axboe @ 2025-03-21 19:24 UTC (permalink / raw) To: io-uring; +Cc: asml.silence, Jens Axboe There are two types of work here: 1) Fallback work, if the task is exiting 2) The exit side cancelations and both of them may do the final fput() of a file. When this happens, fput() will schedule delayed work. This slows down exits when io_uring needs to wait for that work to finish. It is possible to flush this via flush_delayed_fput(), but that's a big hammer as other unrelated files could be involved, and from other tasks as well. Add two io_uring helpers to temporarily clear PF_NO_TASKWORK for the worker threads, and run any queued task_work before setting the flag again. Then we can ensure we only flush related items that received their final fput as part of work cancelation and flushing. For now these are io_uring private, but could obviously be made generically available, should there be a need to do so. Signed-off-by: Jens Axboe <axboe@kernel.dk> --- io_uring/io_uring.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c index 5f625be52e52..2b9dae588f04 100644 --- a/io_uring/io_uring.c +++ b/io_uring/io_uring.c @@ -238,6 +238,20 @@ static inline void io_req_add_to_cache(struct io_kiocb *req, struct io_ring_ctx wq_stack_add_head(&req->comp_list, &ctx->submit_state.free_list); } +static __cold void io_kworker_tw_start(void) +{ + if (WARN_ON_ONCE(!(current->flags & PF_NO_TASKWORK))) + return; + current->flags &= ~PF_NO_TASKWORK; +} + +static __cold void io_kworker_tw_end(void) +{ + while (task_work_pending(current)) + task_work_run(); + current->flags |= PF_NO_TASKWORK; +} + static __cold void io_ring_ctx_ref_free(struct percpu_ref *ref) { struct io_ring_ctx *ctx = container_of(ref, struct io_ring_ctx, refs); @@ -253,6 +267,8 @@ static __cold void io_fallback_req_func(struct work_struct *work) struct io_kiocb *req, *tmp; struct io_tw_state ts = {}; + io_kworker_tw_start(); + percpu_ref_get(&ctx->refs); mutex_lock(&ctx->uring_lock); llist_for_each_entry_safe(req, tmp, node, io_task_work.node) @@ -260,6 +276,7 @@ static __cold void io_fallback_req_func(struct work_struct *work) io_submit_flush_completions(ctx); mutex_unlock(&ctx->uring_lock); percpu_ref_put(&ctx->refs); + io_kworker_tw_end(); } static int io_alloc_hash_table(struct io_hash_table *table, unsigned bits) @@ -2879,6 +2896,8 @@ static __cold void io_ring_exit_work(struct work_struct *work) struct io_tctx_node *node; int ret; + io_kworker_tw_start(); + /* * If we're doing polled IO and end up having requests being * submitted async (out-of-line), then completions can come in while @@ -2935,6 +2954,8 @@ static __cold void io_ring_exit_work(struct work_struct *work) */ } while (!wait_for_completion_interruptible_timeout(&ctx->ref_comp, interval)); + io_kworker_tw_end(); + init_completion(&exit.completion); init_task_work(&exit.task_work, io_tctx_exit_cb); exit.ctx = ctx; -- 2.49.0 ^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 3/5] io_uring: consider ring dead once the ref is marked dying 2025-03-21 19:24 [PATCHSET RFC v2 0/5] Cancel and wait for all requests on exit Jens Axboe 2025-03-21 19:24 ` [PATCH 1/5] fs: gate final fput task_work on PF_NO_TASKWORK Jens Axboe 2025-03-21 19:24 ` [PATCH 2/5] io_uring: mark exit side kworkers as task_work capable Jens Axboe @ 2025-03-21 19:24 ` Jens Axboe 2025-03-21 21:22 ` Pavel Begunkov 2025-03-21 19:24 ` [PATCH 4/5] io_uring: wait for cancelations on final ring put Jens Axboe 2025-03-21 19:24 ` [PATCH 5/5] io_uring: switch away from percpu refcounts Jens Axboe 4 siblings, 1 reply; 10+ messages in thread From: Jens Axboe @ 2025-03-21 19:24 UTC (permalink / raw) To: io-uring; +Cc: asml.silence, Jens Axboe Don't gate this on the task exiting flag. It's generally not a good idea to gate it on the task PF_EXITING flag anyway. Once the ring is starting to go through ring teardown, the ref is marked as dying. Use that as our fallback/cancel mechanism. Signed-off-by: Jens Axboe <axboe@kernel.dk> --- io_uring/io_uring.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c index 2b9dae588f04..984db01f5184 100644 --- a/io_uring/io_uring.c +++ b/io_uring/io_uring.c @@ -555,7 +555,8 @@ static void io_queue_iowq(struct io_kiocb *req) * procedure rather than attempt to run this request (or create a new * worker for it). */ - if (WARN_ON_ONCE(!same_thread_group(tctx->task, current))) + if (WARN_ON_ONCE(!same_thread_group(tctx->task, current) && + !percpu_ref_is_dying(&req->ctx->refs))) atomic_or(IO_WQ_WORK_CANCEL, &req->work.flags); trace_io_uring_queue_async_work(req, io_wq_is_hashed(&req->work)); @@ -1254,7 +1255,8 @@ static void io_req_normal_work_add(struct io_kiocb *req) return; } - if (likely(!task_work_add(tctx->task, &tctx->task_work, ctx->notify_method))) + if (!percpu_ref_is_dying(&ctx->refs) && + !task_work_add(tctx->task, &tctx->task_work, ctx->notify_method)) return; io_fallback_tw(tctx, false); -- 2.49.0 ^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [PATCH 3/5] io_uring: consider ring dead once the ref is marked dying 2025-03-21 19:24 ` [PATCH 3/5] io_uring: consider ring dead once the ref is marked dying Jens Axboe @ 2025-03-21 21:22 ` Pavel Begunkov 0 siblings, 0 replies; 10+ messages in thread From: Pavel Begunkov @ 2025-03-21 21:22 UTC (permalink / raw) To: Jens Axboe, io-uring On 3/21/25 19:24, Jens Axboe wrote: > Don't gate this on the task exiting flag. It's generally not a good idea Do you refer to tw add and the PF_EXITING logic inside? We can't gate it solely on dying refs as it's not sync'ed (and the patch doesn't). And task is dying is not same as ring is closed. E.g. a task can exit(2) but leave the ring intact to other tasks. > to gate it on the task PF_EXITING flag anyway. Once the ring is starting > to go through ring teardown, the ref is marked as dying. Use that as > our fallback/cancel mechanism. > > Signed-off-by: Jens Axboe <axboe@kernel.dk> > --- > io_uring/io_uring.c | 6 ++++-- > 1 file changed, 4 insertions(+), 2 deletions(-) > > diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c > index 2b9dae588f04..984db01f5184 100644 > --- a/io_uring/io_uring.c > +++ b/io_uring/io_uring.c > @@ -555,7 +555,8 @@ static void io_queue_iowq(struct io_kiocb *req) > * procedure rather than attempt to run this request (or create a new > * worker for it). > */ > - if (WARN_ON_ONCE(!same_thread_group(tctx->task, current))) > + if (WARN_ON_ONCE(!same_thread_group(tctx->task, current) && > + !percpu_ref_is_dying(&req->ctx->refs))) Should it be "||"? Otherwise I don't understand the purpose of it. > atomic_or(IO_WQ_WORK_CANCEL, &req->work.flags); > > trace_io_uring_queue_async_work(req, io_wq_is_hashed(&req->work)); > @@ -1254,7 +1255,8 @@ static void io_req_normal_work_add(struct io_kiocb *req) > return; > } > > - if (likely(!task_work_add(tctx->task, &tctx->task_work, ctx->notify_method))) > + if (!percpu_ref_is_dying(&ctx->refs) && > + !task_work_add(tctx->task, &tctx->task_work, ctx->notify_method)) > return; > > io_fallback_tw(tctx, false); -- Pavel Begunkov ^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH 4/5] io_uring: wait for cancelations on final ring put 2025-03-21 19:24 [PATCHSET RFC v2 0/5] Cancel and wait for all requests on exit Jens Axboe ` (2 preceding siblings ...) 2025-03-21 19:24 ` [PATCH 3/5] io_uring: consider ring dead once the ref is marked dying Jens Axboe @ 2025-03-21 19:24 ` Jens Axboe 2025-03-21 19:24 ` [PATCH 5/5] io_uring: switch away from percpu refcounts Jens Axboe 4 siblings, 0 replies; 10+ messages in thread From: Jens Axboe @ 2025-03-21 19:24 UTC (permalink / raw) To: io-uring; +Cc: asml.silence, Jens Axboe We still offload the cancelation to a workqueue, as not to introduce dependencies between the exiting task waiting on cleanup, and that task needing to run task_work to complete the process. This means that once the final ring put is done, any request that was inflight and needed cancelation will be done as well. Notably requests that hold references to files - once the ring fd close is done, we will have dropped any of those references too. Signed-off-by: Jens Axboe <axboe@kernel.dk> --- include/linux/io_uring_types.h | 2 ++ io_uring/io_uring.c | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/include/linux/io_uring_types.h b/include/linux/io_uring_types.h index c17d2eedf478..79e223fd4733 100644 --- a/include/linux/io_uring_types.h +++ b/include/linux/io_uring_types.h @@ -450,6 +450,8 @@ struct io_ring_ctx { struct io_mapped_region param_region; /* just one zcrx per ring for now, will move to io_zcrx_ifq eventually */ struct io_mapped_region zcrx_region; + + struct completion *exit_comp; }; /* diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c index 984db01f5184..d9b65a322ae1 100644 --- a/io_uring/io_uring.c +++ b/io_uring/io_uring.c @@ -2894,6 +2894,7 @@ static __cold void io_ring_exit_work(struct work_struct *work) struct io_ring_ctx *ctx = container_of(work, struct io_ring_ctx, exit_work); unsigned long timeout = jiffies + HZ * 60 * 5; unsigned long interval = HZ / 20; + struct completion *exit_comp; struct io_tctx_exit exit; struct io_tctx_node *node; int ret; @@ -2958,6 +2959,10 @@ static __cold void io_ring_exit_work(struct work_struct *work) io_kworker_tw_end(); + exit_comp = READ_ONCE(ctx->exit_comp); + if (exit_comp) + complete(exit_comp); + init_completion(&exit.completion); init_task_work(&exit.task_work, io_tctx_exit_cb); exit.ctx = ctx; @@ -3020,9 +3025,21 @@ static __cold void io_ring_ctx_wait_and_kill(struct io_ring_ctx *ctx) static int io_uring_release(struct inode *inode, struct file *file) { struct io_ring_ctx *ctx = file->private_data; + DECLARE_COMPLETION_ONSTACK(exit_comp); file->private_data = NULL; + WRITE_ONCE(ctx->exit_comp, &exit_comp); io_ring_ctx_wait_and_kill(ctx); + + /* + * Wait for cancel to run before exiting task + */ + do { + if (current->io_uring) + io_fallback_tw(current->io_uring, false); + cond_resched(); + } while (wait_for_completion_interruptible(&exit_comp)); + return 0; } -- 2.49.0 ^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 5/5] io_uring: switch away from percpu refcounts 2025-03-21 19:24 [PATCHSET RFC v2 0/5] Cancel and wait for all requests on exit Jens Axboe ` (3 preceding siblings ...) 2025-03-21 19:24 ` [PATCH 4/5] io_uring: wait for cancelations on final ring put Jens Axboe @ 2025-03-21 19:24 ` Jens Axboe 4 siblings, 0 replies; 10+ messages in thread From: Jens Axboe @ 2025-03-21 19:24 UTC (permalink / raw) To: io-uring; +Cc: asml.silence, Jens Axboe For the common cases, the io_uring ref counts are all batched and hence need not be a percpu reference. This saves some memory on systems, but outside of that, it gets rid of needing a full RCU grace period on tearing down the reference. With io_uring now waiting on cancelations and IO during exit, this slows down the tear down a lot, up to 100x as slow. Signed-off-by: Jens Axboe <axboe@kernel.dk> --- include/linux/io_uring_types.h | 2 +- io_uring/io_uring.c | 47 ++++++++++++---------------------- io_uring/io_uring.h | 3 ++- io_uring/msg_ring.c | 4 +-- io_uring/refs.h | 43 +++++++++++++++++++++++++++++++ io_uring/register.c | 2 +- io_uring/rw.c | 2 +- io_uring/sqpoll.c | 2 +- io_uring/zcrx.c | 4 +-- 9 files changed, 70 insertions(+), 39 deletions(-) diff --git a/include/linux/io_uring_types.h b/include/linux/io_uring_types.h index 79e223fd4733..8894b0639a3a 100644 --- a/include/linux/io_uring_types.h +++ b/include/linux/io_uring_types.h @@ -256,7 +256,7 @@ struct io_ring_ctx { struct task_struct *submitter_task; struct io_rings *rings; - struct percpu_ref refs; + atomic_long_t refs; clockid_t clockid; enum tk_offsets clock_offset; diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c index d9b65a322ae1..69b8f3237b1a 100644 --- a/io_uring/io_uring.c +++ b/io_uring/io_uring.c @@ -252,13 +252,6 @@ static __cold void io_kworker_tw_end(void) current->flags |= PF_NO_TASKWORK; } -static __cold void io_ring_ctx_ref_free(struct percpu_ref *ref) -{ - struct io_ring_ctx *ctx = container_of(ref, struct io_ring_ctx, refs); - - complete(&ctx->ref_comp); -} - static __cold void io_fallback_req_func(struct work_struct *work) { struct io_ring_ctx *ctx = container_of(work, struct io_ring_ctx, @@ -269,13 +262,13 @@ static __cold void io_fallback_req_func(struct work_struct *work) io_kworker_tw_start(); - percpu_ref_get(&ctx->refs); + io_ring_ref_get(ctx); mutex_lock(&ctx->uring_lock); llist_for_each_entry_safe(req, tmp, node, io_task_work.node) req->io_task_work.func(req, ts); io_submit_flush_completions(ctx); mutex_unlock(&ctx->uring_lock); - percpu_ref_put(&ctx->refs); + io_ring_ref_put(ctx); io_kworker_tw_end(); } @@ -333,10 +326,8 @@ static __cold struct io_ring_ctx *io_ring_ctx_alloc(struct io_uring_params *p) hash_bits = clamp(hash_bits, 1, 8); if (io_alloc_hash_table(&ctx->cancel_table, hash_bits)) goto err; - if (percpu_ref_init(&ctx->refs, io_ring_ctx_ref_free, - 0, GFP_KERNEL)) - goto err; + io_ring_ref_init(ctx); ctx->flags = p->flags; ctx->hybrid_poll_time = LLONG_MAX; atomic_set(&ctx->cq_wait_nr, IO_CQ_WAKE_INIT); @@ -360,7 +351,7 @@ static __cold struct io_ring_ctx *io_ring_ctx_alloc(struct io_uring_params *p) ret |= io_futex_cache_init(ctx); ret |= io_rsrc_cache_init(ctx); if (ret) - goto free_ref; + goto err; init_completion(&ctx->ref_comp); xa_init_flags(&ctx->personalities, XA_FLAGS_ALLOC1); mutex_init(&ctx->uring_lock); @@ -386,9 +377,6 @@ static __cold struct io_ring_ctx *io_ring_ctx_alloc(struct io_uring_params *p) mutex_init(&ctx->mmap_lock); return ctx; - -free_ref: - percpu_ref_exit(&ctx->refs); err: io_free_alloc_caches(ctx); kvfree(ctx->cancel_table.hbs); @@ -556,7 +544,7 @@ static void io_queue_iowq(struct io_kiocb *req) * worker for it). */ if (WARN_ON_ONCE(!same_thread_group(tctx->task, current) && - !percpu_ref_is_dying(&req->ctx->refs))) + !io_ring_ref_is_dying(req->ctx))) atomic_or(IO_WQ_WORK_CANCEL, &req->work.flags); trace_io_uring_queue_async_work(req, io_wq_is_hashed(&req->work)); @@ -998,7 +986,7 @@ __cold bool __io_alloc_req_refill(struct io_ring_ctx *ctx) ret = 1; } - percpu_ref_get_many(&ctx->refs, ret); + io_ring_ref_get_many(ctx, ret); while (ret--) { struct io_kiocb *req = reqs[ret]; @@ -1053,7 +1041,7 @@ static void ctx_flush_and_put(struct io_ring_ctx *ctx, io_tw_token_t tw) io_submit_flush_completions(ctx); mutex_unlock(&ctx->uring_lock); - percpu_ref_put(&ctx->refs); + io_ring_ref_put(ctx); } /* @@ -1077,7 +1065,7 @@ struct llist_node *io_handle_tw_list(struct llist_node *node, ctx_flush_and_put(ctx, ts); ctx = req->ctx; mutex_lock(&ctx->uring_lock); - percpu_ref_get(&ctx->refs); + io_ring_ref_get(ctx); } INDIRECT_CALL_2(req->io_task_work.func, io_poll_task_func, io_req_rw_complete, @@ -1106,10 +1094,10 @@ static __cold void __io_fallback_tw(struct llist_node *node, bool sync) if (sync && last_ctx != req->ctx) { if (last_ctx) { flush_delayed_work(&last_ctx->fallback_work); - percpu_ref_put(&last_ctx->refs); + io_ring_ref_put(last_ctx); } last_ctx = req->ctx; - percpu_ref_get(&last_ctx->refs); + io_ring_ref_get(last_ctx); } if (llist_add(&req->io_task_work.node, &req->ctx->fallback_llist)) @@ -1118,7 +1106,7 @@ static __cold void __io_fallback_tw(struct llist_node *node, bool sync) if (last_ctx) { flush_delayed_work(&last_ctx->fallback_work); - percpu_ref_put(&last_ctx->refs); + io_ring_ref_put(last_ctx); } } @@ -1255,7 +1243,7 @@ static void io_req_normal_work_add(struct io_kiocb *req) return; } - if (!percpu_ref_is_dying(&ctx->refs) && + if (!io_ring_ref_is_dying(ctx) && !task_work_add(tctx->task, &tctx->task_work, ctx->notify_method)) return; @@ -2739,7 +2727,7 @@ static void io_req_caches_free(struct io_ring_ctx *ctx) nr++; } if (nr) - percpu_ref_put_many(&ctx->refs, nr); + io_ring_ref_put_many(ctx, nr); mutex_unlock(&ctx->uring_lock); } @@ -2773,7 +2761,6 @@ static __cold void io_ring_ctx_free(struct io_ring_ctx *ctx) if (!(ctx->flags & IORING_SETUP_NO_SQARRAY)) static_branch_dec(&io_key_has_sqarray); - percpu_ref_exit(&ctx->refs); free_uid(ctx->user); io_req_caches_free(ctx); if (ctx->hash_map) @@ -2798,7 +2785,7 @@ static __cold void io_activate_pollwq_cb(struct callback_head *cb) * might've been lost due to loose synchronisation. */ wake_up_all(&ctx->poll_wq); - percpu_ref_put(&ctx->refs); + io_ring_ref_put(ctx); } __cold void io_activate_pollwq(struct io_ring_ctx *ctx) @@ -2816,9 +2803,9 @@ __cold void io_activate_pollwq(struct io_ring_ctx *ctx) * only need to sync with it, which is done by injecting a tw */ init_task_work(&ctx->poll_wq_task_work, io_activate_pollwq_cb); - percpu_ref_get(&ctx->refs); + io_ring_ref_get(ctx); if (task_work_add(ctx->submitter_task, &ctx->poll_wq_task_work, TWA_SIGNAL)) - percpu_ref_put(&ctx->refs); + io_ring_ref_put(ctx); out: spin_unlock(&ctx->completion_lock); } @@ -3005,7 +2992,7 @@ static __cold void io_ring_ctx_wait_and_kill(struct io_ring_ctx *ctx) struct creds *creds; mutex_lock(&ctx->uring_lock); - percpu_ref_kill(&ctx->refs); + io_ring_ref_kill(ctx); xa_for_each(&ctx->personalities, index, creds) io_unregister_personality(ctx, index); mutex_unlock(&ctx->uring_lock); diff --git a/io_uring/io_uring.h b/io_uring/io_uring.h index 87f883130286..67e5921771be 100644 --- a/io_uring/io_uring.h +++ b/io_uring/io_uring.h @@ -13,6 +13,7 @@ #include "slist.h" #include "filetable.h" #include "opdef.h" +#include "refs.h" #ifndef CREATE_TRACE_POINTS #include <trace/events/io_uring.h> @@ -143,7 +144,7 @@ static inline void io_lockdep_assert_cq_locked(struct io_ring_ctx *ctx) * Not from an SQE, as those cannot be submitted, but via * updating tagged resources. */ - if (!percpu_ref_is_dying(&ctx->refs)) + if (!io_ring_ref_is_dying(ctx)) lockdep_assert(current == ctx->submitter_task); } #endif diff --git a/io_uring/msg_ring.c b/io_uring/msg_ring.c index 0bbcbbcdebfd..30d4cabb66d6 100644 --- a/io_uring/msg_ring.c +++ b/io_uring/msg_ring.c @@ -83,7 +83,7 @@ static void io_msg_tw_complete(struct io_kiocb *req, io_tw_token_t tw) } if (req) kmem_cache_free(req_cachep, req); - percpu_ref_put(&ctx->refs); + io_ring_ref_put(ctx); } static int io_msg_remote_post(struct io_ring_ctx *ctx, struct io_kiocb *req, @@ -95,7 +95,7 @@ static int io_msg_remote_post(struct io_ring_ctx *ctx, struct io_kiocb *req, } req->cqe.user_data = user_data; io_req_set_res(req, res, cflags); - percpu_ref_get(&ctx->refs); + io_ring_ref_get(ctx); req->ctx = ctx; req->tctx = NULL; req->io_task_work.func = io_msg_tw_complete; diff --git a/io_uring/refs.h b/io_uring/refs.h index 63982ead9f7d..a794e6980cb8 100644 --- a/io_uring/refs.h +++ b/io_uring/refs.h @@ -52,4 +52,47 @@ static inline void io_req_set_refcount(struct io_kiocb *req) { __io_req_set_refcount(req, 1); } + +#define IO_RING_REF_DEAD (1ULL << 63) +#define IO_RING_REF_MASK (~IO_RING_REF_DEAD) + +static inline bool io_ring_ref_is_dying(struct io_ring_ctx *ctx) +{ + return atomic_long_read(&ctx->refs) & IO_RING_REF_DEAD; +} + +static inline void io_ring_ref_put_many(struct io_ring_ctx *ctx, int nr_refs) +{ + unsigned long refs; + + refs = atomic_long_sub_return(nr_refs, &ctx->refs); + if (!(refs & IO_RING_REF_MASK)) + complete(&ctx->ref_comp); +} + +static inline void io_ring_ref_put(struct io_ring_ctx *ctx) +{ + io_ring_ref_put_many(ctx, 1); +} + +static inline void io_ring_ref_kill(struct io_ring_ctx *ctx) +{ + atomic_long_xor(IO_RING_REF_DEAD, &ctx->refs); + io_ring_ref_put(ctx); +} + +static inline void io_ring_ref_init(struct io_ring_ctx *ctx) +{ + atomic_long_set(&ctx->refs, 1); +} + +static inline void io_ring_ref_get_many(struct io_ring_ctx *ctx, int nr_refs) +{ + atomic_long_add(nr_refs, &ctx->refs); +} + +static inline void io_ring_ref_get(struct io_ring_ctx *ctx) +{ + atomic_long_inc(&ctx->refs); +} #endif diff --git a/io_uring/register.c b/io_uring/register.c index cc23a4c205cd..54fe94a0101b 100644 --- a/io_uring/register.c +++ b/io_uring/register.c @@ -637,7 +637,7 @@ static int __io_uring_register(struct io_ring_ctx *ctx, unsigned opcode, * We don't quiesce the refs for register anymore and so it can't be * dying as we're holding a file ref here. */ - if (WARN_ON_ONCE(percpu_ref_is_dying(&ctx->refs))) + if (WARN_ON_ONCE(io_ring_ref_is_dying(ctx))) return -ENXIO; if (ctx->submitter_task && ctx->submitter_task != current) diff --git a/io_uring/rw.c b/io_uring/rw.c index 039e063f7091..e010d548edea 100644 --- a/io_uring/rw.c +++ b/io_uring/rw.c @@ -496,7 +496,7 @@ static bool io_rw_should_reissue(struct io_kiocb *req) * Don't attempt to reissue from that path, just let it fail with * -EAGAIN. */ - if (percpu_ref_is_dying(&ctx->refs)) + if (io_ring_ref_is_dying(ctx)) return false; io_meta_restore(io, &rw->kiocb); diff --git a/io_uring/sqpoll.c b/io_uring/sqpoll.c index d037cc68e9d3..b71f8d52386e 100644 --- a/io_uring/sqpoll.c +++ b/io_uring/sqpoll.c @@ -184,7 +184,7 @@ static int __io_sq_thread(struct io_ring_ctx *ctx, bool cap_entries) * Don't submit if refs are dying, good for io_uring_register(), * but also it is relied upon by io_ring_exit_work() */ - if (to_submit && likely(!percpu_ref_is_dying(&ctx->refs)) && + if (to_submit && likely(!io_ring_ref_is_dying(ctx)) && !(ctx->flags & IORING_SETUP_R_DISABLED)) ret = io_submit_sqes(ctx, to_submit); mutex_unlock(&ctx->uring_lock); diff --git a/io_uring/zcrx.c b/io_uring/zcrx.c index 9c95b5b6ec4e..07719e3bf1b3 100644 --- a/io_uring/zcrx.c +++ b/io_uring/zcrx.c @@ -629,7 +629,7 @@ static int io_pp_zc_init(struct page_pool *pp) if (pp->p.dma_dir != DMA_FROM_DEVICE) return -EOPNOTSUPP; - percpu_ref_get(&ifq->ctx->refs); + io_ring_ref_get(ifq->ctx); return 0; } @@ -640,7 +640,7 @@ static void io_pp_zc_destroy(struct page_pool *pp) if (WARN_ON_ONCE(area->free_count != area->nia.num_niovs)) return; - percpu_ref_put(&ifq->ctx->refs); + io_ring_ref_put(ifq->ctx); } static int io_pp_nl_fill(void *mp_priv, struct sk_buff *rsp, -- 2.49.0 ^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCHSET RFC 0/5] Wait on cancelations at release time @ 2024-06-04 19:01 Jens Axboe 2024-06-04 19:01 ` [PATCH 2/5] io_uring: mark exit side kworkers as task_work capable Jens Axboe 0 siblings, 1 reply; 10+ messages in thread From: Jens Axboe @ 2024-06-04 19:01 UTC (permalink / raw) To: io-uring Hi, I've posted this before, but did a bit more work on it and sending it out again. The idea is to ensure that we've done any fputs that we need to when a task using a ring exit, so that we don't leave references that will get put "shortly afterwards". Currently cancelations are done by ring exit work, which is punted to a kworker. This means that after the final ->release() on the io_uring fd has completed, there can still be pending fputs. This can be observed by running the following script: #!/bin/bash DEV=/dev/nvme0n1 MNT=/data ITER=0 while true; do echo loop $ITER sudo mount $DEV $MNT fio --name=test --ioengine=io_uring --iodepth=2 --filename=$MNT/foo --size=1g --buffered=1 --overwrite=0 --numjobs=12 --minimal --rw=randread --thread=1 --output=/dev/null --eta=never & Y=$(($RANDOM % 3)) X=$(($RANDOM % 10)) VAL="$Y.$X" sleep $VAL FIO_PID=$(pidof fio) if [ -z "$FIO_PID" ]; then ((ITER++)) continue fi ps -e | grep fio > /dev/null 2>&1 while [ $? -eq 0 ]; do killall -KILL $FIO_PID > /dev/null 2>&1 echo will wait wait > /dev/null 2>&1 echo done waiting ps -e | grep "fio " > /dev/null 2>&1 done sudo umount /data if [ $? -ne 0 ]; then break fi ((ITER++)) done which just starts a fio job doing writes, kills it, waits on the task to exit, and then immediately tries to umount it. Currently that will at some point trigger: [...] loop 9 will wait(f=12) done waiting umount: /data: target is busy. as the umount raced with the final fputs on the files being accessed on the mount point. There are a few parts to this: 1) Final fput is done via task_work, but for kernel threads, it's done via a delayed work queue. Patches 1+2 allows for kernel threads to use task_work like other threads, as we can then quiesce the fputs for the task rather than need to flush a system wide global pending list that can have pending final releases for any task or file. 2) Patch 3 moves away from percpu reference counts, as those require an RCU sync on freeing. As the goal is to move to sync cancelations on exit, this can add considerable latency. Outside of that, percpu ref counts provide a lot of guarantees and features that io_uring doesn't need, and the new approach is faster. 3) Finally, make the cancelations sync. They are still offloaded to a kworker, but the task doing ->release() waits for them to finish. With this, the above test case works fine, as expected. I'll send patches 1+2 separately, but wanted to get this out for review and discussion first. Patches are against current -git, with io_uring 6.10 and 6.11 pending changes pulled in. You can also find the patches here: https://git.kernel.dk/cgit/linux/log/?h=io_uring-exit-cancel fs/file_table.c | 2 +- include/linux/io_uring_types.h | 4 +- include/linux/sched.h | 2 +- io_uring/Makefile | 2 +- io_uring/io_uring.c | 77 ++++++++++++++++++++++++---------- io_uring/io_uring.h | 3 +- io_uring/refs.c | 58 +++++++++++++++++++++++++ io_uring/refs.h | 53 +++++++++++++++++++++++ io_uring/register.c | 3 +- io_uring/rw.c | 3 +- io_uring/sqpoll.c | 3 +- kernel/fork.c | 2 +- 12 files changed, 182 insertions(+), 30 deletions(-) -- Jens Axboe ^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH 2/5] io_uring: mark exit side kworkers as task_work capable 2024-06-04 19:01 [PATCHSET RFC 0/5] Wait on cancelations at release time Jens Axboe @ 2024-06-04 19:01 ` Jens Axboe 2024-06-05 15:01 ` Pavel Begunkov 0 siblings, 1 reply; 10+ messages in thread From: Jens Axboe @ 2024-06-04 19:01 UTC (permalink / raw) To: io-uring; +Cc: Jens Axboe There are two types of work here: 1) Fallback work, if the task is exiting 2) The exit side cancelations and both of them may do the final fput() of a file. When this happens, fput() will schedule delayed work. This slows down exits when io_uring needs to wait for that work to finish. It is possible to flush this via flush_delayed_fput(), but that's a big hammer as other unrelated files could be involved, and from other tasks as well. Add two io_uring helpers to temporarily clear PF_NO_TASKWORK for the worker threads, and run any queued task_work before setting the flag again. Then we can ensure we only flush related items that received their final fput as part of work cancelation and flushing. For now these are io_uring private, but could obviously be made generically available, should there be a need to do so. Signed-off-by: Jens Axboe <axboe@kernel.dk> --- io_uring/io_uring.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c index 96f6da0bf5cd..3ad915262a45 100644 --- a/io_uring/io_uring.c +++ b/io_uring/io_uring.c @@ -234,6 +234,20 @@ static inline void io_req_add_to_cache(struct io_kiocb *req, struct io_ring_ctx wq_stack_add_head(&req->comp_list, &ctx->submit_state.free_list); } +static __cold void io_kworker_tw_start(void) +{ + if (WARN_ON_ONCE(!(current->flags & PF_NO_TASKWORK))) + return; + current->flags &= ~PF_NO_TASKWORK; +} + +static __cold void io_kworker_tw_end(void) +{ + while (task_work_pending(current)) + task_work_run(); + current->flags |= PF_NO_TASKWORK; +} + static __cold void io_ring_ctx_ref_free(struct percpu_ref *ref) { struct io_ring_ctx *ctx = container_of(ref, struct io_ring_ctx, refs); @@ -249,6 +263,8 @@ static __cold void io_fallback_req_func(struct work_struct *work) struct io_kiocb *req, *tmp; struct io_tw_state ts = {}; + io_kworker_tw_start(); + percpu_ref_get(&ctx->refs); mutex_lock(&ctx->uring_lock); llist_for_each_entry_safe(req, tmp, node, io_task_work.node) @@ -256,6 +272,7 @@ static __cold void io_fallback_req_func(struct work_struct *work) io_submit_flush_completions(ctx); mutex_unlock(&ctx->uring_lock); percpu_ref_put(&ctx->refs); + io_kworker_tw_end(); } static int io_alloc_hash_table(struct io_hash_table *table, unsigned bits) @@ -2720,6 +2737,8 @@ static __cold void io_ring_exit_work(struct work_struct *work) struct io_tctx_node *node; int ret; + io_kworker_tw_start(); + /* * If we're doing polled IO and end up having requests being * submitted async (out-of-line), then completions can come in while @@ -2770,6 +2789,8 @@ static __cold void io_ring_exit_work(struct work_struct *work) */ } while (!wait_for_completion_interruptible_timeout(&ctx->ref_comp, interval)); + io_kworker_tw_end(); + init_completion(&exit.completion); init_task_work(&exit.task_work, io_tctx_exit_cb); exit.ctx = ctx; -- 2.43.0 ^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [PATCH 2/5] io_uring: mark exit side kworkers as task_work capable 2024-06-04 19:01 ` [PATCH 2/5] io_uring: mark exit side kworkers as task_work capable Jens Axboe @ 2024-06-05 15:01 ` Pavel Begunkov 2024-06-05 18:08 ` Jens Axboe 0 siblings, 1 reply; 10+ messages in thread From: Pavel Begunkov @ 2024-06-05 15:01 UTC (permalink / raw) To: Jens Axboe, io-uring On 6/4/24 20:01, Jens Axboe wrote: > There are two types of work here: > > 1) Fallback work, if the task is exiting > 2) The exit side cancelations > > and both of them may do the final fput() of a file. When this happens, > fput() will schedule delayed work. This slows down exits when io_uring > needs to wait for that work to finish. It is possible to flush this via > flush_delayed_fput(), but that's a big hammer as other unrelated files > could be involved, and from other tasks as well. > > Add two io_uring helpers to temporarily clear PF_NO_TASKWORK for the > worker threads, and run any queued task_work before setting the flag > again. Then we can ensure we only flush related items that received > their final fput as part of work cancelation and flushing. > > For now these are io_uring private, but could obviously be made > generically available, should there be a need to do so. > > Signed-off-by: Jens Axboe <axboe@kernel.dk> > --- > io_uring/io_uring.c | 21 +++++++++++++++++++++ > 1 file changed, 21 insertions(+) > > diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c > index 96f6da0bf5cd..3ad915262a45 100644 > --- a/io_uring/io_uring.c > +++ b/io_uring/io_uring.c > @@ -234,6 +234,20 @@ static inline void io_req_add_to_cache(struct io_kiocb *req, struct io_ring_ctx > wq_stack_add_head(&req->comp_list, &ctx->submit_state.free_list); > } > > +static __cold void io_kworker_tw_start(void) > +{ > + if (WARN_ON_ONCE(!(current->flags & PF_NO_TASKWORK))) > + return; > + current->flags &= ~PF_NO_TASKWORK; > +} > + > +static __cold void io_kworker_tw_end(void) > +{ > + while (task_work_pending(current)) > + task_work_run(); Clear TIF_NOTIFY_SIGNAL/RESUME? Maybe even retrying task_work_run() after and looping around if there are items to execute. > + current->flags |= PF_NO_TASKWORK; > +} > + > static __cold void io_ring_ctx_ref_free(struct percpu_ref *ref) > { > struct io_ring_ctx *ctx = container_of(ref, struct io_ring_ctx, refs); > @@ -249,6 +263,8 @@ static __cold void io_fallback_req_func(struct work_struct *work) > struct io_kiocb *req, *tmp; > struct io_tw_state ts = {}; > > + io_kworker_tw_start(); > + > percpu_ref_get(&ctx->refs); > mutex_lock(&ctx->uring_lock); > llist_for_each_entry_safe(req, tmp, node, io_task_work.node) > @@ -256,6 +272,7 @@ static __cold void io_fallback_req_func(struct work_struct *work) > io_submit_flush_completions(ctx); > mutex_unlock(&ctx->uring_lock); > percpu_ref_put(&ctx->refs); > + io_kworker_tw_end(); > } > > static int io_alloc_hash_table(struct io_hash_table *table, unsigned bits) > @@ -2720,6 +2737,8 @@ static __cold void io_ring_exit_work(struct work_struct *work) > struct io_tctx_node *node; > int ret; > > + io_kworker_tw_start(); > + > /* > * If we're doing polled IO and end up having requests being > * submitted async (out-of-line), then completions can come in while > @@ -2770,6 +2789,8 @@ static __cold void io_ring_exit_work(struct work_struct *work) > */ > } while (!wait_for_completion_interruptible_timeout(&ctx->ref_comp, interval)); > > + io_kworker_tw_end(); > + > init_completion(&exit.completion); > init_task_work(&exit.task_work, io_tctx_exit_cb); > exit.ctx = ctx; -- Pavel Begunkov ^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH 2/5] io_uring: mark exit side kworkers as task_work capable 2024-06-05 15:01 ` Pavel Begunkov @ 2024-06-05 18:08 ` Jens Axboe 0 siblings, 0 replies; 10+ messages in thread From: Jens Axboe @ 2024-06-05 18:08 UTC (permalink / raw) To: Pavel Begunkov, io-uring On 6/5/24 9:01 AM, Pavel Begunkov wrote: >> diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c >> index 96f6da0bf5cd..3ad915262a45 100644 >> --- a/io_uring/io_uring.c >> +++ b/io_uring/io_uring.c >> @@ -234,6 +234,20 @@ static inline void io_req_add_to_cache(struct io_kiocb *req, struct io_ring_ctx >> wq_stack_add_head(&req->comp_list, &ctx->submit_state.free_list); >> } >> +static __cold void io_kworker_tw_start(void) >> +{ >> + if (WARN_ON_ONCE(!(current->flags & PF_NO_TASKWORK))) >> + return; >> + current->flags &= ~PF_NO_TASKWORK; >> +} >> + >> +static __cold void io_kworker_tw_end(void) >> +{ >> + while (task_work_pending(current)) >> + task_work_run(); > > Clear TIF_NOTIFY_SIGNAL/RESUME? Maybe even retrying task_work_run() > after and looping around if there are items to execute. Yeah good point, it should handle clear the notifiers too. Will make that change. -- Jens Axboe ^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2025-03-21 21:21 UTC | newest] Thread overview: 10+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2025-03-21 19:24 [PATCHSET RFC v2 0/5] Cancel and wait for all requests on exit Jens Axboe 2025-03-21 19:24 ` [PATCH 1/5] fs: gate final fput task_work on PF_NO_TASKWORK Jens Axboe 2025-03-21 19:24 ` [PATCH 2/5] io_uring: mark exit side kworkers as task_work capable Jens Axboe 2025-03-21 19:24 ` [PATCH 3/5] io_uring: consider ring dead once the ref is marked dying Jens Axboe 2025-03-21 21:22 ` Pavel Begunkov 2025-03-21 19:24 ` [PATCH 4/5] io_uring: wait for cancelations on final ring put Jens Axboe 2025-03-21 19:24 ` [PATCH 5/5] io_uring: switch away from percpu refcounts Jens Axboe -- strict thread matches above, loose matches on Subject: below -- 2024-06-04 19:01 [PATCHSET RFC 0/5] Wait on cancelations at release time Jens Axboe 2024-06-04 19:01 ` [PATCH 2/5] io_uring: mark exit side kworkers as task_work capable Jens Axboe 2024-06-05 15:01 ` Pavel Begunkov 2024-06-05 18:08 ` Jens Axboe
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox