Re: [PATCH 3/6] srcu: Make call_srcu() safe to call from any context
"Zqiang" <[email protected]>
| Newsgroups | org.kernel.vger.bpf,dev.linux.lists.linux-rt-devel,org.kernel.vger.linux-kernel,org.kernel.vger.rcu |
|---|---|
| Message-ID | <[email protected]> |
> > call_srcu() has the same constraint as call_rcu(): its callback list and > locks are only touched with interrupts disabled. srcu_gp_start_if_needed() > enqueues under raw_spin_lock_irqsave() and may walk the srcu_node tree, > and callback invocation and grace-period work do likewise. So a > call_srcu() with interrupts disabled may be racing a list operation in > flight on this CPU, and enqueuing directly could corrupt the list or > deadlock. > > Defer as call_rcu() does: stage the callback on the srcu_data's > ->defer_cbs, chain that srcu_data onto a per-CPU list, and kick a per-CPU > irq_work that re-issues it straight to the enqueue helper -- never back > through __call_srcu(), so it cannot defer again. > > The irq_work is global (per CPU, not per srcu_struct) and statically > initialized, so the deferral never runs check_init_srcu_struct(). It is > IRQ_WORK_INIT_HARD to re-issue promptly, as for call_rcu(). srcu_barrier() > and cleanup_srcu_struct() flush it first, and rcutree_migrate_callbacks() > calls srcu_offline_drain() to drain an outgoing CPU's deferred callbacks. > Gated by CONFIG_RCU_DEFER, as for call_rcu(). > > Suggested-by: Paul E. McKenney <[email protected]> > Signed-off-by: Puranjay Mohan <[email protected]> > --- > include/linux/srcutree.h | 4 ++ > kernel/rcu/rcu.h | 3 + > kernel/rcu/srcutree.c | 138 ++++++++++++++++++++++++++++++++++++++- > kernel/rcu/tree.c | 2 + > 4 files changed, 144 insertions(+), 3 deletions(-) > > diff --git a/include/linux/srcutree.h b/include/linux/srcutree.h > index 75e54e4f963fa..1ce759fb70948 100644 > --- a/include/linux/srcutree.h > +++ b/include/linux/srcutree.h > @@ -13,6 +13,8 @@ > > #include <linux/rcu_node_tree.h> > #include <linux/completion.h> > +#include <linux/irq_work_types.h> > +#include <linux/llist.h> > > struct srcu_node; > struct srcu_struct; > @@ -41,6 +43,8 @@ struct srcu_data { > bool srcu_cblist_invoking; /* Invoking these CBs? */ > struct timer_list delay_work; /* Delay for CB invoking */ > struct work_struct work; /* Context for CB invoking. */ > + struct llist_head defer_cbs; /* Callbacks deferred on re-entry. */ > + struct llist_node defer_link; /* Links onto the per-CPU deferral drain list */ > struct rcu_head srcu_barrier_head; /* For srcu_barrier() use. */ > struct rcu_head srcu_ec_head; /* For srcu_expedite_current() use. */ > int srcu_ec_state; /* State for srcu_expedite_current(). */ > diff --git a/kernel/rcu/rcu.h b/kernel/rcu/rcu.h > index ed6604445f2ac..036d28516e92d 100644 > --- a/kernel/rcu/rcu.h > +++ b/kernel/rcu/rcu.h > @@ -587,6 +587,9 @@ static inline bool should_rcu_defer(void) > return irqs_disabled() && rcu_scheduler_active != RCU_SCHEDULER_INACTIVE; > } > > +/* Drain an outgoing CPU's deferred SRCU callbacks; see rcutree_migrate_callbacks(). */ > +void srcu_offline_drain(int cpu); > + > enum rcutorture_type { > RCU_FLAVOR, > RCU_TASKS_FLAVOR, > diff --git a/kernel/rcu/srcutree.c b/kernel/rcu/srcutree.c > index 304112674e8a2..b9fe57ff91000 100644 > --- a/kernel/rcu/srcutree.c > +++ b/kernel/rcu/srcutree.c > @@ -20,6 +20,7 @@ > #include <linux/percpu.h> > #include <linux/preempt.h> > #include <linux/irq_work.h> > +#include <linux/llist.h> > #include <linux/rcupdate_wait.h> > #include <linux/sched.h> > #include <linux/smp.h> > @@ -79,6 +80,46 @@ static void process_srcu(struct work_struct *work); > static void srcu_irq_work(struct irq_work *work); > static void srcu_delay_timer(struct timer_list *t); > > +static void srcu_defer_drain(struct irq_work *iw); > + > +/* > + * Per-CPU call_srcu() deferral state. The irq_work is global (shared by every > + * srcu_struct), so it must find which srcu_structs deferred callbacks: a > + * callback is staged on its srcu_data's ->defer_cbs, and that srcu_data is > + * chained (->defer_link) onto this per-CPU ->list that the irq_work walks. > + */ > +struct srcu_defer { > + struct llist_head list; > + struct irq_work iw; > + raw_spinlock_t lock; > +}; > + > +static DEFINE_PER_CPU(struct srcu_defer, srcu_defer) = { > + .lock = __RAW_SPIN_LOCK_UNLOCKED(srcu_defer.lock), > + .iw = IRQ_WORK_INIT_HARD(srcu_defer_drain), > +}; > + > +/* > + * Register pending deferred call_srcu() callbacks so a following srcu_barrier() > + * waits for them. An online CPU's own irq_work re-issues its callbacks, so wait > + * it out; an offline CPU's irq_work may never run again, so drain its list > + * directly onto this CPU instead. > + */ > +static void srcu_defer_flush(void) > +{ > + int cpu; > + > + if (!IS_ENABLED(CONFIG_RCU_DEFER)) > + return; > + > + for_each_possible_cpu(cpu) { > + if (cpu_online(cpu)) > + irq_work_sync(&per_cpu(srcu_defer, cpu).iw); > + else > + srcu_defer_drain(&per_cpu(srcu_defer, cpu).iw); > + } > +} > + > /* > * Initialize SRCU per-CPU data. Note that statically allocated > * srcu_struct structures might already have srcu_read_lock() and > @@ -107,6 +148,11 @@ static void init_srcu_struct_data(struct srcu_struct *ssp) > sdp->cpu = cpu; > INIT_WORK(&sdp->work, srcu_invoke_callbacks); > timer_setup(&sdp->delay_work, srcu_delay_timer, 0); > + /* > + * ->defer_cbs and ->defer_link are valid when zeroed and are not > + * reinitialized here, lest we clobber callbacks a reentrant > + * call_srcu() already staged. See __call_srcu(). > + */ > sdp->ssp = ssp; > } > } > @@ -697,6 +743,8 @@ void cleanup_srcu_struct(struct srcu_struct *ssp) > return; /* Just leak it! */ > /* Wait for irq_work to finish first as it may queue a new work. */ > irq_work_sync(&sup->irq_work); > + /* Drain any deferred callbacks (shared irq_work) before freeing ->sda. */ > + srcu_defer_flush(); 1. Should we move srcu_defer_flush() before irq_work_sync(&sup->irq_work) ? 2. Will the following scenario trigger soft/hard_lockup ? In irq_work context, irq disbaled: srcu_defer_drain ->srcu_do_enqueue ->srcu_gp_start_if_needed ->rcu_segcblist_enqueue fentry ebpf prog call_rcu_tasks_trace() ->__call_srcu() ->should_rcu_defer() = true ->llist_add() + irq_work_queue() the ipi send and irq_work run again -> srcu_defer_drain()->srcu_do_enqueue Did I miss something? Thanks Zqiang > flush_delayed_work(&sup->work); > for_each_possible_cpu(cpu) { > struct srcu_data *sdp = per_cpu_ptr(ssp->sda, cpu); > @@ -1410,8 +1458,18 @@ static unsigned long srcu_gp_start_if_needed(struct srcu_struct *ssp, > * srcu_read_lock(), and srcu_read_unlock() that are all passed the same > * srcu_struct structure. > */ > -static void __call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp, > - rcu_callback_t func, bool do_norm) > +/* > + * The srcu_cblist and the srcu_node tree are only ever accessed with interrupts > + * disabled (srcu_gp_start_if_needed() enqueues under raw_spin_lock_irqsave() and > + * may walk the tree; callback invocation and grace-period work likewise). So > + * like call_rcu(), enqueuing while a list operation is in flight on this CPU > + * would corrupt it or deadlock -- e.g. call_rcu_tasks_trace(), which is > + * call_srcu() on a dedicated srcu_struct, from a BPF program. __call_srcu() > + * defers whenever interrupts are disabled, via a per-CPU irq_work; only kernels > + * where that can happen (CONFIG_RCU_DEFER) need this. > + */ > +static void srcu_do_enqueue(struct srcu_struct *ssp, struct rcu_head *rhp, > + rcu_callback_t func, bool do_norm) > { > if (debug_rcu_head_queue(rhp)) { > /* Probable double call_srcu(), so leak the callback. */ > @@ -1423,6 +1481,72 @@ static void __call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp, > (void)srcu_gp_start_if_needed(ssp, rhp, do_norm); > } > > +static void __call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp, > + rcu_callback_t func, bool do_norm) > +{ > + if (should_rcu_defer()) { > + struct srcu_data *sdp = this_cpu_ptr(ssp->sda); > + > + rhp->func = func; > + if (llist_add((struct llist_node *)rhp, &sdp->defer_cbs)) { > + /* First deferral on this srcu_data: chain it for the drain. */ > + struct srcu_defer *sndp = this_cpu_ptr(&srcu_defer); > + > + sdp->ssp = ssp; > + if (llist_add(&sdp->defer_link, &sndp->list)) > + irq_work_queue(&sndp->iw); > + } > + return; > + } > + > + /* An NMI reaching here entered with irqs enabled, so the enqueue can race. */ > + WARN_ON_ONCE(IS_ENABLED(CONFIG_PROVE_RCU) && in_nmi()); > + > + srcu_do_enqueue(ssp, rhp, func, do_norm); > +} > + > +/* > + * Re-issue this CPU's deferred callbacks, going straight to srcu_do_enqueue() so > + * they cannot defer again. The irq_work walks its ->list of srcu_data, each > + * carrying its own ->defer_cbs. ->lock is held across the drain so drainers > + * (irq_work, srcu_defer_flush() and srcu_offline_drain()) serialize and never > + * leave a callback off the lists yet not on a callback list. > + */ > +static void srcu_defer_drain(struct irq_work *iw) > +{ > + struct srcu_defer *sndp = container_of(iw, struct srcu_defer, iw); > + struct llist_node *snode, *snext; > + unsigned long flags; > + > + raw_spin_lock_irqsave(&sndp->lock, flags); > + llist_for_each_safe(snode, snext, llist_del_all(&sndp->list)) { > + struct srcu_data *sdp = container_of(snode, struct srcu_data, defer_link); > + struct srcu_struct *ssp = sdp->ssp; > + struct llist_node *cnode, *cnext; > + > + cnode = llist_del_all(&sdp->defer_cbs); > + llist_for_each_safe(cnode, cnext, cnode) { > + struct rcu_head *rhp = (struct rcu_head *)cnode; > + > + srcu_do_enqueue(ssp, rhp, rhp->func, true); > + } > + } > + raw_spin_unlock_irqrestore(&sndp->lock, flags); > +} > + > +/* > + * Drain @cpu's deferred call_srcu() callbacks; called from > + * rcutree_migrate_callbacks() once @cpu is dead. One pass covers every > + * srcu_struct, as the per-CPU srcu_defer list chains them all, and the > + * re-issue lands on the current CPU's srcu_data. > + */ > +void srcu_offline_drain(int cpu) > +{ > + if (!IS_ENABLED(CONFIG_RCU_DEFER)) > + return; > + srcu_defer_drain(&per_cpu(srcu_defer, cpu).iw); > +} > + > /** > * call_srcu() - Queue a callback for invocation after an SRCU grace period > * @ssp: srcu_struct in queue the callback > @@ -1677,9 +1801,17 @@ void srcu_barrier(struct srcu_struct *ssp) > { > int cpu; > int idx; > - unsigned long s = rcu_seq_snap(&ssp->srcu_sup->srcu_barrier_seq); > + unsigned long s; > > check_init_srcu_struct(ssp); > + > + /* > + * Register any deferred callbacks before snapshotting the sequence. The > + * shared irq_work may also drain other srcu_structs', which is harmless. > + */ > + srcu_defer_flush(); > + > + s = rcu_seq_snap(&ssp->srcu_sup->srcu_barrier_seq); > mutex_lock(&ssp->srcu_sup->srcu_barrier_mutex); > if (rcu_seq_done(&ssp->srcu_sup->srcu_barrier_seq, s)) { > smp_mb(); /* Force ordering following return. */ > diff --git a/kernel/rcu/tree.c b/kernel/rcu/tree.c > index 93c4242345e67..f8c6930041746 100644 > --- a/kernel/rcu/tree.c > +++ b/kernel/rcu/tree.c > @@ -4627,6 +4627,8 @@ void rcutree_migrate_callbacks(int cpu) > * returns; the re-issue lands on this CPU. > */ > rcu_defer_drain(&rdp->defer_work); > + /* Likewise for the outgoing CPU's deferred call_srcu() callbacks. */ > + srcu_offline_drain(cpu); > > if (rcu_rdp_is_offloaded(rdp)) > return; > -- > 2.53.0-Meta >