Re: [PATCH v7 2/5] block: add task-context bio completion infrastructure
Jan Kara <[email protected]> Thu, 30 Jul 2026 11:29:41 +0200
| Newsgroups | org.ozlabs.lists.linux-erofs,org.kernel.vger.linux-block,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel,org.kernel.vger.linux-xfs,org.kvack.linux-mm |
|---|---|
| Message-ID | <roo3rxpocrjzz66mko3cf4p4thur2w2sgf24cahbpjk3462ext@3jep6oc2raqs> |
On Thu 30-07-26 02:57:52, Tal Zussman wrote: > Some bio completion handlers need to run from preemptible task context, > but bio_endio() may be called from IRQ context (e.g., buffer_head > writeback). Callers need a way to ensure their callback eventually runs > from a sleepable context. Add infrastructure for that, in two forms: > > 1. BIO_COMPLETE_IN_TASK, a bio flag the submitter sets when it knows > in advance that its callback needs task context (e.g., dropbehind > writeback). bio_endio() sees the flag and offloads completion to a > worker automatically. > > 2. bio_complete_in_task(), a helper that completion callbacks can > invoke from within bi_end_io() when the deferral decision is > dynamic (e.g., fserror reporting). > > Both share a per-CPU list drained by a work item on a WQ_PERCPU > workqueue. Producers push the bio onto the local CPU's list and schedule > the work item, which then dispatches each bio's bi_end_io() from task > context. > > Both methods are gated on bio_in_atomic(), which returns true in any > context where a sleeping bi_end_io() is unsafe, including > non-preemptible task context. > > Two CPU hotplug callbacks are used to drain remaining bios from the > departing CPU's batch, while maintaining the per-CPU behavior. The > CPUHP_AP_ONLINE_DYN callback disables the per-CPU work item while the > CPU is still online, preventing it from running on an unbound worker > later. CPUHP_BP_PREPARE_DYN then drains any bios added between disabling > the work item and CPU offline. > > Link: https://lore.kernel.org/all/[email protected]/ > Suggested-by: Matthew Wilcox <[email protected]> > Suggested-by: Christoph Hellwig <[email protected]> > Signed-off-by: Tal Zussman <[email protected]> Looks good! Feel free to add: Reviewed-by: Jan Kara <[email protected]> Honza > --- > block/bio.c | 132 +++++++++++++++++++++++++++++++++++++++++++++- > include/linux/bio.h | 24 +++++++++ > include/linux/blk_types.h | 1 + > 3 files changed, 156 insertions(+), 1 deletion(-) > > diff --git a/block/bio.c b/block/bio.c > index 6a2f6fc3413e..a9610950325b 100644 > --- a/block/bio.c > +++ b/block/bio.c > @@ -1741,6 +1741,61 @@ void bio_check_pages_dirty(struct bio *bio) > schedule_work(&bio_dirty_work); > } > > +/* > + * Infrastructure for deferring bio completions to task-context via a per-CPU > + * workqueue. Triggered either by the BIO_COMPLETE_IN_TASK bio flag (static > + * decision at submit time) or by calling bio_complete_in_task() from > + * bi_end_io() (dynamic decision at completion time). > + */ > + > +struct bio_complete_batch { > + struct bio_list list; > + struct work_struct work; > + int cpu; > +}; > + > +static DEFINE_PER_CPU(struct bio_complete_batch, bio_complete_batch); > +static struct workqueue_struct *bio_complete_wq; > + > +static void bio_complete_work_fn(struct work_struct *w) > +{ > + struct bio_complete_batch *batch = > + container_of(w, struct bio_complete_batch, work); > + > + while (1) { > + struct bio_list list; > + struct bio *bio; > + > + local_irq_disable(); > + list = batch->list; > + bio_list_init(&batch->list); > + local_irq_enable(); > + > + if (bio_list_empty(&list)) > + break; > + > + while ((bio = bio_list_pop(&list))) > + bio->bi_end_io(bio); > + } > +} > + > +void __bio_complete_in_task(struct bio *bio) > +{ > + struct bio_complete_batch *batch; > + unsigned long flags; > + bool was_empty; > + > + local_irq_save(flags); > + batch = this_cpu_ptr(&bio_complete_batch); > + was_empty = bio_list_empty(&batch->list); > + bio_list_add(&batch->list, bio); > + local_irq_restore(flags); > + > + if (was_empty) > + queue_work_on(batch->cpu, bio_complete_wq, &batch->work); > +} > +EXPORT_SYMBOL_GPL(__bio_complete_in_task); > + > static inline bool bio_remaining_done(struct bio *bio) > { > /* > @@ -1815,7 +1870,9 @@ void bio_endio(struct bio *bio) > } > #endif > > - if (bio->bi_end_io) > + if (bio_flagged(bio, BIO_COMPLETE_IN_TASK) && bio_in_atomic()) > + __bio_complete_in_task(bio); > + else if (bio->bi_end_io) > bio->bi_end_io(bio); > } > EXPORT_SYMBOL(bio_endio); > @@ -2001,6 +2058,55 @@ int bioset_init(struct bio_set *bs, > } > EXPORT_SYMBOL(bioset_init); > > +static int bio_complete_batch_cpu_online(unsigned int cpu) > +{ > + struct bio_complete_batch *batch = &per_cpu(bio_complete_batch, cpu); > + > + enable_work(&batch->work); > + if (!bio_list_empty(&batch->list)) > + queue_work_on(cpu, bio_complete_wq, &batch->work); > + return 0; > +} > + > +/* > + * Disable this CPU's work item so that it cannot run on an unbound worker > + * after the CPU is offlined. > + */ > +static int bio_complete_batch_cpu_down_prep(unsigned int cpu) > +{ > + disable_work_sync(&per_cpu(bio_complete_batch, cpu).work); > + return 0; > +} > + > +/* > + * Drain a dead CPU's deferred bio completions. The CPU is dead and the worker > + * is canceled so no locking is needed. > + */ > +static int bio_complete_batch_cpu_dead(unsigned int cpu) > +{ > + struct bio_complete_batch *batch = > + per_cpu_ptr(&bio_complete_batch, cpu); > + struct bio *bio; > + > + while ((bio = bio_list_pop(&batch->list))) > + bio->bi_end_io(bio); > + > + return 0; > +} > + > +static void __init bio_complete_batch_init(int cpu) > +{ > + struct bio_complete_batch *batch = > + per_cpu_ptr(&bio_complete_batch, cpu); > + > + bio_list_init(&batch->list); > + INIT_WORK(&batch->work, bio_complete_work_fn); > + batch->cpu = cpu; > + > + if (!cpu_online(cpu)) > + disable_work_sync(&batch->work); > +} > + > static int __init init_bio(void) > { > int i; > @@ -2015,6 +2121,30 @@ static int __init init_bio(void) > SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL); > } > > + for_each_possible_cpu(i) > + bio_complete_batch_init(i); > + > + bio_complete_wq = alloc_workqueue("bio_complete", > + WQ_MEM_RECLAIM | WQ_PERCPU, 0); > + if (!bio_complete_wq) > + panic("bio: can't allocate bio_complete workqueue\n"); > + > + /* > + * bio task-context completion draining on hot-unplugged CPUs: > + * > + * 1. Stop the per-CPU work item while the CPU is still online, so > + * that it cannot run on an unbound worker later. > + * 2. Drain leftover bios added between worker disabling and CPU > + * offlining. > + */ > + cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN, > + "block/bio:complete:online", > + bio_complete_batch_cpu_online, > + bio_complete_batch_cpu_down_prep); > + cpuhp_setup_state_nocalls(CPUHP_BP_PREPARE_DYN, > + "block/bio:complete:dead", > + NULL, bio_complete_batch_cpu_dead); > + > cpuhp_setup_state_multi(CPUHP_BIO_DEAD, "block/bio:dead", NULL, > bio_cpu_dead); > > diff --git a/include/linux/bio.h b/include/linux/bio.h > index 62a983758e09..a8091a3e9d87 100644 > --- a/include/linux/bio.h > +++ b/include/linux/bio.h > @@ -383,6 +383,30 @@ static inline bool bio_in_atomic(void) > return !preemptible(); > } > > +void __bio_complete_in_task(struct bio *bio); > + > +/** > + * bio_complete_in_task - ensure a bio is completed in preemptible task context > + * @bio: bio to complete > + * > + * If called from non-task context, offload the bio completion to a worker > + * thread and return %true. Else return %false and do nothing. > + * > + * Uses BIO_COMPLETE_IN_TASK as a sentinel: if set, the bio was already > + * deferred and we are running in the worker — return %false so the > + * callback proceeds instead of re-deferring. > + */ > +static inline bool bio_complete_in_task(struct bio *bio) > +{ > + if (bio_flagged(bio, BIO_COMPLETE_IN_TASK)) > + return false; > + if (!bio_in_atomic()) > + return false; > + bio_set_flag(bio, BIO_COMPLETE_IN_TASK); > + __bio_complete_in_task(bio); > + return true; > +} > + > extern void bio_endio(struct bio *); > > /** > diff --git a/include/linux/blk_types.h b/include/linux/blk_types.h > index 8808ee76e73c..d49d97a050d0 100644 > --- a/include/linux/blk_types.h > +++ b/include/linux/blk_types.h > @@ -322,6 +322,7 @@ enum { > BIO_REMAPPED, > BIO_ZONE_WRITE_PLUGGING, /* bio handled through zone write plugging */ > BIO_EMULATES_ZONE_APPEND, /* bio emulates a zone append operation */ > + BIO_COMPLETE_IN_TASK, /* complete bi_end_io() in task context */ > BIO_FLAG_LAST > }; > > > -- > 2.39.5 > -- Jan Kara <[email protected]> SUSE Labs, CR