Rust concurrency makes heavy use of ownership and borrowing. The purpose of this post is not to give an exposition of Rust's capabilities and limitations in this area, but rather to give a series of examples of ownership in the Linux kernel.
The first example involves Linux-kernel per-CPU variables. In some cases, such variables are protected by per-CPU locks, for example, a number of fields in the per-CPU
rcu_datastructure are used by the kernel threads that manage grace periods for offloaded callbacks, and these fields are protected by the
->nocb_gp_lockfield in the same instance of that same structure. In other cases, access to a given per-CPU variable is permitted only by the corresponding CPU, and even then only if that CPU has disabled preemption. For example, the per-CPU
rcu_datastructure's
->ticks_this_gpfield may be updated only from the corresponding CPU, and only when preemption is disabled. In the particular case, preemption is disabled as a side-effect of having disabled interrupts.
The second example builds on the first. In kernels built with
CONFIG_RCU_NOCB_CPU=n, the per-CPU
rcu_datastructure's
->cblistfield may be updated from the corresponding CPU, and only when preemption is disabled. However, it is also allowed from some other CPU when the corresponding CPU has been taken offline, but only when that other CPU that is orchestrating the offlining of the corresponding CPU.
(What about kernels built with
CONFIG_RCU_NOCB_CPU=y? They must also acquire a
->nocb_lockthat is also contained within the per-CPU
rcu_datastructure.)
The third example moves to the non-per-CPU
rcu_nodestructures, which are arranged into a combining tree that processes events from an arbitrarily large number of CPUs while maintaining bounded lock contention. Each
rcu_node>structure has a
->qsmaskbitmask that tracks which of its children need to report a quiescent state for the current grace period, and a
->gp_taskspointer that, when non-
NULL, references a list of tasks that blocked while in an RCU read-side critical section that blocks the current grace period. The children of each leaf
rcu_nodestructure are the
rcu_datastructures feeding into that
rcu_nodestructure. Finally, there is a singleton
rcu_statestructure that contains a
->gp_seqfield that numbers the current grace period and also indicates whether or not it is in progress.
We now have enough information to state the ownership rule for the
rcu_statestructure's
->gp_seqfield. This field may be updated only if all of the following hold:
- The current CPU holds the root rcu_node structure's ->lock .
- All rcu_node structures' ->qsmask fields are zero.
- All rcu_node structures' ->gp_tasks pointers are NULL .
This might seem excessively ornate, but it is the natural consequence of RCU's semantics and design:
- The next grace period is not permitted to start until after the previous one has ended. This is a design choice that allows RCU to function well in the face of update-side overload from large numbers of CPUs.
- A grace period is not allowed to end until all CPUs have been observed in a quiescent state, that is, until all rcu_node structures' ->qsmask fields have become zero.
- A grace period is not allowed to end until all tasks that were preempted while executing within a critical section blocking that grace period have resumed and exited their critical sections, that is, until all rcu_node structures' ->gp_tasks pointers have become NULL .
- If multiple CPUs would like to start a grace period at the same time, there has to be something that works out which CPU is actually going to do the starting, and the last part of that something is the root rcu_node structure's ->lock .
Trust me, there are far more complex ownership models in the Linux kernel!
The fourth example involves per-CPU data that is protected by a reader-writer lock. A CPU is permitted to both read and update its own data only if it read-holds the lock. A CPU is permitted to read other CPUs' data only if it write-holds the lock. That is right, CPUs are allowed to write when read-holding the lock and and are allowed to read while write-holding the lock. Perhaps reader-writer locks should instead have been called exclusive-shared locks, but it is some decades too late now!
The fifth and final example involves multiple locks, for example, when some readers must traverse the data structure from an interrupt handler and others must sleep while traversing that same structure. One way to implement this is to have one interrupt-disabled spinlock (for readers in interrupt handlers) and a mutex (for readers that must sleep during the traversal). Updaters must then hold both locks.
So how on earth does the current C-language Linux kernel code keep all this straight???
One indispensable tool is the assertion, which in the Linux kernel includes
WARN(),
BUG(), and friends. For example, RCU uses these to verify the values of the aforementioned
->qsmaskand
->gp_tasksfields during between-grace-periods traversals of the
rcu_nodecombining tree.
Another indispensable tool is lockdep, which, in addition to checking for deadlocks, allows code to verify that conditions are right. A few of the many available lockdep assertions are shown below:
- lockdep_assert_held() : Complains if the specified lock is not held by the current CPU/task.
- lockdep_assert_held_write() : Complains if the specified reader-writer lock is not write-held by the current CPU/task.
- lockdep_assert_held_read(): : Complains if the specified reader-writer lock is not read-held by the current CPU/task.
- lockdep_assert_none_held_once() : Complains if the current CPU/task holds any locks.
- lockdep_assert_irqs_disabled() : Complains if the current CPU has interrupts enabled.
- rcu_read_lock_held() : Complains if the current CPU/task is not within an RCU read-side critical section.
Of course, none of these assertions are going to do anything for you unless you make use of them. On the other hand, some of the more complex ownership criteria are going to be quite difficult for compilers or other tooling to intuit without significant help from the developer.
A more entertaining ownership scenario is described in Section 5.4.6 ("Applying Exact Limit Counters") of perfbook. Chapter 8 ("Data Ownership") describes several other ownership scenarios.
History
October 12, 2021: Self-review.
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.