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
next prev 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®