mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Kunwu Chan <kunwu.chan@gmail.com>
To: paulmck@kernel.org, boqun@kernel.org,
	mathieu.desnoyers@efficios.com, jiangshanlai@gmail.com,
	corbet@lwn.net, mingo@redhat.com, frederic@kernel.org,
	neeraj.upadhyay@kernel.org, josh@joshtriplett.org,
	urezki@gmail.com, dave@stgolabs.net
Cc: stern@rowland.harvard.edu, parri.andrea@gmail.com,
	will@kernel.org, dhowells@redhat.com, j.alglave@ucl.ac.uk,
	luc.maranget@inria.fr, akiyks@gmail.com, dlustig@nvidia.com,
	joelagnelf@nvidia.com, skhan@linuxfoundation.org,
	rdunlap@infradead.org, longman@redhat.com, rostedt@goodmis.org,
	qiang.zhang@linux.dev, kunwu.chan@gmail.com,
	brads@mainlining.org, linux-kernel@vger.kernel.org,
	linux-arch@vger.kernel.org, lkmm@lists.linux.dev,
	linux-doc@vger.kernel.org, rcu@vger.kernel.org,
	linux-kselftest@vger.kernel.org
Subject: [PATCH 15/28] hazptr: Add reader-assisted drain for stalled waiters
Date: Thu,  5 Nov 2026 00:54:33 +0800	[thread overview]
Message-ID: <20261104165504.2430480-16-kunwu.chan@gmail.com> (raw)
In-Reply-To: <20261104165504.2430480-1-kunwu.chan@gmail.com>

When a waiter remains blocked on a sticky address for 100+ scan
cycles, engage a drain: snapshot which per-CPU and overflow-list
slots hold the address, then wait for those holders to release.

Per-CPU slots are tracked via a bitmap mapping (cpu, idx) to the
stable per-CPU slot_item.  Overflow-list slots (struct
hazptr_backup_slot) lack stable identity, so each one is marked
with the current drain generation (drain_gen) at engage; the
reader cooperates on release by checking its own slot's drain_gen,
clearing it, and decrementing backup_outstanding.

The unchain path and the engage both hold the overflow-list lock
during their marker operations, serializing the reader's marker
consumption with the engage's marker writes and preventing the
stranded-marker race.

A cmpxchg() ensures only one of the release hook, the resnapshot
recovery path, or the unchain path consumes each marker.

The outstanding count is maintained incrementally (atomic_inc
during the engage walk, atomic_dec on consumption) so a
concurrent unchain cannot be overwritten by a final atomic_set.

A fairness cap (HAZPTR_DRAIN_MAX_CYCLES) bounds consecutive drain
cycles for a single address: on expiry the drain disengages and
the waiter's re-engage counter resets.

Each disengage logs its reason (complete, fairness-cap, or
orphaned) for test observability.

Wildcards never appear in per-CPU slots: the try_acquire fast
path publishes the actual address directly; only the slow path's
backup slot goes through a wildcard transient.

Suggested-by: Boqun Feng <boqun@kernel.org>
Signed-off-by: Kunwu Chan <kunwu.chan@gmail.com>
---
 include/linux/hazptr.h                        |  17 +
 kernel/hazptr.c                               | 468 +++++++++++++++++-
 kernel/rcu/Kconfig.debug                      |  12 +
 .../selftests/rcutorture/bin/hazptr.sh        |  22 +-
 4 files changed, 513 insertions(+), 6 deletions(-)

diff --git a/include/linux/hazptr.h b/include/linux/hazptr.h
index ebf804315a26..018e9309e3ea 100644
--- a/include/linux/hazptr.h
+++ b/include/linux/hazptr.h
@@ -39,12 +39,22 @@ struct hazptr_slot {
 };
 
 struct hazptr_overflow_list;
