Add a page on the RCU pseudo-transactions to the RCU handbook: when they apply and what they cost, how a commit proceeds, what readers and writers call, how a transaction allocates, and the two data structures built on them. It ends with the API reference extracted from the headers. The overview at the top of <linux/rcu_txn.h> points to it.
Assisted-by: LLM Signed-off-by: Mathieu Desnoyers <[email protected]> --- Documentation/RCU/index.rst | 1 + Documentation/RCU/rcu_txn.rst | 230 ++++++++++++++++++++++++++++++++++ MAINTAINERS | 1 + include/linux/rcu_txn.h | 2 + 4 files changed, 234 insertions(+) create mode 100644 Documentation/RCU/rcu_txn.rst diff --git a/Documentation/RCU/index.rst b/Documentation/RCU/index.rst index 035871687ee2..4bca16c8573b 100644 --- a/Documentation/RCU/index.rst +++ b/Documentation/RCU/index.rst @@ -18,6 +18,7 @@ RCU Handbook rcu rculist_nulls rcuref + rcu_txn torture stallwarn listRCU diff --git a/Documentation/RCU/rcu_txn.rst b/Documentation/RCU/rcu_txn.rst new file mode 100644 index 000000000000..6e48306ebc19 --- /dev/null +++ b/Documentation/RCU/rcu_txn.rst @@ -0,0 +1,230 @@ +.. SPDX-License-Identifier: GPL-2.0 + +===================================== +Single-writer RCU pseudo-transactions +===================================== + +An RCU pseudo-transaction switches a *set* of RCU-protected pointers from +their old values to their new values with a single store, as observed by +concurrent RCU readers: at any instant, every pointer of the set reads old, or +every pointer reads new. A reader that follows several of them, each load +depending on the previous one, sees old values, then new ones, and never an +old value after a new one. + +The implementation is in ``include/linux/rcu_txn.h`` and ``lib/rcu_txn.c``. +Code that uses it selects ``CONFIG_RCU_TXN``. + +When to use it +============== + +``rcu_assign_pointer()`` publishes one pointer atomically. An update that +re-points several pointers one at a time exposes its intermediate states: a +reader that loads two of them may get the new value of one and then the old +value of the other. Moving an object between two hash buckets is such an +update, and so is every insertion into or removal from a doubly-linked list. + +A pseudo-transaction removes the intermediate states. It does not replace the +update-side lock: the writers of a slot are excluded by the caller, and the +transaction neither detects nor resolves conflicts between them. It is +"pseudo" in that sense: there is no read set and no abort, and readers get no +snapshot. + +A pseudo-transaction is not free, on the update side above all. The writer +allocates a descriptor for each transaction, with the ``gfp_t`` given to +``rcu_txn_init()``, and a transaction that switches two pointers or more frees +it with ``kfree_rcu()``, a grace period after its commit, as readers may still +be reading it: an allocation, which can fail, and a deferred free for every +update, where ``rcu_assign_pointer()`` needs neither. The reader pays a test of +the low bits of every pointer it loads, which is nearly free: only while a +commit is in flight on that pointer does the reader also read the descriptor, +with three more loads, one of them an acquire. + +How it works +============ + +The pointers a transaction switches are called *slots*: they are the *edges* of +the data structure that the update re-points. The writer builds a *record* per +slot, holding the slot's old and new targets, and commits in three steps: + +1. *Install.* Each slot is made to point at its record, with tag bits set that + mark the value as a *proxy* rather than a target. A reader that finds a + proxy resolves it through the record, which still answers with the old + target. + +2. *Commit.* The records of a transaction share one word, the *selector* of + their *flip group*. The writer stores 1 to it. From then on every proxy of + the transaction resolves to its new target. + +3. *Settle.* Each slot is overwritten with its new target, and the records + are freed after a grace period. + +The selector is written once, from 0 to 1, so a reader that resolves several +slots of a transaction over time sees old targets, then new targets, and never +an old target after a new one, provided that each of its loads depends on the +previous one. The header describes the memory ordering in detail. + +A transaction of a single record installs no proxy: its commit is one +``rcu_assign_pointer()``. + +Tags +==== + +The embedder chooses the tag that marks a proxy in its slots: typically bit 0, +or a reserved value of a type code that its pointers already carry. The tag +must be within ``RCU_TXN_TAG_MASK``, the low bits that the alignment of a +record leaves clear, and no value that the embedder stores in a slot may carry +all of the tag's bits. + +Readers +======= + +A reader loads a slot with ``rcu_txn_dereference()`` instead of +``rcu_dereference()``:: + + rcu_read_lock(); + child = rcu_txn_dereference(node->left, FOO_TAG); + if (child) + do_something_with(child); + rcu_read_unlock(); + +``rcu_txn_dereference()`` is ``rcu_dereference()`` followed by +``rcu_txn_resolve()``. A reader that already has the loaded value, for +instance because it tests other bits of it, calls ``rcu_txn_resolve()`` +itself. + +The load and the resolution must happen in one RCU read-side critical section: +the record a proxy points at is freed with ``kfree_rcu()``. + +Writers +======= + +The writer declares a handle on its stack, records the edges of the update and +commits them:: + + struct rcu_txn txn; + struct foo *child; + int err; + + rcu_txn_init(&txn, GFP_KERNEL); + rcu_txn_reserve(&txn, 2); + + spin_lock(&foo_lock); + child = rcu_dereference_protected(a->left, lockdep_is_held(&foo_lock)); + rcu_txn_record(&txn, rcu_txn_slot(&a->left), child, NULL, FOO_TAG); + rcu_txn_record(&txn, rcu_txn_slot(&b->right), NULL, child, FOO_TAG); + err = rcu_txn_commit(&txn); + spin_unlock(&foo_lock); + +Here ``child`` moves from ``a->left`` to an empty ``b->right``: at any instant +it is in exactly one of the two places. + +Slots are ``__rcu`` pointers. ``rcu_txn_slot()`` converts the address of one, +of any type, to the ``void __rcu **`` the engine takes, and lets sparse check +the annotation. + +A handle ends in ``rcu_txn_commit()`` or in ``rcu_txn_cancel()``. Either +consumes it, and it must be initialized again before it is reused. + +Allocation +---------- + +A transaction allocates a descriptor, with the ``gfp_t`` given to +``rcu_txn_init()``. ``rcu_txn_reserve()`` allocates it for a given number of +records; otherwise the first ``rcu_txn_record()`` does, even for a single +record, and a later one may grow it. + +A failed allocation is sticky: later records do nothing, and +``rcu_txn_commit()`` publishes nothing and returns ``-ENOMEM``. A writer may +therefore ignore the return values of ``rcu_txn_reserve()`` and +``rcu_txn_record()`` and check that of ``rcu_txn_commit()`` only, as in the +example above. + +A writer that records under a spinlock reserves its record bound before it +takes the lock, as above, or initializes the handle with a non-sleeping +``gfp_t``. Under a raw or a bit spinlock only the reserve will do, since +``PREEMPT_RT`` does not allow the allocator there. ``rcu_txn_commit()`` and +``rcu_txn_cancel()`` allocate nothing, do not sleep, and may be called under +such a lock. + +An update that re-points a single slot needs no transaction. Every +transaction settles its slots before its commit returns, and the writers of the +slot are excluded, so the slot holds no proxy: ``rcu_assign_pointer()`` +publishes the update, as the ``_rcu`` operations of ``rcu_txn_hlist`` do. + +Writing a slot twice +-------------------- + +``rcu_txn_record()`` requires the records of a transaction to target distinct +slots. A writer that may write the same slot twice in one transaction reads +the slot with ``rcu_txn_load()``, which returns the transaction's pending value +for it, and records with ``rcu_txn_record_chain()``, which updates the record +the slot already has:: + + old = rcu_txn_load(&txn, slot, FOO_TAG); + rcu_txn_record_chain(&txn, slot, old, new, FOO_TAG); + +This is what allows several operations on one data structure to be composed in +one transaction. The lookup among the transaction's records is a linear scan, +behind a Bloom filter once the transaction holds enough of them. +``rcu_txn_declare_disjoint()`` turns the lookup off for a transaction whose +slots are known to be distinct. + +Data structures +=============== + +Two data structures are built on the engine. Each provides ``_prepare`` +operations, which record into a transaction owned by the caller, and ``_rcu`` +operations, which carry out a single operation by themselves. +Readers walk them with ``_rcu`` iterators. A writer that excludes the others +walks them with the same iterators outside of an RCU read-side critical +section, if it passes them a lockdep condition for that exclusion, such as +``lockdep_is_held()`` of its lock, as with ``list_for_each_entry_rcu()``. + +``include/linux/rcu_txn_hlist.h`` + A list with a single-pointer head, like ``hlist``. Its ``_prepare`` + operations compose: several nodes can be added to or removed from one + or several lists atomically. An ``_rcu`` operation publishes its + update with one ``rcu_assign_pointer()`` and allocates nothing. + +``include/linux/rcu_txn_list.h`` + A circular doubly-linked list whose forward and backward chains are + mutually coherent at every instant, which allows readers to walk it in + both directions. An insertion or a removal switches a forward and a + backward edge, and a move six, the moved node's own included: each + allocates a descriptor. + +Debugging +========= + +``CONFIG_DEBUG_RCU_TXN`` enables the checks that cost more than a test of the +handle's state. It validates the tags, reports a slot recorded twice, and +validates the single-writer contract: it warns when a handle is driven by a +task other than the one that initialized it, and when a slot that a transaction +records, installs to or settles was changed by another writer in the meantime. +A task that takes over a handle which another got ready, with nothing recorded +yet, calls ``rcu_txn_adopt()`` first, which makes it the task the check +expects. + +Misuse of a handle, such as recording into one that was already committed, +warns in every build. + +Testing +======= + +``CONFIG_RCU_TXN_KUNIT_TEST`` builds the KUnit suite ``rcu_txn``, which covers +the engine, both data structures and the debug checks:: + + ./tools/testing/kunit/kunit.py run --arch=x86_64 \ + --kconfig_add CONFIG_RCU_TXN_KUNIT_TEST=y \ + --kconfig_add CONFIG_DEBUG_KERNEL=y \ + --kconfig_add CONFIG_DEBUG_RCU_TXN=y \ + 'rcu_txn*' + +API +=== + +.. kernel-doc:: include/linux/rcu_txn.h + +.. kernel-doc:: include/linux/rcu_txn_hlist.h + +.. kernel-doc:: include/linux/rcu_txn_list.h diff --git a/MAINTAINERS b/MAINTAINERS index 5126f4ece4e7..f34040369eb4 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -22857,6 +22857,7 @@ RCU PSEUDO-TRANSACTIONS M: Mathieu Desnoyers <[email protected]> L: [email protected] S: Maintained +F: Documentation/RCU/rcu_txn.rst F: include/linux/rcu_txn*.h F: lib/rcu_txn* F: lib/tests/rcu_txn_kunit.c diff --git a/include/linux/rcu_txn.h b/include/linux/rcu_txn.h index b9ee242a01ce..19f1365ae8ae 100644 --- a/include/linux/rcu_txn.h +++ b/include/linux/rcu_txn.h @@ -12,6 +12,8 @@ * the slots, from their old to their new targets with a single store, as * observed by concurrent RCU readers: at any instant, every slot of the set * resolves to its old target, or every slot to its new one. + * Documentation/RCU/rcu_txn.rst says when to use them, and describes the data + * structures built on them. * * The slots are the edges of the data structure that an update re-points; the * writer records each edge as its slot with the slot's old and new targets. -- 2.47.3

