srcu: Make Tiny call_srcu() safe to call from any context

Give Tiny call_srcu() the same treatment as Tree SRCU.  When interrupts
are disabled and the scheduler is up, stage the callback on the
srcu_struct's lockless list for an irq_work to re-issue later.  Tiny SRCU
is uniprocessor, so there is no CPU-offline drain.  A draining flag drops
a deferring call_srcu() that re-enters mid-drain (unless from an NMI), as
in Tree SRCU; such a drop can strand state the caller tied to the callback,
not just the callback itself.

Interrupts stay off for the whole batch.  TINY_SRCU implies !SMP, where
arch_irq_work_has_interrupt() is false, so the drain always waits for the
tick and a batch is whatever one tick's worth of interrupts-disabled
call_srcu()s staged.  Unlike the other three flavors srcu_do_enqueue() here
has no debug_rcu_head_queue(), so nothing reports a double call_srcu();
termination of the drain rests on srcu_do_enqueue() clearing ->next, and
the callback list self-links at the tail exactly as a double call_srcu()
made it before.

srcu_barrier() (now out of line) and cleanup_srcu_struct() drain the
deferred list first, so a deferred callback is re-issued onto the callback
list and invoked by the grace-period work that cleanup_srcu_struct()
flushes, rather than stranded on a soon-to-be-freed srcu_struct.
cleanup_srcu_struct() also syncs ->defer_iw, since that irq_work is
embedded in the srcu_struct the caller is about to free.

The draining flag is global rather than per-srcu_struct: a re-entrant
call_srcu(B) inside a drain of A raises B's own ->defer_iw, whose drain can
stage back onto A, so a per-srcu_struct flag would not break the chain.
The cost is that a drain of A also drops a non-NMI call_srcu() to any
other srcu_struct for its duration.

Gated by CONFIG_RCU_DEFER like Tree SRCU, though the srcu_struct members
are unconditional.

Suggested-by: Paul E. McKenney <paulmck@kernel.org>
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Signed-off-by: Paul E. McKenney <paulmck@kernel.org>
This commit is contained in:
Puranjay Mohan 2026-08-10 05:27:53 -07:00 committed by Paul E. McKenney
parent 35a26d8704
commit a04b2854be
2 changed files with 97 additions and 8 deletions

View File