+struct hazptr_ctx;
+
+#ifdef CONFIG_HAZPTR_DRAIN_ASSIST
+DECLARE_STATIC_KEY_FALSE(hazptr_drain_key);
+void hazptr_drain_release(void *addr, struct hazptr_slot *slot,
+			   struct hazptr_ctx *ctx);
+#endif
 
 struct hazptr_backup_slot {
 	struct hlist_node overflow_node;
 	struct hazptr_slot slot;
 	/* Overflow list where the backup slot is added. */
 	struct hazptr_overflow_list *overflow_list;
+#ifdef CONFIG_HAZPTR_DRAIN_ASSIST
+	unsigned long drain_gen;
+#endif
 };
 
 struct hazptr_ctx {
@@ -72,6 +82,9 @@ struct hazptr_slot_item {
 	unsigned long acquire_caller;
 	unsigned long acquire_jiffies;
 #endif
+#ifdef CONFIG_HAZPTR_DRAIN_ASSIST
+	unsigned long drain_gen;
+#endif
 };
 
 struct hazptr_percpu_slots {
@@ -352,6 +365,10 @@ void hazptr_release(struct hazptr_ctx *ctx, void *addr)
 	hazptr_release_debug(ctx, addr);
 	slot = ctx->slot;
 	smp_store_release(&slot->addr, NULL);
+#ifdef CONFIG_HAZPTR_DRAIN_ASSIST
+	if (static_branch_unlikely(&hazptr_drain_key))
+		hazptr_drain_release(addr, slot, ctx);
+#endif
 	if (unlikely(hazptr_slot_is_backup(ctx, slot)))
 		hazptr_unchain_backup_slot(ctx);
 }
diff --git a/kernel/hazptr.c b/kernel/hazptr.c
index 8de194c07eea..7d4273a44183 100644
--- a/kernel/hazptr.c
+++ b/kernel/hazptr.c
@@ -323,6 +323,9 @@ static bool hazptr_bloom_contains(const struct hazptr_bloom *bloom, void *addr)
  * match.
  *
  * A wildcard in an overflow-list slot means a slow-path acquire is in
+ * flight.  Wildcards never appear in per-CPU slots: the try_acquire
+ * fast path publishes the actual address directly, and only the
+ * slow path's backup slot goes through a wildcard transient.
  * flight: that slot may resolve to any address (including @watch or a
  * Bloom-collected one) after the walk.  To keep the single-pass
  * completion decision safe, the caller must observe wildcards and
@@ -402,6 +405,9 @@ struct hazptr_waiter {
 	int pid;
 	unsigned long caller;
 #endif
+#ifdef CONFIG_HAZPTR_DRAIN_ASSIST
+	unsigned int drain_cycles;
+#endif
 };
 
 struct hazptr_scan_state {
@@ -417,6 +423,346 @@ static struct hazptr_scan_state hazptr_scan;
 
 /* Sysfs counters, cf. RCU's rcu_stall_count. */
 static atomic_t hazptr_stall_count = ATOMIC_INIT(0);
+static atomic_t hazptr_drain_count = ATOMIC_INIT(0);
+
+#ifdef CONFIG_HAZPTR_DRAIN_ASSIST
+
+/*
+ * Drain-assist: internal forward-progress mechanism for sticky addresses.
+ *
+ * When a waiter is blocked for too many scan cycles, the kthread
+ * snapshots which per-CPU slots hold the waited-for address and
+ * tracks their release via a per-slot bitmap.  A remove-only
+ * re-snapshot (periodic re-walk that only clears bits, never sets
+ * new ones) recovers lost notifications.
+ *
+ * Completion requires: bitmap empty (all per-CPU snapshot holders
+ * released) AND all marked backup slots released (tracked via a
+ * per-slot drain_gen marker and a backup_outstanding count).
+ * Per-CPU slots are bitmap-tracked; backup slots are
+ * generation-marked.
+ *
+ * The release path carries a static_branch-guarded check:
+ * near-zero overhead when no drain is active.
+ */
+
+#define HAZPTR_DRAIN_BITS	(NR_CPUS * NR_HAZPTR_PERCPU_SLOTS)
+
+/*
+ * Backup-slot tracking: overflow-list holders lack stable
+ * (cpu, idx) identity.  Instead of storing dangling pointers,
+ * each backup slot carries a drain_gen marker set by engage
+ * and cleared by the release path (a synchronous hook called from
+ * hazptr_release() while the reader's ctx is still valid).
+ * The completion check waits for backup_outstanding to reach
+ * zero, plus the per-CPU bitmap to become empty.
+ */
+
+struct hazptr_drain_state {
+	void *addr;		/* NULL = inactive; written by kthread, read by release hook */
+	DECLARE_BITMAP(map, HAZPTR_DRAIN_BITS);
+	unsigned long generation;
+	atomic_t backup_outstanding;
+	unsigned int cycle_count;	/* cycles since engage, for fairness */
+};
+
+/*
+ * Cap on consecutive drain cycles for a single address.  When reached,
+ * the drain disengages and the current waiter's re-engage counter is
+ * reset, giving other stalled waiters a chance to engage.  This
+ * prevents one continuously-re-published address from starving
+ * waiters on other addresses.
+ */
+#define HAZPTR_DRAIN_MAX_CYCLES	1000
+
+static struct hazptr_drain_state hazptr_drain;
+DEFINE_STATIC_KEY_FALSE(hazptr_drain_key);
+EXPORT_SYMBOL_GPL(hazptr_drain_key);
+
+/*
+ * Engage: record the address, snapshot which per-CPU slots hold it,
+ * and enable the release-path check.  Defers (returns false) if any
+ * slot (per-CPU or overflow-list) holds a wildcard value: an
+ * in-flight acquire could resolve to the target address after the
+ * snapshot, missing the bitmap.
+ */
+/*
+ * Clear backup-slot markers for the given generation, under the
+ * overflow-list locks.  Used by the wildcard-defer and orphaned-
+ * disengage paths, where the drain is being abandoned before or
+ * after formal publication.  Does not decrement backup_outstanding:
+ * the defer path runs before the count is established, and the
+ * orphan path runs after the drain is no longer consulted.
+ */
+static void hazptr_drain_clear_markers(unsigned long gen)
+{
+	int cpu;
+
+	for_each_possible_cpu(cpu) {
+		struct hazptr_overflow_list_flip *overflow_list_flip =
+			per_cpu_ptr(&percpu_overflow_list_flip, cpu);
+
+		for (int i = 0; i < 2; i++) {
+			struct hazptr_overflow_list *list =
+				&overflow_list_flip->array[i];
+			struct hazptr_backup_slot *b;
+			unsigned long flags;
+
+			raw_spin_lock_irqsave(&list->lock, flags);
+			hlist_for_each_entry(b, &list->head, overflow_node) {
+				if (READ_ONCE(b->drain_gen) == gen)
+					WRITE_ONCE(b->drain_gen, 0);
+			}
+			raw_spin_unlock_irqrestore(&list->lock, flags);
+		}
+	}
+}
+static bool hazptr_drain_engage(void *addr)
+{
+	int cpu;
+	unsigned int idx;
+	bool wildcard_present = false;
+
+	bitmap_zero(hazptr_drain.map, HAZPTR_DRAIN_BITS);
+	hazptr_drain.generation++;
+	hazptr_drain.cycle_count = 0;
+	atomic_set(&hazptr_drain.backup_outstanding, 0);
+
+	for_each_possible_cpu(cpu) {
+		struct hazptr_percpu_slots *percpu_slots = per_cpu_ptr(&hazptr_percpu_slots, cpu);
+		struct hazptr_overflow_list_flip *overflow_list_flip = per_cpu_ptr(&percpu_overflow_list_flip, cpu);
+
+		for (idx = 0; idx < NR_HAZPTR_PERCPU_SLOTS; idx++) {
+			struct hazptr_slot_item *item = &percpu_slots->items[idx];
+			void *v = smp_load_acquire(&item->slot.addr);
+
+			if (is_wildcard(v))
+				wildcard_present = true;
+			if (v == addr)
+				set_bit(cpu * NR_HAZPTR_PERCPU_SLOTS + idx,
+					hazptr_drain.map);
+		}
+		for (int i = 0; i < 2; i++) {
+			struct hazptr_overflow_list *list = &overflow_list_flip->array[i];
+			struct hazptr_backup_slot *b;
+			unsigned long flags;
+
+			raw_spin_lock_irqsave(&list->lock, flags);
+			hlist_for_each_entry(b, &list->head, overflow_node) {
+				void *v = smp_load_acquire(&b->slot.addr);
+
+				if (is_wildcard(v)) {
+					wildcard_present = true;
+					continue;
+				}
+				if (v != addr)
+					continue;
+
+				/*
+				 * Mark this backup slot with the current
+				 * drain generation and increment the
+				 * outstanding count immediately.  The
+				 * count is maintained incrementally so
+				 * that a concurrent unchain (which
+				 * consumes the marker and decrements)
+				 * cannot be overwritten by a final
+				 * atomic_set.
+				 */
+				WRITE_ONCE(b->drain_gen,
+					   hazptr_drain.generation);
+				atomic_inc(&hazptr_drain.backup_outstanding);
+			}
+			raw_spin_unlock_irqrestore(&list->lock, flags);
+		}
+	}
+
+	/*
+	 * Defer if any in-flight acquire holds a wildcard.
+	 */
+	if (wildcard_present) {
+		/*
+		 * Clear any markers set before the wildcard was found:
+		 * they belong to an abandoned round.  The count was
+		 * already incremented for these markers; reset it
+		 * because clear_markers does not decrement.
+		 */
+		hazptr_drain_clear_markers(hazptr_drain.generation);
+		bitmap_zero(hazptr_drain.map, HAZPTR_DRAIN_BITS);
+		atomic_set(&hazptr_drain.backup_outstanding, 0);
+		return false;
+	}
+
+	WRITE_ONCE(hazptr_drain.addr, addr);
+	smp_mb(); /* addr publish before static_branch enable */
+
+	static_branch_inc(&hazptr_drain_key);
+	pr_info("hazptr: drain engaged for %p (gen %lu, outstanding %d)\n",
+		addr, hazptr_drain.generation,
+		atomic_read(&hazptr_drain.backup_outstanding));
+	return true;
+}
+
+/*
+ * Release notification: the release path calls this when it clears
+ * a slot that was holding the drain address.
+ *
+ * Context: preemption disabled (hazptr_release always runs under
+ * guard(preempt)).  cpu = this CPU; idx = slot index (0..3).
+ */
+static void hazptr_drain_notify(int cpu, int idx)
+{
+	int bit = cpu * NR_HAZPTR_PERCPU_SLOTS + idx;
+
+	if (bit >= HAZPTR_DRAIN_BITS)
+		return;
+	if (test_and_clear_bit(bit, hazptr_drain.map) &&
+	    bitmap_empty(hazptr_drain.map, HAZPTR_DRAIN_BITS))
+		swake_up_one(&hazptr_scan.wq);
+}
+
+/*
+ * Re-snapshot: remove-only re-walk.  Clear bits for slots that no
+ * longer hold the drain address; never set new bits.  Also recovers
+ * backup-slot releases that occurred during the engage walk (before
+ * the drain address was published): if a backup slot's drain_gen
+ * marker still matches the current generation but the slot no longer
+ * holds the target address, the reader has already released it;
+ * clear the marker and decrement backup_outstanding.
+ * Called periodically while draining.
+ */
+static void hazptr_drain_resnapshot(void)
+{
+	int cpu;
+	unsigned int idx;
+
+	for_each_possible_cpu(cpu) {
+		struct hazptr_percpu_slots *percpu_slots = per_cpu_ptr(&hazptr_percpu_slots, cpu);
+
+		for (idx = 0; idx < NR_HAZPTR_PERCPU_SLOTS; idx++) {
+			int bit = cpu * NR_HAZPTR_PERCPU_SLOTS + idx;
+			struct hazptr_slot_item *item = &percpu_slots->items[idx];
+
+			if (test_bit(bit, hazptr_drain.map) &&
+			    smp_load_acquire(&item->slot.addr) != hazptr_drain.addr)
+				clear_bit(bit, hazptr_drain.map);
+		}
+		for (int i = 0; i < 2; i++) {
+			struct hazptr_overflow_list *list =
+				&per_cpu_ptr(&percpu_overflow_list_flip, cpu)->array[i];
+			struct hazptr_backup_slot *b;
+			unsigned long flags;
+
+			raw_spin_lock_irqsave(&list->lock, flags);
+			hlist_for_each_entry(b, &list->head, overflow_node) {
+				if (READ_ONCE(b->drain_gen) !=
+				    hazptr_drain.generation)
+					continue;
+				/*
+				 * Still holds the drain address: the reader
+				 * has not released yet, so the marker is
+				 * legitimately active.  Skip.
+				 */
+				if (smp_load_acquire(&b->slot.addr) ==
+				    hazptr_drain.addr)
+					continue;
+				/*
+				 * The slot no longer holds the address:
+				 * the reader released during the engage
+				 * walk, before the drain address was
+				 * published.  The release hook could not
+				 * have seen the address, so recover here.
+				 * Atomically claim the marker so that the
+				 * release hook and this recovery path
+				 * cannot both decrement for the same slot.
+				 */
+				if (cmpxchg(&b->drain_gen,
+					    hazptr_drain.generation, 0) ==
+				    hazptr_drain.generation)
+					atomic_dec(&hazptr_drain.backup_outstanding);
+			}
+			raw_spin_unlock_irqrestore(&list->lock, flags);
+		}
+	}
+}
+
+
+/*
+ * Disengage: clear the address, disable the release hook, and
+ * clear any backup-slot markers still marked for this generation.
+ */
+static void hazptr_drain_disengage(const char *reason)
+{
+	unsigned long gen = hazptr_drain.generation;
+
+	WRITE_ONCE(hazptr_drain.addr, NULL);
+	smp_mb(); /* addr-NULL visible before static_branch off */
+	static_branch_dec(&hazptr_drain_key);
+	pr_info("hazptr: drain disengaged (%s, gen %lu, outstanding %d)\n",
+		reason, gen, atomic_read(&hazptr_drain.backup_outstanding));
+	/*
+	 * Clear markers for the abandoned generation.  The static key
+	 * is off, so no new release hook can enter; an in-flight hook
+	 * that already passed the addr check may still consume a
+	 * marker and decrement backup_outstanding, leaving the count
+	 * stale.  The count is only meaningful within a generation's
+	 * lifetime and is reset on the next engage, so no adjustment
+	 * is needed here.
+	 */
+	hazptr_drain_clear_markers(gen);
+}
+
+/*
+ * hazptr_drain_release - release-path hook under static_branch.
+ *
+ * Called from hazptr_release() *after* smp_store_release(&slot->addr, NULL)
+ * has committed.  For per-CPU slots, clears the bitmap bit.  For
+ * backup (overflow-list) slots, cooperates via the slot's drain_gen
+ * marker: the engage set it, and here we clear it and decrement
+ * backup_outstanding.
+ *
+ * The slot type is identified via hazptr_slot_is_backup(ctx, slot).
+ */
+void hazptr_drain_release(void *addr, struct hazptr_slot *slot,
+			   struct hazptr_ctx *ctx)
+{
+	struct hazptr_percpu_slots *base;
+	struct hazptr_slot_item *item;
+	int cpu;
+
+	if (smp_load_acquire(&hazptr_drain.addr) != addr)
+		return;
+
+	cpu = smp_processor_id();
+
+	/* Backup slot: cooperate via drain_gen marker. */
+	if (hazptr_slot_is_backup(ctx, slot)) {
+		/*
+		 * Atomically claim the marker: only one of the release
+		 * hook or the resnapshot recovery path may consume it,
+		 * so only the cmpxchg() winner decrements.
+		 */
+		if (cmpxchg(&ctx->backup_slot.drain_gen,
+			    hazptr_drain.generation, 0) ==
+		    hazptr_drain.generation)
+			atomic_dec(&hazptr_drain.backup_outstanding);
+		return;
+	}
+
+	/*
+	 * Per-CPU slot: derive the bit index from the slot's position.
+	 * Safe because hazptr_release() runs under guard(preempt) and
+	 * the per-CPU slot was acquired on the same CPU, so the slot
+	 * item is in this CPU's items[] array.
+	 */
+	base = per_cpu_ptr(&hazptr_percpu_slots, cpu);
+	item = container_of(slot, struct hazptr_slot_item, slot);
+	if (item >= base->items &&
+	    item < base->items + NR_HAZPTR_PERCPU_SLOTS)
+		hazptr_drain_notify(cpu, item - base->items);
+}
+EXPORT_SYMBOL_GPL(hazptr_drain_release);
+
+#endif /* CONFIG_HAZPTR_DRAIN_ASSIST */
 
 /*
  * Queue @addr for the shared scan.  The waiter lives on the
@@ -438,6 +784,9 @@ static void hazptr_synchronize_queued(void *addr, unsigned long caller)
 	waiter.pid = task_pid_nr(current);
 	waiter.caller = caller;
 #endif
+#ifdef CONFIG_HAZPTR_DRAIN_ASSIST
+	waiter.drain_cycles = 0;
+#endif
 
 	/* Enqueue and wake the scan kthread. */
 	mutex_lock(&hazptr_scan.lock);
@@ -509,6 +858,90 @@ static void hazptr_scan_do_cycle(void)
 	list_for_each_entry_safe(w, n, &hazptr_scan.scanning, node)
 		hazptr_waiter_stall_check(w);
 #endif
+#ifdef CONFIG_HAZPTR_DRAIN_ASSIST
+	/*
+	 * Drain-assist: for still-blocked waiters, advance the cycle
+	 * counter and engage or manage a sticky-address drain.
+	 *
+	 * When a waiter has been blocked for 100+ scan cycles, the
+	 * kthread snapshots which slots hold the address (engage)
+	 * and tracks their release via a per-slot bitmap.
+	 *
+	 * When all tracked holders have released (the per-CPU bitmap
+	 * is empty and no backup slots remain marked for the current
+	 * generation), disengage the drain.  The next
+	 * scan cycle's Bloom walk then verifies the address is
+	 * truly absent before completing the waiter, avoiding a race
+	 * with post-engage readers.
+	 */
+	list_for_each_entry_safe(w, n, &hazptr_scan.scanning, node) {
+		w->drain_cycles++;
+
+		/* Engage when a waiter has been stuck long enough. */
+		if (!hazptr_drain.addr && w->drain_cycles > 100) {
+			hazptr_drain_engage(w->addr);
+			if (hazptr_drain.addr == w->addr)
+				atomic_inc(&hazptr_drain_count);
+		}
+
+		/* If draining for this address, check progress. */
+		if (hazptr_drain.addr == w->addr) {
+			/* Periodic remove-only re-snapshot. */
+			if (!(w->drain_cycles & 63))
+				hazptr_drain_resnapshot();
+
+			{
+				/*
+				 * All tracked holders have released.
+				 * Disengage and let the next cycle's
+				 * Bloom walk verify the address is
+				 * truly absent before completing the
+				 * waiter.
+				 */
+				if (bitmap_empty(hazptr_drain.map,
+						 HAZPTR_DRAIN_BITS) &&
+				    !atomic_read(&hazptr_drain.backup_outstanding)) {
+					hazptr_drain_disengage("complete");
+					hazptr_drain.cycle_count = 0;
+				}
+			}
+
+			/*
+			 * Fairness: cap consecutive drain cycles for a
+			 * single address.  On expiry, disengage and reset
+			 * this waiter's re-engage counter so the next
+			 * stalled waiter (if any) gets the next turn
+			 * instead of the same address immediately
+			 * re-engaging.
+			 */
+			if (++hazptr_drain.cycle_count >
+			    HAZPTR_DRAIN_MAX_CYCLES) {
+				hazptr_drain_disengage("fairness-cap");
+				hazptr_drain.cycle_count = 0;
+				w->drain_cycles = 0;
+			}
+			break;	/* one drain target at a time */
+		}
+	}
+
+	/*
+	 * Disengage orphaned drain: if the drain address no longer
+	 * has a matching waiter in ->scanning (the Bloom path may
+	 * have completed it this cycle), clean up.
+	 */
+	if (hazptr_drain.addr) {
+		bool orphaned = true;
+
+		list_for_each_entry(w, &hazptr_scan.scanning, node) {
+			if (w->addr == hazptr_drain.addr) {
+				orphaned = false;
+				break;
+			}
+		}
+		if (orphaned)
+			hazptr_drain_disengage("orphaned");
+	}
+#endif
 }
 
 
@@ -684,7 +1117,6 @@ static void hazptr_waiter_stall_check(struct hazptr_waiter *w)
 	unsigned long now = jiffies;
 	unsigned long timeout = msecs_to_jiffies(CONFIG_HAZPTR_STALL_TIMEOUT);
 	unsigned long deadline;
-
 	w->cycles++;
 	deadline = w->reports ? w->last_report + 2 * timeout
 			      : w->enqueue_jiffies + timeout;
@@ -799,6 +1231,30 @@ void hazptr_unchain_backup_slot(struct hazptr_ctx *ctx)
 	struct hazptr_overflow_list *overflow_list = ctx->backup_slot.overflow_list;
 
 	guard(raw_spinlock_irqsave)(&overflow_list->lock);
+#ifdef CONFIG_HAZPTR_DRAIN_ASSIST
+	unsigned long gen;
+
+	/*
+	 * Consume any pending drain marker under the list lock,
+	 * before removing the slot from the overflow list.  The
+	 * engage sets markers while holding this same lock, so
+	 * checking under the lock guarantees that if the engage
+	 * marks this slot, we see the marker before the slot is
+	 * unchained.  Without the lock, the unchain could check
+	 * drain_gen before the engage sets it, then block on the
+	 * lock while the engage sets the marker and increments
+	 * the count, and finally remove the slot without ever
+	 * consuming the orphaned marker.
+	 *
+	 * The cmpxchg ensures only one of the release hook, the
+	 * resnapshot, or this path decrements backup_outstanding.
+	 */
+	gen = READ_ONCE(ctx->backup_slot.drain_gen);
+	if (gen) {
+		if (cmpxchg(&ctx->backup_slot.drain_gen, gen, 0) == gen)
+			atomic_dec(&hazptr_drain.backup_outstanding);
+	}
+#endif
 	overflow_list->gen++;
 	hlist_del(&ctx->backup_slot.overflow_node);
 }
