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 <[email protected]>
---
 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


Reply via email to