mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
To: "Paul E . McKenney" <paulmck@kernel.org>
Cc: linux-kernel@vger.kernel.org,
	Mathieu Desnoyers <mathieu.desnoyers@efficios.com>,
	Boqun Feng <boqun@kernel.org>,
	Josh Triplett <josh@joshtriplett.org>,
	Frederic Weisbecker <frederic@kernel.org>,
	Neeraj Upadhyay <neeraj.upadhyay@kernel.org>,
	Joel Fernandes <joelagnelf@nvidia.com>,
	Uladzislau Rezki <urezki@gmail.com>,
	Steven Rostedt <rostedt@goodmis.org>,
	Lai Jiangshan <jiangshanlai@gmail.com>,
	Zqiang <qiang.zhang@linux.dev>,
	Linus Torvalds <torvalds@linux-foundation.org>,
	Matthew Wilcox <willy@infradead.org>,
	Christian Brauner <brauner@kernel.org>,
	Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
	"Liam R . Howlett" <liam@infradead.org>,
	Lorenzo Stoakes <ljs@kernel.org>,
	Alice Ryhl <aliceryhl@google.com>,
	Andrew Ballance <andrewjballance@gmail.com>,
	Peter Zijlstra <peterz@infradead.org>,
	Will Deacon <will@kernel.org>,
	Mark Rutland <mark.rutland@arm.com>,
	Ingo Molnar <mingo@kernel.org>, Thomas Gleixner <tglx@kernel.org>,
	rcu@vger.kernel.org
Subject: [RFC PATCH 09/12] rcu_txn: Test a list writer against concurrent readers
Date: Fri, 16 Oct 2026 09:33:16 -0400	[thread overview]
Message-ID: <20261016133319.433617-10-mathieu.desnoyers@efficios.com> (raw)
In-Reply-To: <20261016133319.433617-1-mathieu.desnoyers@efficios.com>

Run one writer on a key-sorted rcu_txn_list while kthreads walk it
forward and backward.

Each walk must see sorted keys.  That alone cannot catch an operation
whose two edges are published separately, since such a list is still
sorted in each direction taken alone.  So, during phases where the
writer only inserts, each forward step p -> q also loads the prev of q
and requires its key to be at least that of p.  That catches an insert
that publishes its forward edge before its backward one, as
list_add_rcu() does; the other order cannot be told from a completed
insert by key.

The test is marked slow.  It needs more than one CPU to exercise real
concurrency.

Assisted-by: LLM
Signed-off-by: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
---
 Documentation/RCU/rcu_txn.rst |   4 +-
 lib/tests/rcu_txn_kunit.c     | 307 ++++++++++++++++++++++++++++++++++
 2 files changed, 310 insertions(+), 1 deletion(-)

diff --git a/Documentation/RCU/rcu_txn.rst b/Documentation/RCU/rcu_txn.rst
index 7bac4bd02456..133ef1ade43e 100644
--- a/Documentation/RCU/rcu_txn.rst
+++ b/Documentation/RCU/rcu_txn.rst
@@ -212,9 +212,11 @@ Testing
 =======
 
 ``CONFIG_RCU_TXN_KUNIT_TEST`` builds the KUnit suite ``rcu_txn``, which covers
-the engine, both data structures and the debug checks::
+the engine, both data structures and the debug checks, and runs one writer
+against concurrent readers of a list::
 
 	./tools/testing/kunit/kunit.py run --arch=x86_64 \
+		--qemu_args="-smp 4" \
 		--kconfig_add CONFIG_RCU_TXN_KUNIT_TEST=y \
 		--kconfig_add CONFIG_DEBUG_KERNEL=y \
 		--kconfig_add CONFIG_DEBUG_RCU_TXN=y \
diff --git a/lib/tests/rcu_txn_kunit.c b/lib/tests/rcu_txn_kunit.c
index 9b67caaa5db7..fa01dcb7df2b 100644
--- a/lib/tests/rcu_txn_kunit.c
+++ b/lib/tests/rcu_txn_kunit.c
@@ -7,11 +7,15 @@
 
 #include <kunit/test.h>
 #include <linux/bug.h>
