summaryrefslogtreecommitdiff
path: root/kernel
diff options
context:
space:
mode:
authorPuranjay Mohan <puranjay@kernel.org>2026-08-10 05:27:53 -0700
committerPaul E. McKenney <paulmck@kernel.org>2026-10-01 17:18:24 -0700
commita04b2854bedc788d2e1bb421592c2a68633fd036 (patch)
treee9f15945048286270837a2fff24fa3cce8e5c959 /kernel
parent35a26d87042b7c9814165f244a141104655ed345 (diff)
downloadlinux-next-a04b2854bedc788d2e1bb421592c2a68633fd036.tar.gz
linux-next-a04b2854bedc788d2e1bb421592c2a68633fd036.zip
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>
Diffstat (limited to 'kernel')
-rw-r--r--kernel/rcu/srcutiny.c93
1 files changed, 89 insertions, 4 deletions
diff --git a/kernel/rcu/srcutiny.c b/kernel/rcu/srcutiny.c
index 558ba8d316db..5de9a6905838 100644
--- a/kernel/rcu/srcutiny.c
+++ b/kernel/rcu/srcutiny.c
@@ -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
*/