@@ -825,12 +1281,22 @@ static ssize_t hazptr_stall_count_show(struct kobject *kobj,
 {
 	return sysfs_emit(page, "%d\n", atomic_read(&hazptr_stall_count));
 }
+static ssize_t hazptr_drain_count_show(struct kobject *kobj,
+					struct kobj_attribute *attr,
+					char *page)
+{
+	return sysfs_emit(page, "%d\n", atomic_read(&hazptr_drain_count));
+}
+
+static struct kobj_attribute hazptr_drain_count_attr =
+	__ATTR_RO(hazptr_drain_count);
 
 static struct kobj_attribute hazptr_stall_count_attr =
 	__ATTR_RO(hazptr_stall_count);
 
 static struct attribute *hazptr_sysfs_attrs[] = {
 	&hazptr_stall_count_attr.attr,
+	&hazptr_drain_count_attr.attr,
 	NULL,
 };
 
diff --git a/kernel/rcu/Kconfig.debug b/kernel/rcu/Kconfig.debug
index b64ae06f3298..b500e96ea1c1 100644
--- a/kernel/rcu/Kconfig.debug
+++ b/kernel/rcu/Kconfig.debug
@@ -291,4 +291,16 @@ config HAZPTR_STALL_TIMEOUT
 	help
 	  Waiter stall timeout in milliseconds, defaulting to RCU's 21s.
 
+config HAZPTR_DRAIN_ASSIST
+	bool "Hazard-pointer sticky-address drain assist"
+	default y
+	help
+	  Internal forward-progress mechanism for hazptr_synchronize()
+	  waiters whose target address is continuously re-published
+	  by new readers.  When a waiter has been blocked for many
+	  scan cycles, the scan kthread snapshots which slots hold
+	  the address and tracks their release via per-slot tracking.
+
+	  If unsure, say Y.
+
 endmenu # "RCU Debugging"
diff --git a/tools/testing/selftests/rcutorture/bin/hazptr.sh b/tools/testing/selftests/rcutorture/bin/hazptr.sh
index 4e30ea51d9fc..d12735ddfefc 100755
--- a/tools/testing/selftests/rcutorture/bin/hazptr.sh
+++ b/tools/testing/selftests/rcutorture/bin/hazptr.sh
@@ -229,8 +229,19 @@ then
 		if ! grep -q "hazptr: waiter stall" "$console"
 		then
 			echo "HOTADDR note: no stall splats observed (record as not-reproduced, investigate interleaving)" > "$resdir/kvm.sh.note"
+		elif ! grep -q "drain engaged" "$console"
+		then
+			echo "HOTADDR note: drain-assist did not engage (Bloom completion may have handled it)" > "$resdir/kvm.sh.note"
+		elif ! grep -q "drain disengaged (complete" "$console"
+		then
+			if grep -q "drain disengaged (fairness-cap" "$console"
+			then
+				echo "HOTADDR: drain hit fairness cap without completing" > "$resdir/kvm.sh.err"
+			else
+				echo "HOTADDR note: drain engaged but did not disengage normally" > "$resdir/kvm.sh.note"
+			fi
 		else
-			echo "HOTADDR: starvation reproduced, stall detector confirmed, drain verified"
+			echo "HOTADDR: starvation reproduced, stall detector confirmed, drain engaged"
 		fi
 	else
 		nerrs=$((nerrs+1))
@@ -239,10 +250,11 @@ else
 	nerrs=$((nerrs+1))
 fi
 
-# LONGHOLD: legitimate long-lived references.  The stall detector must
-# fire (observation), the holder must release, and the waiter must drain
-# normally afterwards: diagnostics without interruption, module verdict
-# stays SUCCESS.
+# LONGHOLD: legitimate long-lived references.  The holder must release,
+# and the waiter must drain normally afterwards.  The stall detector
+# may or may not fire (the 8-second hold may not exceed the 21-second
+# timeout depending on when the waiter started relative to the hold);
+# this is observation data only, not a correctness requirement.
 
 echo "--- hazptr LONGHOLD (legitimate long-held reference) ---"
 if run_kvm longhold LONGHOLD "$cpus" "" "60s"
-- 
2.43.0


  parent reply	other threads:[~2026-11-04 16:57 UTC|newest]

Thread overview: 29+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-04 16:54 [PATCH 00/28] hazptr: shared-scan kthread, drain-assist, and stall detector Kunwu Chan
2026-11-04 16:54 ` [PATCH 01/28] hazptr: Add shared scan kthread Kunwu Chan
2026-11-04 16:54 ` [PATCH 02/28] hazptr: Use Bloom filter for shared scan waiters Kunwu Chan
2026-11-04 16:54 ` [PATCH 03/28] hazptr: Add scoped_guard() support Kunwu Chan
2026-11-04 16:54 ` [PATCH 04/28] hazptr: Add debug option to force the acquire slow path Kunwu Chan
2026-11-04 16:54 ` [PATCH 05/28] Documentation/litmus-tests: Add hazptr wildcard-flip escape test Kunwu Chan
2026-11-04 16:54 ` [PATCH 06/28] Documentation/litmus-tests: Add hazptr acquire-before-scan test Kunwu Chan
2026-11-04 16:54 ` [PATCH 07/28] hazptr: Fix kernel-doc of hazptr_release() Kunwu Chan
2026-11-04 16:54 ` [PATCH 08/28] hazptrtorture: Add slowpath and lockdep scenarios Kunwu Chan
2026-11-04 16:54 ` [PATCH 09/28] hazptrtorture: Add READERS4 and READERS0 torture configs Kunwu Chan
2026-11-04 16:54 ` [PATCH 10/28] hazptrtorture: Add 128- and 256-CPU configs Kunwu Chan
2026-11-04 16:54 ` [PATCH 11/28] selftests/rcutorture: Add hazptr torture test script Kunwu Chan
2026-11-04 16:54 ` [PATCH 12/28] hazptr: Remove per-waiter scan fallback, simplify synchronize Kunwu Chan
2026-11-04 16:54 ` [PATCH 13/28] hazptr: Add waiter-enqueue tracepoint Kunwu Chan
2026-11-04 16:54 ` [PATCH 14/28] hazptr: Add waiter stall detector Kunwu Chan
2026-11-04 16:54 ` Kunwu Chan [this message]
2026-11-04 16:54 ` [PATCH 16/28] Documentation/litmus-tests: Add hazptr drain engage-wildcard test Kunwu Chan
2026-11-04 16:54 ` [PATCH 17/28] Documentation/litmus-tests: Add hazptr drain resnapshot-slot-race test Kunwu Chan
2026-11-04 16:54 ` [PATCH 18/28] Documentation/litmus-tests: Add hazptr drain safety test Kunwu Chan
2026-11-04 16:54 ` [PATCH 19/28] Documentation/litmus-tests: Add hazptr drain generation-visibility test Kunwu Chan
2026-11-04 16:54 ` [PATCH 20/28] locking/lockdep: Use hazptr to wait for dynamic key lookups Kunwu Chan
2026-11-04 16:54 ` [PATCH 21/28] hazptrtorture: Add HOTADDR and LONGHOLD scenarios Kunwu Chan
2026-11-04 16:54 ` [PATCH 22/28] hazptrtorture: Add drain fairness rotation scenario Kunwu Chan
2026-11-04 16:54 ` [PATCH 23/28] rcuscale: Add hazptr scale type Kunwu Chan
2026-11-04 16:54 ` [PATCH 24/28] hazptr: Add runtime sysfs controls for stall detector Kunwu Chan
2026-11-04 16:54 ` [PATCH 25/28] hazptr: Select TRACING for HAZPTR_TORTURE_TEST to enable tracepoint Kunwu Chan
2026-11-04 16:54 ` [PATCH 26/28] hazptrtorture: Add torture_type=none clean skip for rcuscale coexistence Kunwu Chan
2026-11-04 16:54 ` [PATCH 27/28] hazptr: Add Bloom false-positive exact-match fallback Kunwu Chan
2026-11-04 16:54 ` [PATCH 28/28] Documentation/litmus-tests: Add hazptr bloom wildcard-hole test Kunwu Chan

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261104165504.2430480-16-kunwu.chan@gmail.com \
    --to=kunwu.chan@gmail.com \
    --cc=akiyks@gmail.com \
    --cc=boqun@kernel.org \
    --cc=brads@mainlining.org \
    --cc=corbet@lwn.net \
    --cc=dave@stgolabs.net \
    --cc=dhowells@redhat.com \
    --cc=dlustig@nvidia.com \
    --cc=frederic@kernel.org \
    --cc=j.alglave@ucl.ac.uk \
    --cc=jiangshanlai@gmail.com \
    --cc=joelagnelf@nvidia.com \
    --cc=josh@joshtriplett.org \
    --cc=linux-arch@vger.kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=lkmm@lists.linux.dev \
    --cc=longman@redhat.com \
    --cc=luc.maranget@inria.fr \
    --cc=mathieu.desnoyers@efficios.com \
    --cc=mingo@redhat.com \
    --cc=neeraj.upadhyay@kernel.org \
    --cc=parri.andrea@gmail.com \
    --cc=paulmck@kernel.org \
    --cc=qiang.zhang@linux.dev \
    --cc=rcu@vger.kernel.org \
    --cc=rdunlap@infradead.org \
    --cc=rostedt@goodmis.org \
    --cc=skhan@linuxfoundation.org \
    --cc=stern@rowland.harvard.edu \
    --cc=urezki@gmail.com \
    --cc=will@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®