1084 lines
39 KiB
Plaintext
1084 lines
39 KiB
Plaintext
Linux-Kernel Memory Model Litmus Tests
|
|
======================================
|
|
|
|
This file describes the LKMM litmus-test format by example, describes
|
|
some tricks and traps, and finally outlines LKMM's limitations. Earlier
|
|
versions of this material appeared in a number of LWN articles, including:
|
|
|
|
https://lwn.net/Articles/720550/
|
|
A formal kernel memory-ordering model (part 2)
|
|
https://lwn.net/Articles/608550/
|
|
Axiomatic validation of memory barriers and atomic instructions
|
|
https://lwn.net/Articles/470681/
|
|
Validating Memory Barriers and Atomic Instructions
|
|
|
|
This document presents information in decreasing order of applicability,
|
|
so that, where possible, the information that has proven more commonly
|
|
useful is shown near the beginning.
|
|
|
|
For information on installing LKMM, including the underlying "herd7"
|
|
tool, please see tools/memory-model/README.
|
|
|
|
|
|
Copy-Pasta
|
|
==========
|
|
|
|
As with other software, it is often better (if less macho) to adapt an
|
|
existing litmus test than it is to create one from scratch. A number
|
|
of litmus tests may be found in the kernel source tree:
|
|
|
|
tools/memory-model/litmus-tests/
|
|
Documentation/litmus-tests/
|
|
|
|
Several thousand more example litmus tests are available on github
|
|
and kernel.org:
|
|
|
|
https://github.com/paulmckrcu/litmus
|
|
https://git.kernel.org/pub/scm/linux/kernel/git/paulmck/perfbook.git/tree/CodeSamples/formal/herd
|
|
https://git.kernel.org/pub/scm/linux/kernel/git/paulmck/perfbook.git/tree/CodeSamples/formal/litmus
|
|
|
|
The -l and -L arguments to "git grep" can be quite helpful in identifying
|
|
existing litmus tests that are similar to the one you need. But even if
|
|
you start with an existing litmus test, it is still helpful to have a
|
|
good understanding of the litmus-test format.
|
|
|
|
|
|
Examples and Format
|
|
===================
|
|
|
|
This section describes the overall format of litmus tests, starting
|
|
with a small example of the message-passing pattern and moving on to
|
|
more complex examples that illustrate explicit initialization and LKMM's
|
|
minimalistic set of flow-control statements.
|
|
|
|
|
|
Message-Passing Example
|
|
-----------------------
|
|
|
|
This section gives an overview of the format of a litmus test using an
|
|
example based on the common message-passing use case. This use case
|
|
appears often in the Linux kernel. For example, a flag (modeled by "y"
|
|
below) indicates that a buffer (modeled by "x" below) is now completely
|
|
filled in and ready for use. It would be very bad if the consumer saw the
|
|
flag set, but, due to memory misordering, saw old values in the buffer.
|
|
|
|
This example asks whether smp_store_release() and smp_load_acquire()
|
|
suffices to avoid this bad outcome:
|
|
|
|
1 C MP+pooncerelease+poacquireonce
|
|
2
|
|
3 {}
|
|
4
|
|
5 P0(int *x, int *y)
|
|
6 {
|
|
7 WRITE_ONCE(*x, 1);
|
|
8 smp_store_release(y, 1);
|
|
9 }
|
|
10
|
|
11 P1(int *x, int *y)
|
|
12 {
|
|
13 int r0;
|
|
14 int r1;
|
|
15
|
|
16 r0 = smp_load_acquire(y);
|
|
17 r1 = READ_ONCE(*x);
|
|
18 }
|
|
19
|
|
20 exists (1:r0=1 /\ 1:r1=0)
|
|
|
|
Line 1 starts with "C", which identifies this file as being in the
|
|
LKMM C-language format (which, as we will see, is a small fragment
|
|
of the full C language). The remainder of line 1 is the name of
|
|
the test, which by convention is the filename with the ".litmus"
|
|
suffix stripped. In this case, the actual test may be found in
|
|
tools/memory-model/litmus-tests/MP+pooncerelease+poacquireonce.litmus
|
|
in the Linux-kernel source tree.
|
|
|
|
Mechanically generated litmus tests will often have an optional
|
|
double-quoted comment string on the second line. Such strings are ignored
|
|
when running the test. Yes, you can add your own comments to litmus
|
|
tests, but this is a bit involved due to the use of multiple parsers.
|
|
For now, you can use C-language comments in the C code, and these comments
|
|
may be in either the "/* */" or the "//" style. A later section will
|
|
cover the full litmus-test commenting story.
|
|
|
|
Line 3 is the initialization section. Because the default initialization
|
|
to zero suffices for this test, the "{}" syntax is used, which mean the
|
|
initialization section is empty. Litmus tests requiring non-default
|
|
initialization must have non-empty initialization sections, as in the
|
|
example that will be presented later in this document.
|
|
|
|
Lines 5-9 show the first process and lines 11-18 the second process. Each
|
|
process corresponds to a Linux-kernel task (or kthread, workqueue, thread,
|
|
and so on; LKMM discussions often use these terms interchangeably).
|
|
The name of the first process is "P0" and that of the second "P1".
|
|
You can name your processes anything you like as long as the names consist
|
|
of a single "P" followed by a number, and as long as the numbers are
|
|
consecutive starting with zero. This can actually be quite helpful,
|
|
for example, a .litmus file matching "^P1(" but not matching "^P2("
|
|
must contain a two-process litmus test.
|
|
|
|
The argument list for each function are pointers to the global variables
|
|
used by that function. Unlike normal C-language function parameters, the
|
|
names are significant. The fact that both P0() and P1() have a formal
|
|
parameter named "x" means that these two processes are working with the
|
|
same global variable, also named "x". So the "int *x, int *y" on P0()
|
|
and P1() mean that both processes are working with two shared global
|
|
variables, "x" and "y". Global variables are always passed to processes
|
|
by reference, hence "P0(int *x, int *y)", but *never* "P0(int x, int y)".
|
|
|
|
P0() has no local variables, but P1() has two of them named "r0" and "r1".
|
|
These names may be freely chosen, but for historical reasons stemming from
|
|
other litmus-test formats, it is conventional to use names consisting of
|
|
"r" followed by a number as shown here. A common bug in litmus tests
|
|
is forgetting to add a global variable to a process's parameter list.
|
|
This will sometimes result in an error message, but can also cause the
|
|
intended global to instead be silently treated as an undeclared local
|
|
variable.
|
|
|
|
Each process's code is similar to Linux-kernel C, as can be seen on lines
|
|
7-8 and 13-17. This code may use many of the Linux kernel's atomic
|
|
operations, some of its exclusive-lock functions, and some of its RCU
|
|
and SRCU functions. An approximate list of the currently supported
|
|
functions may be found in the linux-kernel.def file.
|
|
|
|
The P0() process does "WRITE_ONCE(*x, 1)" on line 7. Because "x" is a
|
|
pointer in P0()'s parameter list, this does an unordered store to global
|
|
variable "x". Line 8 does "smp_store_release(y, 1)", and because "y"
|
|
is also in P0()'s parameter list, this does a release store to global
|
|
variable "y".
|
|
|
|
The P1() process declares two local variables on lines 13 and 14.
|
|
Line 16 does "r0 = smp_load_acquire(y)" which does an acquire load
|
|
from global variable "y" into local variable "r0". Line 17 does a
|
|
"r1 = READ_ONCE(*x)", which does an unordered load from "*x" into local
|
|
variable "r1". Both "x" and "y" are in P1()'s parameter list, so both
|
|
reference the same global variables that are used by P0().
|
|
|
|
Line 20 is the "exists" assertion expression to evaluate the final state.
|
|
This final state is evaluated after the dust has settled: both processes
|
|
have completed and all of their memory references and memory barriers
|
|
have propagated to all parts of the system. The references to the local
|
|
variables "r0" and "r1" in line 24 must be prefixed with "1:" to specify
|
|
which process they are local to.
|
|
|
|
Note that the assertion expression is written in the litmus-test
|
|
language rather than in C. For example, single "=" is an equality
|
|
operator rather than an assignment. The "/\" character combination means
|
|
"and". Similarly, "\/" stands for "or". Both of these are ASCII-art
|
|
representations of the corresponding mathematical symbols. Finally,
|
|
"~" stands for "logical not", which is "!" in C, and not to be confused
|
|
with the C-language "~" operator which instead stands for "bitwise not".
|
|
Parentheses may be used to override precedence.
|
|
|
|
The "exists" assertion on line 20 is satisfied if the consumer sees the
|
|
flag ("y") set but the buffer ("x") as not yet filled in, that is, if P1()
|
|
loaded a value from "x" that was equal to 1 but loaded a value from "y"
|
|
that was still equal to zero.
|
|
|
|
This example can be checked by running the following command, which
|
|
absolutely must be run from the tools/memory-model directory and from
|
|
this directory only:
|
|
|
|
herd7 -conf linux-kernel.cfg litmus-tests/MP+pooncerelease+poacquireonce.litmus
|
|
|
|
The output is the result of something similar to a full state-space
|
|
search, and is as follows:
|
|
|
|
1 Test MP+pooncerelease+poacquireonce Allowed
|
|
2 States 3
|
|
3 1:r0=0; 1:r1=0;
|
|
4 1:r0=0; 1:r1=1;
|
|
5 1:r0=1; 1:r1=1;
|
|
6 No
|
|
7 Witnesses
|
|
8 Positive: 0 Negative: 3
|
|
9 Condition exists (1:r0=1 /\ 1:r1=0)
|
|
10 Observation MP+pooncerelease+poacquireonce Never 0 3
|
|
11 Time MP+pooncerelease+poacquireonce 0.00
|
|
12 Hash=579aaa14d8c35a39429b02e698241d09
|
|
|
|
The most pertinent line is line 10, which contains "Never 0 3", which
|
|
indicates that the bad result flagged by the "exists" clause never
|
|
happens. This line might instead say "Sometimes" to indicate that the
|
|
bad result happened in some but not all executions, or it might say
|
|
"Always" to indicate that the bad result happened in all executions.
|
|
(The herd7 tool doesn't judge, so it is only an LKMM convention that the
|
|
"exists" clause indicates a bad result. To see this, invert the "exists"
|
|
clause's condition and run the test.) The numbers ("0 3") at the end
|
|
of this line indicate the number of end states satisfying the "exists"
|
|
clause (0) and the number not not satisfying that clause (3).
|
|
|
|
Another important part of this output is shown in lines 2-5, repeated here:
|
|
|
|
2 States 3
|
|
3 1:r0=0; 1:r1=0;
|
|
4 1:r0=0; 1:r1=1;
|
|
5 1:r0=1; 1:r1=1;
|
|
|
|
Line 2 gives the total number of end states, and each of lines 3-5 list
|
|
one of these states, with the first ("1:r0=0; 1:r1=0;") indicating that
|
|
both of P1()'s loads returned the value "0". As expected, given the
|
|
"Never" on line 10, the state flagged by the "exists" clause is not
|
|
listed. This full list of states can be helpful when debugging a new
|
|
litmus test.
|
|
|
|
The rest of the output is not normally needed, either due to irrelevance
|
|
or due to being redundant with the lines discussed above. However, the
|
|
following paragraph lists them for the benefit of readers possessed of
|
|
an insatiable curiosity. Other readers should feel free to skip ahead.
|
|
|
|
Line 1 echos the test name, along with the "Test" and "Allowed". Line 6's
|
|
"No" says that the "exists" clause was not satisfied by any execution,
|
|
and as such it has the same meaning as line 10's "Never". Line 7 is a
|
|
lead-in to line 8's "Positive: 0 Negative: 3", which lists the number
|
|
of end states satisfying and not satisfying the "exists" clause, just
|
|
like the two numbers at the end of line 10. Line 9 repeats the "exists"
|
|
clause so that you don't have to look it up in the litmus-test file.
|
|
The number at the end of line 11 (which begins with "Time") gives the
|
|
time in seconds required to analyze the litmus test. Small tests such
|
|
as this one complete in a few milliseconds, so "0.00" is quite common.
|
|
Line 12 gives a hash of the contents for the litmus-test file, and is used
|
|
by tooling that manages litmus tests and their output. This tooling is
|
|
used by people modifying LKMM itself, and among other things lets such
|
|
people know which of the several thousand relevant litmus tests were
|
|
affected by a given change to LKMM.
|
|
|
|
|
|
Initialization
|
|
--------------
|
|
|
|
The previous example relied on the default zero initialization for
|
|
"x" and "y", but a similar litmus test could instead initialize them
|
|
to some other value:
|
|
|
|
1 C MP+pooncerelease+poacquireonce
|
|
2
|
|
3 {
|
|
4 x=42;
|
|
5 y=42;
|
|
6 }
|
|
7
|
|
8 P0(int *x, int *y)
|
|
9 {
|
|
10 WRITE_ONCE(*x, 1);
|
|
11 smp_store_release(y, 1);
|
|
12 }
|
|
13
|
|
14 P1(int *x, int *y)
|
|
15 {
|
|
16 int r0;
|
|
17 int r1;
|
|
18
|
|
19 r0 = smp_load_acquire(y);
|
|
20 r1 = READ_ONCE(*x);
|
|
21 }
|
|
22
|
|
23 exists (1:r0=1 /\ 1:r1=42)
|
|
|
|
Lines 3-6 now initialize both "x" and "y" to the value 42. This also
|
|
means that the "exists" clause on line 23 must change "1:r1=0" to
|
|
"1:r1=42".
|
|
|
|
Running the test gives the same overall result as before, but with the
|
|
value 42 appearing in place of the value zero:
|
|
|
|
1 Test MP+pooncerelease+poacquireonce Allowed
|
|
2 States 3
|
|
3 1:r0=1; 1:r1=1;
|
|
4 1:r0=42; 1:r1=1;
|
|
5 1:r0=42; 1:r1=42;
|
|
6 No
|
|
7 Witnesses
|
|
8 Positive: 0 Negative: 3
|
|
9 Condition exists (1:r0=1 /\ 1:r1=42)
|
|
10 Observation MP+pooncerelease+poacquireonce Never 0 3
|
|
11 Time MP+pooncerelease+poacquireonce 0.02
|
|
12 Hash=ab9a9b7940a75a792266be279a980156
|
|
|
|
It is tempting to avoid the open-coded repetitions of the value "42"
|
|
by defining another global variable "initval=42" and replacing all
|
|
occurrences of "42" with "initval". This will not, repeat *not*,
|
|
initialize "x" and "y" to 42, but instead to the address of "initval"
|
|
(try it!). See the section below on linked lists to learn more about
|
|
why this approach to initialization can be useful.
|
|
|
|
|
|
Control Structures
|
|
------------------
|
|
|
|
LKMM supports the C-language "if" statement, which allows modeling of
|
|
conditional branches. In LKMM, conditional branches can affect ordering,
|
|
but only if you are *very* careful (compilers are surprisingly able
|
|
to optimize away conditional branches). The following example shows
|
|
the "load buffering" (LB) use case that is used in the Linux kernel to
|
|
synchronize between ring-buffer producers and consumers. In the example
|
|
below, P0() is one side checking to see if an operation may proceed and
|
|
P1() is the other side completing its update.
|
|
|
|
1 C LB+fencembonceonce+ctrlonceonce
|
|
2
|
|
3 {}
|
|
4
|
|
5 P0(int *x, int *y)
|
|
6 {
|
|
7 int r0;
|
|
8
|
|
9 r0 = READ_ONCE(*x);
|
|
10 if (r0)
|
|
11 WRITE_ONCE(*y, 1);
|
|
12 }
|
|
13
|
|
14 P1(int *x, int *y)
|
|
15 {
|
|
16 int r0;
|
|
17
|
|
18 r0 = READ_ONCE(*y);
|
|
19 smp_mb();
|
|
20 WRITE_ONCE(*x, 1);
|
|
21 }
|
|
22
|
|
23 exists (0:r0=1 /\ 1:r0=1)
|
|
|
|
P1()'s "if" statement on line 10 works as expected, so that line 11 is
|
|
executed only if line 9 loads a non-zero value from "x". Because P1()'s
|
|
write of "1" to "x" happens only after P1()'s read from "y", one would
|
|
hope that the "exists" clause cannot be satisfied. LKMM agrees:
|
|
|
|
1 Test LB+fencembonceonce+ctrlonceonce Allowed
|
|
2 States 2
|
|
3 0:r0=0; 1:r0=0;
|
|
4 0:r0=1; 1:r0=0;
|
|
5 No
|
|
6 Witnesses
|
|
7 Positive: 0 Negative: 2
|
|
8 Condition exists (0:r0=1 /\ 1:r0=1)
|
|
9 Observation LB+fencembonceonce+ctrlonceonce Never 0 2
|
|
10 Time LB+fencembonceonce+ctrlonceonce 0.00
|
|
11 Hash=e5260556f6de495fd39b556d1b831c3b
|
|
|
|
However, there is no "while" statement due to the fact that full
|
|
state-space search has some difficulty with iteration. However, there
|
|
are tricks that may be used to handle some special cases, which are
|
|
discussed below. In addition, loop-unrolling tricks may be applied,
|
|
albeit sparingly.
|
|
|
|
|
|
Tricks and Traps
|
|
================
|
|
|
|
This section covers extracting debug output from herd7, emulating
|
|
spin loops, handling trivial linked lists, adding comments to litmus tests,
|
|
emulating call_rcu(), and finally tricks to improve herd7 performance
|
|
in order to better handle large litmus tests.
|
|
|
|
|
|
Debug Output
|
|
------------
|
|
|
|
By default, the herd7 state output includes all variables mentioned
|
|
in the "exists" clause. But sometimes debugging efforts are greatly
|
|
aided by the values of other variables. Consider this litmus test
|
|
(tools/memory-order/litmus-tests/SB+rfionceonce-poonceonces.litmus but
|
|
slightly modified), which probes an obscure corner of hardware memory
|
|
ordering:
|
|
|
|
1 C SB+rfionceonce-poonceonces
|
|
2
|
|
3 {}
|
|
4
|
|
5 P0(int *x, int *y)
|
|
6 {
|
|
7 int r1;
|
|
8 int r2;
|
|
9
|
|
10 WRITE_ONCE(*x, 1);
|
|
11 r1 = READ_ONCE(*x);
|
|
12 r2 = READ_ONCE(*y);
|
|
13 }
|
|
14
|
|
15 P1(int *x, int *y)
|
|
16 {
|
|
17 int r3;
|
|
18 int r4;
|
|
19
|
|
20 WRITE_ONCE(*y, 1);
|
|
21 r3 = READ_ONCE(*y);
|
|
22 r4 = READ_ONCE(*x);
|
|
23 }
|
|
24
|
|
25 exists (0:r2=0 /\ 1:r4=0)
|
|
|
|
The herd7 output is as follows:
|
|
|
|
1 Test SB+rfionceonce-poonceonces Allowed
|
|
2 States 4
|
|
3 0:r2=0; 1:r4=0;
|
|
4 0:r2=0; 1:r4=1;
|
|
5 0:r2=1; 1:r4=0;
|
|
6 0:r2=1; 1:r4=1;
|
|
7 Ok
|
|
8 Witnesses
|
|
9 Positive: 1 Negative: 3
|
|
10 Condition exists (0:r2=0 /\ 1:r4=0)
|
|
11 Observation SB+rfionceonce-poonceonces Sometimes 1 3
|
|
12 Time SB+rfionceonce-poonceonces 0.01
|
|
13 Hash=c7f30fe0faebb7d565405d55b7318ada
|
|
|
|
(This output indicates that CPUs are permitted to "snoop their own
|
|
store buffers", which all of Linux's CPU families other than s390 will
|
|
happily do. Such snooping results in disagreement among CPUs on the
|
|
order of stores from different CPUs, which is rarely an issue.)
|
|
|
|
But the herd7 output shows only the two variables mentioned in the
|
|
"exists" clause. Someone modifying this test might wish to know the
|
|
values of "x", "y", "0:r1", and "0:r3" as well. The "locations"
|
|
statement on line 25 shows how to cause herd7 to display additional
|
|
variables:
|
|
|
|
1 C SB+rfionceonce-poonceonces
|
|
2
|
|
3 {}
|
|
4
|
|
5 P0(int *x, int *y)
|
|
6 {
|
|
7 int r1;
|
|
8 int r2;
|
|
9
|
|
10 WRITE_ONCE(*x, 1);
|
|
11 r1 = READ_ONCE(*x);
|
|
12 r2 = READ_ONCE(*y);
|
|
13 }
|
|
14
|
|
15 P1(int *x, int *y)
|
|
16 {
|
|
17 int r3;
|
|
18 int r4;
|
|
19
|
|
20 WRITE_ONCE(*y, 1);
|
|
21 r3 = READ_ONCE(*y);
|
|
22 r4 = READ_ONCE(*x);
|
|
23 }
|
|
24
|
|
25 locations [0:r1; 1:r3; x; y]
|
|
26 exists (0:r2=0 /\ 1:r4=0)
|
|
|
|
The herd7 output then displays the values of all the variables:
|
|
|
|
1 Test SB+rfionceonce-poonceonces Allowed
|
|
2 States 4
|
|
3 0:r1=1; 0:r2=0; 1:r3=1; 1:r4=0; x=1; y=1;
|
|
4 0:r1=1; 0:r2=0; 1:r3=1; 1:r4=1; x=1; y=1;
|
|
5 0:r1=1; 0:r2=1; 1:r3=1; 1:r4=0; x=1; y=1;
|
|
6 0:r1=1; 0:r2=1; 1:r3=1; 1:r4=1; x=1; y=1;
|
|
7 Ok
|
|
8 Witnesses
|
|
9 Positive: 1 Negative: 3
|
|
10 Condition exists (0:r2=0 /\ 1:r4=0)
|
|
11 Observation SB+rfionceonce-poonceonces Sometimes 1 3
|
|
12 Time SB+rfionceonce-poonceonces 0.01
|
|
13 Hash=40de8418c4b395388f6501cafd1ed38d
|
|
|
|
What if you would like to know the value of a particular global variable
|
|
at some particular point in a given process's execution? One approach
|
|
is to use a READ_ONCE() to load that global variable into a new local
|
|
variable, then add that local variable to the "locations" clause.
|
|
But be careful: In some litmus tests, adding a READ_ONCE() will change
|
|
the outcome! For one example, please see the C-READ_ONCE.litmus and
|
|
C-READ_ONCE-omitted.litmus tests located here:
|
|
|
|
https://github.com/paulmckrcu/litmus/blob/master/manual/kernel/
|
|
|
|
|
|
Spin Loops
|
|
----------
|
|
|
|
The analysis carried out by herd7 explores full state space, which is
|
|
at best of exponential time complexity. Adding processes and increasing
|
|
the amount of code in a give process can greatly increase execution time.
|
|
Potentially infinite loops, such as those used to wait for locks to
|
|
become available, are clearly problematic.
|
|
|
|
Fortunately, it is possible to avoid state-space explosion by specially
|
|
modeling such loops. For example, the following litmus tests emulates
|
|
locking using xchg_acquire(), but instead of enclosing xchg_acquire()
|
|
in a spin loop, it instead excludes executions that fail to acquire the
|
|
lock using a herd7 "filter" clause. Note that for exclusive locking, you
|
|
are better off using the spin_lock() and spin_unlock() that LKMM directly
|
|
models, if for no other reason that these are much faster. However, the
|
|
techniques illustrated in this section can be used for other purposes,
|
|
such as emulating reader-writer locking, which LKMM does not yet model.
|
|
|
|
1 C C-SB+l-o-o-u+l-o-o-u-X
|
|
2
|
|
3 {
|
|
4 }
|
|
5
|
|
6 P0(int *sl, int *x0, int *x1)
|
|
7 {
|
|
8 int r2;
|
|
9 int r1;
|
|
10
|
|
11 r2 = xchg_acquire(sl, 1);
|
|
12 WRITE_ONCE(*x0, 1);
|
|
13 r1 = READ_ONCE(*x1);
|
|
14 smp_store_release(sl, 0);
|
|
15 }
|
|
16
|
|
17 P1(int *sl, int *x0, int *x1)
|
|
18 {
|
|
19 int r2;
|
|
20 int r1;
|
|
21
|
|
22 r2 = xchg_acquire(sl, 1);
|
|
23 WRITE_ONCE(*x1, 1);
|
|
24 r1 = READ_ONCE(*x0);
|
|
25 smp_store_release(sl, 0);
|
|
26 }
|
|
27
|
|
28 filter (0:r2=0 /\ 1:r2=0)
|
|
29 exists (0:r1=0 /\ 1:r1=0)
|
|
|
|
This litmus test may be found here:
|
|
|
|
https://git.kernel.org/pub/scm/linux/kernel/git/paulmck/perfbook.git/tree/CodeSamples/formal/herd/C-SB+l-o-o-u+l-o-o-u-X.litmus
|
|
|
|
This test uses two global variables, "x1" and "x2", and also emulates a
|
|
single global spinlock named "sl". This spinlock is held by whichever
|
|
process changes the value of "sl" from "0" to "1", and is released when
|
|
that process sets "sl" back to "0". P0()'s lock acquisition is emulated
|
|
on line 11 using xchg_acquire(), which unconditionally stores the value
|
|
"1" to "sl" and stores either "0" or "1" to "r2", depending on whether
|
|
the lock acquisition was successful or unsuccessful (due to "sl" already
|
|
having the value "1"), respectively. P1() operates in a similar manner.
|
|
|
|
Rather unconventionally, execution appears to proceed to the critical
|
|
section on lines 12 and 13 in either case. Line 14 then uses an
|
|
smp_store_release() to store zero to "sl", thus emulating lock release.
|
|
|
|
The case where xchg_acquire() fails to acquire the lock is handled by
|
|
the "filter" clause on line 28, which tells herd7 to keep only those
|
|
executions in which both "0:r2" and "1:r2" are zero, that is to pay
|
|
attention only to those executions in which both locks are actually
|
|
acquired. Thus, the bogus executions that would execute the critical
|
|
sections are discarded and any effects that they might have had are
|
|
ignored. Note well that the "filter" clause keeps those executions
|
|
for which its expression is satisfied, that is, for which the expression
|
|
evaluates to true. In other words, the "filter" clause says what to
|
|
keep, not what to discard.
|
|
|
|
The result of running this test is as follows:
|
|
|
|
1 Test C-SB+l-o-o-u+l-o-o-u-X Allowed
|
|
2 States 2
|
|
3 0:r1=0; 1:r1=1;
|
|
4 0:r1=1; 1:r1=0;
|
|
5 No
|
|
6 Witnesses
|
|
7 Positive: 0 Negative: 2
|
|
8 Condition exists (0:r1=0 /\ 1:r1=0)
|
|
9 Observation C-SB+l-o-o-u+l-o-o-u-X Never 0 2
|
|
10 Time C-SB+l-o-o-u+l-o-o-u-X 0.03
|
|
|
|
The "Never" on line 9 indicates that this use of xchg_acquire() and
|
|
smp_store_release() really does correctly emulate locking.
|
|
|
|
Why doesn't the litmus test take the simpler approach of using a spin loop
|
|
to handle failed spinlock acquisitions, like the kernel does? The key
|
|
insight behind this litmus test is that spin loops have no effect on the
|
|
possible "exists"-clause outcomes of program execution in the absence
|
|
of deadlock. In other words, given a high-quality lock-acquisition
|
|
primitive in a deadlock-free program running on high-quality hardware,
|
|
each lock acquisition will eventually succeed. Because herd7 already
|
|
explores the full state space, the length of time required to actually
|
|
acquire the lock does not matter. After all, herd7 already models all
|
|
possible durations of the xchg_acquire() statements.
|
|
|
|
Why not just add the "filter" clause to the "exists" clause, thus
|
|
avoiding the "filter" clause entirely? This does work, but is slower.
|
|
The reason that the "filter" clause is faster is that (in the common case)
|
|
herd7 knows to abandon an execution as soon as the "filter" expression
|
|
fails to be satisfied. In contrast, the "exists" clause is evaluated
|
|
only at the end of time, thus requiring herd7 to waste time on bogus
|
|
executions in which both critical sections proceed concurrently. In
|
|
addition, some LKMM users like the separation of concerns provided by
|
|
using the both the "filter" and "exists" clauses.
|
|
|
|
Readers lacking a pathological interest in odd corner cases should feel
|
|
free to skip the remainder of this section.
|
|
|
|
But what if the litmus test were to temporarily set "0:r2" to a non-zero
|
|
value? Wouldn't that cause herd7 to abandon the execution prematurely
|
|
due to an early mismatch of the "filter" clause?
|
|
|
|
Why not just try it? Line 4 of the following modified litmus test
|
|
introduces a new global variable "x2" that is initialized to "1". Line 23
|
|
of P1() reads that variable into "1:r2" to force an early mismatch with
|
|
the "filter" clause. Line 24 does a known-true "if" condition to avoid
|
|
and static analysis that herd7 might do. Finally the "exists" clause
|
|
on line 32 is updated to a condition that is alway satisfied at the end
|
|
of the test.
|
|
|
|
1 C C-SB+l-o-o-u+l-o-o-u-X
|
|
2
|
|
3 {
|
|
4 x2=1;
|
|
5 }
|
|
6
|
|
7 P0(int *sl, int *x0, int *x1)
|
|
8 {
|
|
9 int r2;
|
|
10 int r1;
|
|
11
|
|
12 r2 = xchg_acquire(sl, 1);
|
|
13 WRITE_ONCE(*x0, 1);
|
|
14 r1 = READ_ONCE(*x1);
|
|
15 smp_store_release(sl, 0);
|
|
16 }
|
|
17
|
|
18 P1(int *sl, int *x0, int *x1, int *x2)
|
|
19 {
|
|
20 int r2;
|
|
21 int r1;
|
|
22
|
|
23 r2 = READ_ONCE(*x2);
|
|
24 if (r2)
|
|
25 r2 = xchg_acquire(sl, 1);
|
|
26 WRITE_ONCE(*x1, 1);
|
|
27 r1 = READ_ONCE(*x0);
|
|
28 smp_store_release(sl, 0);
|
|
29 }
|
|
30
|
|
31 filter (0:r2=0 /\ 1:r2=0)
|
|
32 exists (x1=1)
|
|
|
|
If the "filter" clause were to check each variable at each point in the
|
|
execution, running this litmus test would display no executions because
|
|
all executions would be filtered out at line 23. However, the output
|
|
is instead as follows:
|
|
|
|
1 Test C-SB+l-o-o-u+l-o-o-u-X Allowed
|
|
2 States 1
|
|
3 x1=1;
|
|
4 Ok
|
|
5 Witnesses
|
|
6 Positive: 2 Negative: 0
|
|
7 Condition exists (x1=1)
|
|
8 Observation C-SB+l-o-o-u+l-o-o-u-X Always 2 0
|
|
9 Time C-SB+l-o-o-u+l-o-o-u-X 0.04
|
|
10 Hash=080bc508da7f291e122c6de76c0088e3
|
|
|
|
Line 3 shows that there is one execution that did not get filtered out,
|
|
so the "filter" clause is evaluated only on the last assignment to
|
|
the variables that it checks. In this case, the "filter" clause is a
|
|
disjunction, so it might be evaluated twice, once at the final (and only)
|
|
assignment to "0:r2" and once at the final assignment to "1:r2".
|
|
|
|
|
|
Linked Lists
|
|
------------
|
|
|
|
LKMM can handle linked lists, but only linked lists in which each node
|
|
contains nothing except a pointer to the next node in the list. This is
|
|
of course quite restrictive, but there is nevertheless quite a bit that
|
|
can be done within these confines, as can be seen in the litmus test
|
|
at tools/memory-model/litmus-tests/MP+onceassign+derefonce.litmus:
|
|
|
|
1 C MP+onceassign+derefonce
|
|
2
|
|
3 {
|
|
4 y=z;
|
|
5 z=0;
|
|
6 }
|
|
7
|
|
8 P0(int *x, int **y)
|
|
9 {
|
|
10 WRITE_ONCE(*x, 1);
|
|
11 rcu_assign_pointer(*y, x);
|
|
12 }
|
|
13
|
|
14 P1(int *x, int **y)
|
|
15 {
|
|
16 int *r0;
|
|
17 int r1;
|
|
18
|
|
19 rcu_read_lock();
|
|
20 r0 = rcu_dereference(*y);
|
|
21 r1 = READ_ONCE(*r0);
|
|
22 rcu_read_unlock();
|
|
23 }
|
|
24
|
|
25 exists (1:r0=x /\ 1:r1=0)
|
|
|
|
Line 4's "y=z" may seem odd, given that "z" has not yet been initialized.
|
|
But "y=z" does not set the value of "y" to that of "z", but instead
|
|
sets the value of "y" to the *address* of "z". Lines 4 and 5 therefore
|
|
create a simple linked list, with "y" pointing to "z" and "z" having a
|
|
NULL pointer. A much longer linked list could be created if desired,
|
|
and circular singly linked lists can also be created and manipulated.
|
|
|
|
The "exists" clause works the same way, with the "1:r0=x" comparing P1()'s
|
|
"r0" not to the value of "x", but again to its address. This term of the
|
|
"exists" clause therefore tests whether line 20's load from "y" saw the
|
|
value stored by line 11, which is in fact what is required in this case.
|
|
|
|
P0()'s line 10 initializes "x" to the value 1 then line 11 links to "x"
|
|
from "y", replacing "z".
|
|
|
|
P1()'s line 20 loads a pointer from "y", and line 21 dereferences that
|
|
pointer. The RCU read-side critical section spanning lines 19-22 is just
|
|
for show in this example. Note that the address used for line 21's load
|
|
depends on (in this case, "is exactly the same as") the value loaded by
|
|
line 20. This is an example of what is called an "address dependency".
|
|
This particular address dependency extends from the load on line 20 to the
|
|
load on line 21. Address dependencies provide a weak form of ordering.
|
|
|
|
Running this test results in the following:
|
|
|
|
1 Test MP+onceassign+derefonce Allowed
|
|
2 States 2
|
|
3 1:r0=x; 1:r1=1;
|
|
4 1:r0=z; 1:r1=0;
|
|
5 No
|
|
6 Witnesses
|
|
7 Positive: 0 Negative: 2
|
|
8 Condition exists (1:r0=x /\ 1:r1=0)
|
|
9 Observation MP+onceassign+derefonce Never 0 2
|
|
10 Time MP+onceassign+derefonce 0.00
|
|
11 Hash=49ef7a741563570102448a256a0c8568
|
|
|
|
The only possible outcomes feature P1() loading a pointer to "z"
|
|
(which contains zero) on the one hand and P1() loading a pointer to "x"
|
|
(which contains the value one) on the other. This should be reassuring
|
|
because it says that RCU readers cannot see the old preinitialization
|
|
values when accessing a newly inserted list node. This undesirable
|
|
scenario is flagged by the "exists" clause, and would occur if P1()
|
|
loaded a pointer to "x", but obtained the pre-initialization value of
|
|
zero after dereferencing that pointer.
|
|
|
|
|
|
Comments
|
|
--------
|
|
|
|
Different portions of a litmus test are processed by different parsers,
|
|
which has the charming effect of requiring different comment syntax in
|
|
different portions of the litmus test. The C-syntax portions use
|
|
C-language comments (either "/* */" or "//"), while the other portions
|
|
use Ocaml comments "(* *)".
|
|
|
|
The following litmus test illustrates the comment style corresponding
|
|
to each syntactic unit of the test:
|
|
|
|
1 C MP+onceassign+derefonce (* A *)
|
|
2
|
|
3 (* B *)
|
|
4
|
|
5 {
|
|
6 y=z; (* C *)
|
|
7 z=0;
|
|
8 } // D
|
|
9
|
|
10 // E
|
|
11
|
|
12 P0(int *x, int **y) // F
|
|
13 {
|
|
14 WRITE_ONCE(*x, 1); // G
|
|
15 rcu_assign_pointer(*y, x);
|
|
16 }
|
|
17
|
|
18 // H
|
|
19
|
|
20 P1(int *x, int **y)
|
|
21 {
|
|
22 int *r0;
|
|
23 int r1;
|
|
24
|
|
25 rcu_read_lock();
|
|
26 r0 = rcu_dereference(*y);
|
|
27 r1 = READ_ONCE(*r0);
|
|
28 rcu_read_unlock();
|
|
29 }
|
|
30
|
|
31 // I
|
|
32
|
|
33 exists (* J *) (1:r0=x /\ (* K *) 1:r1=0) (* L *)
|
|
|
|
In short, use C-language comments in the C code and Ocaml comments in
|
|
the rest of the litmus test.
|
|
|
|
On the other hand, if you prefer C-style comments everywhere, the
|
|
C preprocessor is your friend.
|
|
|
|
|
|
Asynchronous RCU Grace Periods
|
|
------------------------------
|
|
|
|
The following litmus test is derived from the example show in
|
|
Documentation/litmus-tests/rcu/RCU+sync+free.litmus, but converted to
|
|
emulate call_rcu():
|
|
|
|
1 C RCU+sync+free
|
|
2
|
|
3 {
|
|
4 int x = 1;
|
|
5 int *y = &x;
|
|
6 int z = 1;
|
|
7 }
|
|
8
|
|
9 P0(int *x, int *z, int **y)
|
|
10 {
|
|
11 int *r0;
|
|
12 int r1;
|
|
13
|
|
14 rcu_read_lock();
|
|
15 r0 = rcu_dereference(*y);
|
|
16 r1 = READ_ONCE(*r0);
|
|
17 rcu_read_unlock();
|
|
18 }
|
|
19
|
|
20 P1(int *z, int **y, int *c)
|
|
21 {
|
|
22 rcu_assign_pointer(*y, z);
|
|
23 smp_store_release(*c, 1); // Emulate call_rcu().
|
|
24 }
|
|
25
|
|
26 P2(int *x, int *z, int **y, int *c)
|
|
27 {
|
|
28 int r0;
|
|
29
|
|
30 r0 = smp_load_acquire(*c); // Note call_rcu() request.
|
|
31 synchronize_rcu(); // Wait one grace period.
|
|
32 WRITE_ONCE(*x, 0); // Emulate the RCU callback.
|
|
33 }
|
|
34
|
|
35 filter (2:r0=1) (* Reject too-early starts. *)
|
|
36 exists (0:r0=x /\ 0:r1=0)
|
|
|
|
Lines 4-6 initialize a linked list headed by "y" that initially contains
|
|
"x". In addition, "z" is pre-initialized to prepare for P1(), which
|
|
will replace "x" with "z" in this list.
|
|
|
|
P0() on lines 9-18 enters an RCU read-side critical section, loads the
|
|
list header "y" and dereferences it, leaving the node in "0:r0" and
|
|
the node's value in "0:r1".
|
|
|
|
P1() on lines 20-24 updates the list header to instead reference "z",
|
|
then emulates call_rcu() by doing a release store into "c".
|
|
|
|
P2() on lines 27-33 emulates the behind-the-scenes effect of doing a
|
|
call_rcu(). Line 30 first does an acquire load from "c", then line 31
|
|
waits for an RCU grace period to elapse, and finally line 32 emulates
|
|
the RCU callback, which in turn emulates a call to kfree().
|
|
|
|
Of course, it is possible for P2() to start too soon, so that the
|
|
value of "2:r0" is zero rather than the required value of "1".
|
|
The "filter" clause on line 35 handles this possibility, rejecting
|
|
all executions in which "2:r0" is not equal to the value "1".
|
|
|
|
|
|
Performance
|
|
-----------
|
|
|
|
LKMM's exploration of the full state-space can be extremely helpful,
|
|
but it does not come for free. The price is exponential computational
|
|
complexity in terms of the number of processes, the average number
|
|
of statements in each process, and the total number of stores in the
|
|
litmus test.
|
|
|
|
So it is best to start small and then work up. Where possible, break
|
|
your code down into small pieces each representing a core concurrency
|
|
requirement.
|
|
|
|
That said, herd7 is quite fast. On an unprepossessing x86 laptop, it
|
|
was able to analyze the following 10-process RCU litmus test in about
|
|
six seconds.
|
|
|
|
https://github.com/paulmckrcu/litmus/blob/master/auto/C-RW-R+RW-R+RW-G+RW-G+RW-G+RW-G+RW-R+RW-R+RW-R+RW-R.litmus
|
|
|
|
One way to make herd7 run faster is to use the "-speedcheck true" option.
|
|
This option prevents herd7 from generating all possible end states,
|
|
instead causing it to focus solely on whether or not the "exists"
|
|
clause can be satisfied. With this option, herd7 evaluates the above
|
|
litmus test in about 300 milliseconds, for more than an order of magnitude
|
|
improvement in performance.
|
|
|
|
Larger 16-process litmus tests that would normally consume 15 minutes
|
|
of time complete in about 40 seconds with this option. To be fair,
|
|
you do get an extra 65,535 states when you leave off the "-speedcheck
|
|
true" option.
|
|
|
|
https://github.com/paulmckrcu/litmus/blob/master/auto/C-RW-R+RW-R+RW-G+RW-G+RW-G+RW-G+RW-R+RW-R+RW-R+RW-R+RW-G+RW-G+RW-G+RW-G+RW-R+RW-R.litmus
|
|
|
|
Nevertheless, litmus-test analysis really is of exponential complexity,
|
|
whether with or without "-speedcheck true". Increasing by just three
|
|
processes to a 19-process litmus test requires 2 hours and 40 minutes
|
|
without, and about 8 minutes with "-speedcheck true". Each of these
|
|
results represent roughly an order of magnitude slowdown compared to the
|
|
16-process litmus test. Again, to be fair, the multi-hour run explores
|
|
no fewer than 524,287 additional states compared to the shorter one.
|
|
|
|
https://github.com/paulmckrcu/litmus/blob/master/auto/C-RW-R+RW-R+RW-G+RW-G+RW-G+RW-G+RW-R+RW-R+RW-R+RW-R+RW-R+RW-R+RW-G+RW-G+RW-G+RW-G+RW-R+RW-R+RW-R.litmus
|
|
|
|
If you don't like command-line arguments, you can obtain a similar speedup
|
|
by adding a "filter" clause with exactly the same expression as your
|
|
"exists" clause.
|
|
|
|
However, please note that seeing the full set of states can be extremely
|
|
helpful when developing and debugging litmus tests.
|
|
|
|
|
|
LIMITATIONS
|
|
===========
|
|
|
|
Limitations of the Linux-kernel memory model (LKMM) include:
|
|
|
|
1. Compiler optimizations are not accurately modeled. Of course,
|
|
the use of READ_ONCE() and WRITE_ONCE() limits the compiler's
|
|
ability to optimize, but under some circumstances it is possible
|
|
for the compiler to undermine the memory model. For more
|
|
information, see Documentation/explanation.txt (in particular,
|
|
the "THE PROGRAM ORDER RELATION: po AND po-loc" and "A WARNING"
|
|
sections).
|
|
|
|
Note that this limitation in turn limits LKMM's ability to
|
|
accurately model address, control, and data dependencies.
|
|
For example, if the compiler can deduce the value of some variable
|
|
carrying a dependency, then the compiler can break that dependency
|
|
by substituting a constant of that value.
|
|
|
|
Conversely, LKMM will sometimes overestimate the amount of
|
|
reordering compilers and CPUs can carry out, leading it to miss
|
|
some pretty obvious cases of ordering. A simple example is:
|
|
|
|
r1 = READ_ONCE(x);
|
|
if (r1 == 0)
|
|
smp_mb();
|
|
WRITE_ONCE(y, 1);
|
|
|
|
The WRITE_ONCE() does not depend on the READ_ONCE(), and as a
|
|
result, LKMM does not claim ordering. However, even though no
|
|
dependency is present, the WRITE_ONCE() will not be executed before
|
|
the READ_ONCE(). There are two reasons for this:
|
|
|
|
The presence of the smp_mb() in one of the branches
|
|
prevents the compiler from moving the WRITE_ONCE()
|
|
up before the "if" statement, since the compiler has
|
|
to assume that r1 will sometimes be 0 (but see the
|
|
comment below);
|
|
|
|
CPUs do not execute stores before po-earlier conditional
|
|
branches, even in cases where the store occurs after the
|
|
two arms of the branch have recombined.
|
|
|
|
It is clear that it is not dangerous in the slightest for LKMM to
|
|
make weaker guarantees than architectures. In fact, it is
|
|
desirable, as it gives compilers room for making optimizations.
|
|
For instance, suppose that a 0 value in r1 would trigger undefined
|
|
behavior elsewhere. Then a clever compiler might deduce that r1
|
|
can never be 0 in the if condition. As a result, said clever
|
|
compiler might deem it safe to optimize away the smp_mb(),
|
|
eliminating the branch and any ordering an architecture would
|
|
guarantee otherwise.
|
|
|
|
2. Multiple access sizes for a single variable are not supported,
|
|
and neither are misaligned or partially overlapping accesses.
|
|
|
|
3. Exceptions and interrupts are not modeled. In some cases,
|
|
this limitation can be overcome by modeling the interrupt or
|
|
exception with an additional process.
|
|
|
|
4. I/O such as MMIO or DMA is not supported.
|
|
|
|
5. Self-modifying code (such as that found in the kernel's
|
|
alternatives mechanism, function tracer, Berkeley Packet Filter
|
|
JIT compiler, and module loader) is not supported.
|
|
|
|
6. Complete modeling of all variants of atomic read-modify-write
|
|
operations, locking primitives, and RCU is not provided.
|
|
For example, call_rcu() and rcu_barrier() are not supported.
|
|
However, a substantial amount of support is provided for these
|
|
operations, as shown in the linux-kernel.def file.
|
|
|
|
Here are specific limitations:
|
|
|
|
a. When rcu_assign_pointer() is passed NULL, the Linux
|
|
kernel provides no ordering, but LKMM models this
|
|
case as a store release.
|
|
|
|
b. The "unless" RMW operations are not currently modeled:
|
|
atomic_long_add_unless(), atomic_inc_unless_negative(),
|
|
and atomic_dec_unless_positive(). These can be emulated
|
|
in litmus tests, for example, by using atomic_cmpxchg().
|
|
|
|
One exception of this limitation is atomic_add_unless(),
|
|
which is provided directly by herd7 (so no corresponding
|
|
definition in linux-kernel.def). atomic_add_unless() is
|
|
modeled by herd7 therefore it can be used in litmus tests.
|
|
|
|
c. The call_rcu() function is not modeled. As was shown above,
|
|
it can be emulated in litmus tests by adding another
|
|
process that invokes synchronize_rcu() and the body of the
|
|
callback function, with (for example) a release-acquire
|
|
from the site of the emulated call_rcu() to the beginning
|
|
of the additional process.
|
|
|
|
d. The rcu_barrier() function is not modeled. It can be
|
|
emulated in litmus tests emulating call_rcu() via
|
|
(for example) a release-acquire from the end of each
|
|
additional call_rcu() process to the site of the
|
|
emulated rcu-barrier().
|
|
|
|
e. Reader-writer locking is not modeled. It can be
|
|
emulated in litmus tests using atomic read-modify-write
|
|
operations.
|
|
|
|
The fragment of the C language supported by these litmus tests is quite
|
|
limited and in some ways non-standard:
|
|
|
|
1. There is no automatic C-preprocessor pass. You can of course
|
|
run it manually, if you choose.
|
|
|
|
2. There is no way to create functions other than the Pn() functions
|
|
that model the concurrent processes.
|
|
|
|
3. The Pn() functions' formal parameters must be pointers to the
|
|
global shared variables. Nothing can be passed by value into
|
|
these functions.
|
|
|
|
4. The only functions that can be invoked are those built directly
|
|
into herd7 or that are defined in the linux-kernel.def file.
|
|
|
|
5. The "switch", "do", "for", "while", and "goto" C statements are
|
|
not supported. The "switch" statement can be emulated by the
|
|
"if" statement. The "do", "for", and "while" statements can
|
|
often be emulated by manually unrolling the loop, or perhaps by
|
|
enlisting the aid of the C preprocessor to minimize the resulting
|
|
code duplication. Some uses of "goto" can be emulated by "if",
|
|
and some others by unrolling.
|
|
|
|
6. Although you can use a wide variety of types in litmus-test
|
|
variable declarations, and especially in global-variable
|
|
declarations, the "herd7" tool understands only int and
|
|
pointer types. There is no support for floating-point types,
|
|
enumerations, characters, strings, arrays, or structures.
|
|
|
|
7. Parsing of variable declarations is very loose, with almost no
|
|
type checking.
|
|
|
|
8. Initializers differ from their C-language counterparts.
|
|
For example, when an initializer contains the name of a shared
|
|
variable, that name denotes a pointer to that variable, not
|
|
the current value of that variable. For example, "int x = y"
|
|
is interpreted the way "int x = &y" would be in C.
|
|
|
|
9. Dynamic memory allocation is not supported, although this can
|
|
be worked around in some cases by supplying multiple statically
|
|
allocated variables.
|
|
|
|
Some of these limitations may be overcome in the future, but others are
|
|
more likely to be addressed by incorporating the Linux-kernel memory model
|
|
into other tools.
|
|
|
|
Finally, please note that LKMM is subject to change as hardware, use cases,
|
|
and compilers evolve.
|