+#include <linux/kthread.h>
+#include <linux/minmax.h>
 #include <linux/rcu_txn.h>
 #include <linux/rcu_txn_hlist.h>
 #include <linux/rcu_txn_list.h>
 #include <linux/rcupdate.h>
+#include <linux/sched.h>
 #include <linux/sched/task.h>
+#include <linux/slab.h>
 #include <linux/string.h>
 
 #include "../rcu_txn_internal.h"	/* the steps of a commit */
@@ -1686,6 +1690,308 @@ static void rcu_txn_test_list_debug_disjoint_compose(struct kunit *test)
 	list_expect(test, &head, keys, ARRAY_SIZE(keys));
 }
 
+/*
+ * One writer mutating a key-sorted list while several readers iterate it
+ * forward and backward.
+ *
+ * Two invariants, because the first one alone cannot see the property this
+ * test exists for.
+ *
+ * (1) Monotonicity, per direction.  The writer keeps the list sorted by key at
+ * all times, so a forward walk must see strictly increasing keys and a reverse
+ * walk strictly decreasing ones.  Out-of-range keys and runaway walks are
+ * caught here too.
+ *
+ * What invariant (1) cannot catch is a torn two-edge publish.  Take an insert
+ * of X between P and Q publishing only its forward edge: the forward ring is
+ * P -> X -> Q, still strictly increasing, and the reverse ring is Q -> P, still
+ * strictly decreasing.  Every torn state of a two-edge operation is sorted in
+ * each direction separately.
+ *
+ * (2) Cross-direction agreement, which is where a torn publish shows.  After
+ * each forward step p -> q the reader also resolves the prev of q and requires
+ * its key to be at least p's.  A forward-only torn insert yields the
+ * predecessor that is not back-linked yet, whose key is smaller than p's.  A
+ * reverse-only torn insert is not caught: the prev of q points to X, whose key
+ * lies between p's and q's, as after a completed insert.  list_add_rcu()
+ * publishes the forward edge first, which is the tear this check is for.
+ *
+ * Invariant (2) holds only in a phase without deletes.  Under concurrent
+ * deletes it is simply false: delete p between the reader's two loads and the
+ * prev of q legitimately becomes the node before p, whose key is smaller.  With
+ * inserts alone the legal outcomes are p itself or a node inserted between p
+ * and q, both with a key at least p's.
+ *
+ * The phase change is a handshake.  The writer numbers its phases, the odd ones
+ * allowing deletes, and after publishing an odd phase waits for every reader
+ * to acknowledge that number.  A reader samples the phase at the top of an
+ * iteration and acknowledges an odd one there, so that no cross-checking walk
+ * is in flight when the writer proceeds; an acknowledgment of an earlier
+ * phase does not count.  Store-release and load-acquire order the walks and
+ * the deletes against the handshake.
+ */
+
+#define CONC_KEY_MAX		100		/* keys are 1..CONC_KEY_MAX */
+#define CONC_READERS		4
+#define CONC_STEP_LIMIT		(CONC_KEY_MAX + 8)	/* runaway-walk guard */
+#define CONC_INSERT_ROUNDS	20
+#define CONC_MIXED_OPS		20000
+
+struct cn {
+	struct rcu_txn_list_node node;
+	struct rcu_head rcu;
+	int key;
+};
+
+struct conc {
+	struct rcu_txn_list_head head;
+	unsigned long phase;	/* odd: deletes as well as inserts */
+	struct cn *live[CONC_KEY_MAX + 1];	/* the writer's: node per key */
+};
+
+struct conc_reader {
+	struct conc *conc;
+	struct task_struct *task;
+	long walks;
+	long violations;	/* invariant (1) */
+	long cross_checks;
+	long cross_violations;	/* invariant (2) */
+	unsigned long ack;	/* last odd phase seen: no cross-check in flight */
+};
+
+static int conc_reader_fn(void *arg)
+{
+	struct conc_reader *r = arg;
+	struct rcu_txn_list_head *head = &r->conc->head;
+
+	while (!kthread_should_stop()) {
+		/* Pairs with the writer's store-release of the phase. */
+		unsigned long phase = smp_load_acquire(&r->conc->phase);
+		bool insert_only = !(phase & 1);
+		struct rcu_txn_list_node *p;
+		int prev_key, steps;
+
+		if (!insert_only) {
+			/* Pairs with the writer's load-acquire of the ack. */
+			smp_store_release(&r->ack, phase);
+		}
+
+		/* Forward: strictly increasing keys, bounded. */
+		rcu_read_lock();
+		prev_key = 0;
+		steps = 0;
+		rcu_txn_list_for_each_rcu(p, head) {
+			int key = container_of(p, struct cn, node)->key;
+
+			if (key < 1 || key > CONC_KEY_MAX || key <= prev_key ||
+			    ++steps > CONC_STEP_LIMIT) {
+				r->violations++;
+				break;
+			}
+			/*
+			 * Invariant (2).  Skipped for the first step, whose
+			 * predecessor is the head.
+			 */
+			if (insert_only && prev_key) {
+				struct rcu_txn_list_node *back =
+						rcu_txn_list_prev_rcu(p);
+
+				r->cross_checks++;
+				if (back != &head->node &&
+				    container_of(back, struct cn,
+						 node)->key < prev_key)
+					r->cross_violations++;
+			}
+			prev_key = key;
+		}
+		rcu_read_unlock();
+
+		/* Reverse: strictly decreasing keys, bounded. */
+		rcu_read_lock();
+		prev_key = CONC_KEY_MAX + 1;
+		steps = 0;
+		rcu_txn_list_for_each_reverse_rcu(p, head) {
+			int key = container_of(p, struct cn, node)->key;
+
+			if (key < 1 || key > CONC_KEY_MAX || key >= prev_key ||
+			    ++steps > CONC_STEP_LIMIT) {
+				r->violations++;
+				break;
+			}
+			prev_key = key;
+		}
+		rcu_read_unlock();
+
+		r->walks += 2;
+		cond_resched();
+	}
+	return 0;
+}
+
+/* Insert @key, keeping the list sorted.  Returns false if it cannot. */
+static bool conc_insert(struct conc *c, int key)
+{
+	struct rcu_txn_list_node *succ = &c->head.node;	/* default: last */
+	struct cn *n = kmalloc(sizeof(*n), GFP_KERNEL);
+	int i;
+
+	if (!n)
+		return false;
+	n->key = key;
+	for (i = key + 1; i <= CONC_KEY_MAX; i++) {
+		if (c->live[i]) {
+			succ = &c->live[i]->node;
+			break;
+		}
+	}
+	if (rcu_txn_list_add_before_rcu(&n->node, succ, GFP_KERNEL)) {
+		kfree(n);
+		return false;
+	}
+	c->live[key] = n;
+	return true;
+}
+
+static bool conc_del(struct conc *c, int key)
+{
+	struct cn *n = c->live[key];
+
+	if (rcu_txn_list_del_rcu(&n->node, GFP_KERNEL))
+		return false;
+	c->live[key] = NULL;
+	kfree_rcu(n, rcu);
+	return true;
+}
+
+/* Replace the node of @key by a new one of the same key: order is kept. */
+static bool conc_replace(struct conc *c, int key)
+{
+	struct cn *old = c->live[key];
+	struct cn *n = kmalloc(sizeof(*n), GFP_KERNEL);
+
+	if (!n)
+		return false;
+	n->key = key;
+	if (rcu_txn_list_replace_rcu(&old->node, &n->node, GFP_KERNEL)) {
+		kfree(n);
+		return false;
+	}
+	c->live[key] = n;
+	kfree_rcu(old, rcu);
+	return true;
+}
+
+static void rcu_txn_test_list_concurrent(struct kunit *test)
+{
+	struct conc_reader *readers;
+	long walks = 0, violations = 0, cross_checks = 0, cross_violations = 0;
+	u32 rng = 2463534242u;
+	bool ok = true;
+	struct conc *c;
+	int i, key, round;
+
+	c = kunit_kzalloc(test, sizeof(*c), GFP_KERNEL);
+	readers = kunit_kcalloc(test, CONC_READERS, sizeof(*readers),
+				GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, c);
+	KUNIT_ASSERT_NOT_NULL(test, readers);
+	rcu_txn_list_init(&c->head);
+
+	for (i = 0; i < CONC_READERS; i++) {
+		readers[i].conc = c;
+		readers[i].task = kthread_run(conc_reader_fn, &readers[i],
+					      "rcu_txn_reader/%d", i);
+		if (IS_ERR(readers[i].task)) {
+			readers[i].task = NULL;
+			ok = false;
+		}
+	}
+
+	/*
+	 * An even phase, repeated: fill the list one key at a time, with
+	 * inserts only, so that the readers cross-check against concurrent
+	 * inserts.  Then hand over to an odd phase and empty it with deletes.
+	 */
+	for (round = 0; ok && round < CONC_INSERT_ROUNDS; round++) {
+		int order[CONC_KEY_MAX], other;
+
+		for (i = 0; i < CONC_KEY_MAX; i++)
+			order[i] = i + 1;
+		for (i = CONC_KEY_MAX - 1; i > 0; i--) {	/* shuffle */
+			rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5;
+			other = rng % (i + 1);
+			swap(order[i], order[other]);
+		}
+
+		/* An even phase, after the deletes of the previous one. */
+		smp_store_release(&c->phase, (c->phase | 1) + 1);
+
+		for (i = 0; ok && i < CONC_KEY_MAX; i++) {
+			ok = conc_insert(c, order[i]);
+			/*
+			 * Let the readers walk in between.  On one cpu they
+			 * only run in this phase if the writer sleeps in it.
+			 */
+			if (i == CONC_KEY_MAX / 2)
+				schedule_timeout_uninterruptible(1);
+			else
+				cond_resched();
+		}
+
+		/* An odd phase: wait until no even phase walk is running. */
+		smp_store_release(&c->phase, c->phase + 1);
+		for (i = 0; i < CONC_READERS; i++) {
+			if (!readers[i].task)
+				continue;
+			/* Pairs with the reader's store-release of its ack. */
+			while (smp_load_acquire(&readers[i].ack) != c->phase)
+				schedule_timeout_uninterruptible(1);
+		}
+
+		for (key = 1; ok && key <= CONC_KEY_MAX; key++) {
+			if (c->live[key])
+				ok = conc_del(c, key);
+			cond_resched();
+		}
+	}
+
+	/* Then inserts, deletes and replaces, at random. */
+	for (i = 0; ok && i < CONC_MIXED_OPS; i++) {
+		rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5;
+		key = 1 + rng % CONC_KEY_MAX;
+
+		if (!c->live[key])
+			ok = conc_insert(c, key);
+		else if (!(rng & 0x300))
+			ok = conc_replace(c, key);
+		else
+			ok = conc_del(c, key);
+		cond_resched();
+	}
+
+	for (i = 0; i < CONC_READERS; i++) {
+		if (readers[i].task)
+			kthread_stop(readers[i].task);
+		walks += readers[i].walks;
+		violations += readers[i].violations;
+		cross_checks += readers[i].cross_checks;
+		cross_violations += readers[i].cross_violations;
+	}
+	for (key = 1; key <= CONC_KEY_MAX; key++)
+		if (c->live[key])
+			conc_del(c, key);
+
+	kunit_info(test, "%ld walks, %ld cross-direction checks\n", walks,
+		   cross_checks);
+	KUNIT_EXPECT_TRUE(test, ok);
+	KUNIT_EXPECT_GT(test, walks, 0);
+	/* Forward and reverse walks stayed sorted. */
+	KUNIT_EXPECT_EQ(test, violations, 0);
+	/* No two-edge publish was ever seen torn. */
+	KUNIT_EXPECT_GT(test, cross_checks, 0);
+	KUNIT_EXPECT_EQ(test, cross_violations, 0);
+}
+
 static struct kunit_case rcu_txn_test_cases[] = {
 	KUNIT_CASE(rcu_txn_test_single_record),
 	KUNIT_CASE(rcu_txn_test_multi_record),
@@ -1744,6 +2050,7 @@ static struct kunit_case rcu_txn_test_cases[] = {
 	KUNIT_CASE(rcu_txn_test_list_move_phases),
 	KUNIT_CASE(rcu_txn_test_list_compose_moves),
 	KUNIT_CASE(rcu_txn_test_list_debug_disjoint_compose),
+	KUNIT_CASE_SLOW(rcu_txn_test_list_concurrent),
 	{}
 };
 
-- 
2.47.3


  parent reply	other threads:[~2026-10-16 13:33 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-16 13:33 [RFC PATCH 00/12] RCU pseudo-transactions Mathieu Desnoyers
2026-10-16 13:33 ` [RFC PATCH 01/12] rcu_txn: Add single-writer " Mathieu Desnoyers
2026-10-16 13:33 ` [RFC PATCH 02/12] rcu_txn: Add KUnit tests Mathieu Desnoyers
2026-10-16 13:33 ` [RFC PATCH 03/12] rcu_txn: Skip the record lookup with a Bloom filter Mathieu Desnoyers
2026-10-16 13:33 ` [RFC PATCH 04/12] rcu_txn: Add CONFIG_DEBUG_RCU_TXN Mathieu Desnoyers
2026-10-16 13:33 ` [RFC PATCH 05/12] rcu_txn: Add an hlist edited through transactions Mathieu Desnoyers
2026-10-16 13:33 ` [RFC PATCH 06/12] rcu_txn: Add a doubly-linked list coherent in both directions Mathieu Desnoyers
2026-10-16 13:33 ` [RFC PATCH 07/12] Documentation/RCU: Describe RCU pseudo-transactions Mathieu Desnoyers
2026-10-16 13:33 ` [RFC PATCH 08/12] Documentation/litmus-tests: Add RCU pseudo-transaction tests Mathieu Desnoyers
2026-10-16 13:33 ` Mathieu Desnoyers [this message]
2026-10-16 13:33 ` [RFC PATCH 10/12] rcu_txn: Test a commit from atomic context Mathieu Desnoyers
2026-10-16 13:33 ` [RFC PATCH 11/12] samples: Add a cache on RCU pseudo-transactions under one mutex Mathieu Desnoyers
2026-10-16 13:33 ` [RFC PATCH 12/12] samples: Add a cache on RCU pseudo-transactions with a lock per node Mathieu Desnoyers

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=20261016133319.433617-10-mathieu.desnoyers@efficios.com \
    --to=mathieu.desnoyers@efficios.com \
    --cc=aliceryhl@google.com \
    --cc=andrewjballance@gmail.com \
    --cc=boqun@kernel.org \
    --cc=brauner@kernel.org \
    --cc=frederic@kernel.org \
    --cc=gregkh@linuxfoundation.org \
    --cc=jiangshanlai@gmail.com \
    --cc=joelagnelf@nvidia.com \
    --cc=josh@joshtriplett.org \
    --cc=liam@infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=ljs@kernel.org \
    --cc=mark.rutland@arm.com \
    --cc=mingo@kernel.org \
    --cc=neeraj.upadhyay@kernel.org \
    --cc=paulmck@kernel.org \
    --cc=peterz@infradead.org \
    --cc=qiang.zhang@linux.dev \
    --cc=rcu@vger.kernel.org \
    --cc=rostedt@goodmis.org \
    --cc=tglx@kernel.org \
    --cc=torvalds@linux-foundation.org \
    --cc=urezki@gmail.com \
    --cc=will@kernel.org \
    --cc=willy@infradead.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®