@ -12,6 +12,7 @@
#define _LINUX_SRCU_TINY_H
#include <linux/irq_work_types.h>
#include <linux/llist.h>
#include <linux/swait.h>
struct srcu_struct {
@ -26,6 +27,8 @@ struct srcu_struct {
struct rcu_head **srcu_cb_tail; /* Pending callbacks: Tail. */
struct work_struct srcu_work; /* For driving grace periods. */
struct irq_work srcu_irq_work; /* Defer schedule_work() to irq work. */
struct llist_head defer_cbs; /* Callbacks deferred on re-entry. */
struct irq_work defer_iw; /* Re-issues defer_cbs later. */
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif /* #ifdef CONFIG_DEBUG_LOCK_ALLOC */
@ -33,6 +36,7 @@ struct srcu_struct {
void srcu_drive_gp(struct work_struct *wp);
void srcu_tiny_irq_work(struct irq_work *irq_work);
void srcu_defer_drain(struct irq_work *irq_work);
#define __SRCU_STRUCT_INIT(name, __ignored, ___ignored, ____ignored) \
{ \
@ -40,6 +44,9 @@ void srcu_tiny_irq_work(struct irq_work *irq_work);
.srcu_cb_tail = &name.srcu_cb_head, \
.srcu_work = __WORK_INITIALIZER(name.srcu_work, srcu_drive_gp), \
.srcu_irq_work = { .func = srcu_tiny_irq_work }, \
.defer_cbs = LLIST_HEAD_INIT(name.defer_cbs), \
.defer_iw = { .node = { .u_flags = IRQ_WORK_HARD_IRQ }, \
.func = srcu_defer_drain }, \
__SRCU_DEP_MAP_INIT(name) \
}
@ -131,10 +138,7 @@ static inline void synchronize_srcu_expedited(struct srcu_struct *ssp)
synchronize_srcu(ssp);
}
static inline void srcu_barrier(struct srcu_struct *ssp)
{
synchronize_srcu(ssp);
}
void srcu_barrier(struct srcu_struct *ssp);
static inline void srcu_expedite_current(struct srcu_struct *ssp) { }
#define srcu_check_read_flavor(ssp, read_flavor) do { } while (0)

View File

@ -10,6 +10,7 @@
#include <linux/export.h>
#include <linux/irq_work.h>
#include <linux/llist.h>
#include <linux/mutex.h>
#include <linux/preempt.h>
#include <linux/rcupdate_wait.h>
@ -29,6 +30,8 @@ extern int rcu_scheduler_active;
static LIST_HEAD(srcu_boot_list);
static bool srcu_init_done;
static void __srcu_defer_drain(struct srcu_struct *ssp);
static int init_srcu_struct_fields(struct srcu_struct *ssp)
{
ssp->srcu_lock_nesting[0] = 0;
@ -43,6 +46,8 @@ static int init_srcu_struct_fields(struct srcu_struct *ssp)
INIT_WORK(&ssp->srcu_work, srcu_drive_gp);
INIT_LIST_HEAD(&ssp->srcu_work.entry);
init_irq_work(&ssp->srcu_irq_work, srcu_tiny_irq_work);
init_llist_head(&ssp->defer_cbs);
ssp->defer_iw = IRQ_WORK_INIT_HARD(srcu_defer_drain);
return 0;
}
@ -86,6 +91,16 @@ EXPORT_SYMBOL_GPL(init_srcu_struct_generic);
void cleanup_srcu_struct(struct srcu_struct *ssp)
{
WARN_ON(srcu_readers_active(ssp));
/*
* Re-issue any deferred callbacks, then wait out ->defer_iw before it is
* freed. Skipped entirely with CONFIG_RCU_DEFER=n: irq_work_sync() ends
* in an unconditional synchronize_rcu() wherever
* arch_irq_work_has_interrupt() is false, which is every !SMP target.
*/
if (IS_ENABLED(CONFIG_RCU_DEFER)) {
__srcu_defer_drain(ssp);
irq_work_sync(&ssp->defer_iw);
}
irq_work_sync(&ssp->srcu_irq_work);
flush_work(&ssp->srcu_work);
WARN_ON(ssp->srcu_gp_running);
@ -213,11 +228,11 @@ static void srcu_gp_start_if_needed(struct srcu_struct *ssp)
}
/*
* Enqueue an SRCU callback on the specified srcu_struct structure,
* initiating grace-period processing if it is not already running.
* Also called by __srcu_defer_drain() to re-issue a deferred callback, so it
* must not re-check the deferral condition.
*/
void call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp,
rcu_callback_t func)
static void srcu_do_enqueue(struct srcu_struct *ssp, struct rcu_head *rhp,
rcu_callback_t func)
{
unsigned long flags;
@ -231,6 +246,68 @@ void call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp,
srcu_gp_start_if_needed(ssp);
preempt_enable();
}
/*
* Set only by the irq_work drain, the one drain its own re-issue can re-feed;
* a callback staged during a direct drain is taken by ->defer_iw afterwards.
* Global rather than per-srcu_struct: a re-entrant call_srcu(B) inside a drain
* of A raises B's own ->defer_iw, whose drain can stage back onto A.
*/
static bool srcu_defer_draining;
static void __srcu_defer_drain(struct srcu_struct *ssp)
{
struct llist_node *node, *next;
unsigned long flags;
if (!IS_ENABLED(CONFIG_RCU_DEFER))
return;
/* Re-issued newest-first; nothing depends on call_srcu() ordering. */
local_irq_save(flags);
llist_for_each_safe(node, next, llist_del_all(&ssp->defer_cbs)) {
struct rcu_head *rhp = (struct rcu_head *)node;
srcu_do_enqueue(ssp, rhp, rhp->func);
}
local_irq_restore(flags);
}
/* Only the irq_work drain can be re-fed by its own re-issue; see Tree SRCU. */
void srcu_defer_drain(struct irq_work *iw)
{
struct srcu_struct *ssp = container_of(iw, struct srcu_struct, defer_iw);
WRITE_ONCE(srcu_defer_draining, true);
__srcu_defer_drain(ssp);
WRITE_ONCE(srcu_defer_draining, false);
}
EXPORT_SYMBOL_GPL(srcu_defer_drain);
void call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp,
rcu_callback_t func)
{
if (should_rcu_defer()) {
/* A re-entrant call_srcu() during the drain would livelock it. */
if (READ_ONCE(srcu_defer_draining) && !in_nmi()) {
WARN_ONCE(IS_ENABLED(CONFIG_PROVE_RCU),
"call_srcu() re-entered during callback drain; leaking callback\n");
return;
}
rhp->func = func;
if (llist_add((struct llist_node *)rhp, &ssp->defer_cbs))
irq_work_queue(&ssp->defer_iw);
return;
}
/*
* Only reachable from an NMI when deferral is off: before the scheduler
* is up, or with CONFIG_RCU_DEFER=n. The enqueue can then race.
*/
WARN_ON_ONCE(IS_ENABLED(CONFIG_PROVE_RCU) && in_nmi());
srcu_do_enqueue(ssp, rhp, func);
}
EXPORT_SYMBOL_GPL(call_srcu);
/*
@ -260,6 +337,14 @@ void synchronize_srcu(struct srcu_struct *ssp)
}
EXPORT_SYMBOL_GPL(synchronize_srcu);
/* Register any deferred callbacks, then wait for all in-flight ones. */
void srcu_barrier(struct srcu_struct *ssp)
{
__srcu_defer_drain(ssp);
synchronize_srcu(ssp);
}
EXPORT_SYMBOL_GPL(srcu_barrier);
/*
* get_state_synchronize_srcu - Provide an end-of-grace-period cookie
*/