Cache Coherence and the Store Buffer

Memory barriers feel like a software invention, but they exist to paper over a specific hardware reality: a CPU’s caches are kept coherent with every other CPU’s caches by a protocol like MESI, yet coherence does not imply ordering. Between the CPU core and its coherent cache sit two performance buffers — the store buffer and the invalidate queue — that let a core’s later operations bypass its own not-yet-globally-visible memory operations. The result is that a second CPU can observe one CPU’s writes in an order different from program order, even though every cache line is perfectly coherent. As Paul McKenney’s Memory Barriers: a Hardware View for Software Hackers explains, these buffers are exactly what smp_wmb() and smp_rmb() act upon, and the whole zoo of barriers in Memory Barriers in the Linux Kernel is the software’s only handle on this hidden hardware reordering (McKenney, revision 2010.07.23a). This note explains the why behind the barriers: what coherence does and does not guarantee, how the two buffers produce apparent reordering, and why x86 needs almost no SMP fences while ARM, POWER and RISC-V need real ones.

This note pins its kernel-side API references to Linux 6.12 LTS (released 2024-11-17; mainline had moved into the 7.x series by the time of writing, 2026-09-04, so treat 6.12 as a maintained long-term-support branch rather than as current mainline). Hardware facts (MESI, x86-TSO, the Arm memory model) are architecture properties and are dated individually where they are version-sensitive.


Scope: This Note Is the Hardware Half of a Pair

Memory ordering in the Linux kernel is best learned as two layers, and this vault splits them into two notes deliberately. Confusing the layers is the single most common source of muddle in this area, so it is worth stating the division explicitly before anything else.

flowchart TB
  subgraph SW["The contract — [[Acquire Release and Fence Semantics]]"]
    direction TB
    A1["What smp_store_release and smp_load_acquire<br/>promise, and what they do NOT promise"]
    A2["Full barriers, one-way barriers,<br/>dependencies, READ_ONCE and WRITE_ONCE"]
    A3["Which primitive do I reach for<br/>for this pattern?"]
  end
  subgraph HW["The mechanism — THIS NOTE"]
    direction TB
    B1["Why any reordering exists at all:<br/>store buffer, invalidate queue"]
    B2["What coherence gives you — MESI, MOESI, MESIF —<br/>and what it does NOT give you"]
    B3["Why x86 hides bugs that ARM exposes"]
  end
  subgraph FM["The formalisation — [[The Linux Kernel Memory Model]]"]
    C1["herd7, cat files, litmus tests,<br/>ppo / hb / prop / cumul-fence"]
  end
  HW -->|"motivates"| SW
  SW -->|"is made precise by"| FM
  FM -->|"is validated against"| HW
  HW -.->|"used directly by"| RCU["[[Read-Copy-Update Fundamentals]]<br/>rests on both layers"]
  SW -.->|"used directly by"| RCU

The three-note division of memory ordering, and where this note sits. What it shows: this note answers “why does reordering happen at all”, its sibling Acquire Release and Fence Semantics answers “what am I guaranteed if I write smp_store_release()”, and The Linux Kernel Memory Model answers “how is that guarantee stated precisely enough for a tool to check”. The insight to take: the hardware layer explains the cost of ordering and the sibling explains the contract; you need the contract to write correct code, and you need this note to understand why the contract is shaped the way it is — and why a missing barrier can be invisible for years on one machine and fatal on another. Read-Copy-Update Fundamentals rests directly on both layers and is a third, separate note: it does not duplicate either.

Concretely: if your question is “does smp_load_acquire() order this store against that load?”, read the sibling. If your question is “why is smp_wmb() free on x86 and a real instruction on ARM?”, or “my code passed a million iterations on my laptop and corrupted a list on an Ampere server”, you are in the right note.


Coherence Is Not Ordering

The first thing to untangle is two words that sound synonymous but are not. Nearly every confused conversation about memory barriers collapses onto this one distinction.

Cache coherence is a per-location guarantee. For any single memory location, all CPUs agree on a single sequence of values that location takes, and a read returns the most recent write to that location in that agreed sequence. Coherence is what stops two CPUs from permanently disagreeing about the value of one variable. A coherence protocol — MESI and its richer cousins — enforces this by making cores exchange ownership of cache lines before writing them. McKenney’s perfbook states it in one sentence: “On cache-coherent platforms, all CPUs agree on the order of loads and stores to a given variable” (Is Parallel Programming Hard, v2026.06.21a, §15.3.6).

Memory ordering (memory consistency) is a cross-location guarantee: it governs the order in which writes to different locations become visible to other CPUs. Coherence says nothing whatsoever about it. You can have a perfectly coherent machine where CPU 0 writes a and then b, and CPU 1 sees b updated while a is still stale — each location individually coherent, but the relative order scrambled.

This property has been named so many times that the naming itself causes confusion. Perfbook enumerates the synonyms: “single-variable SC” (sequential consistency restricted to one variable), “single-copy atomic”, and plain “coherence”; it settles on using “cache coherence” and “coherence” interchangeably rather than minting yet another term (§15.3.6). In the Linux-kernel memory model this same property appears as the SV column — “orders later accesses to the same variable” — of the ordering cheat sheet, and it is supplied by every row of that table including the completely unordered ones (tools/memory-model/Documentation/cheatsheet.txt, v6.12). That is the formal statement of “coherence is free; ordering is not”.

flowchart LR
  subgraph COH["COHERENCE — per location, always on"]
    direction TB
    CH1["Every CPU agrees on the<br/>value sequence of x"]
    CH2["Every CPU agrees on the<br/>value sequence of y"]
    CH3["Enforced by MESI / MOESI<br/>No barrier needed<br/>You cannot switch it off"]
    CH1 --- CH3
    CH2 --- CH3
  end
  subgraph ORD["ORDERING — across locations, off by default"]
    direction TB
    OR1["Does CPU 1 see my store to x<br/>before my store to y?"]
    OR2["Relaxed deliberately, for speed"]
    OR3["Restored only by explicit barriers<br/>smp_wmb, smp_rmb, smp_mb,<br/>acquire and release"]
    OR1 --- OR2 --- OR3
  end
  COH -->|"gives you nothing about"| ORD
  BUG["The central misconception:<br/>my caches are coherent,<br/>so I do not need barriers"]
  COH -.->|"leads to"| BUG
  BUG -.->|"broken by"| ORD

Coherence versus ordering, side by side. What it shows: the left box is a property of one variable at a time and is always in force; the right box is a property of the relationship between variables and is off unless you pay for it. The insight to take: these are orthogonal, and the entire remainder of this note is an explanation of why they can come apart — the coherence machinery sits below two buffers (the store buffer and the invalidate queue), and those buffers reorder the cross-location view without ever violating the per-location one.

The kernel documentation states the same distinction from the other side. Among the things that “may not be assumed about memory barriers”, memory-barriers.txt lists: “There is no guarantee that some intervening piece of off-the-CPU hardware will not reorder the memory accesses. CPU cache coherency mechanisms should propagate the indirect effects of a memory barrier between CPUs, but might not do so in order” (Documentation/memory-barriers.txt, v6.12, emphasis added). Coherence migrates the cache line and resolves conflicts; ordering is a separate property the hardware deliberately relaxes for speed, and barriers are how software claws it back.

Two litmus tests in the in-tree corpus test coherence directly, and both are Result: Never on every architecture Linux supports:

Litmus test (v6.12 tools/memory-model/litmus-tests/)What it asksResult
CoRR+poonceonce+Once.litmusAre two successive READ_ONCE()s of the same variable ordered? Can the second read an older value than the first?Never
CoWW+poonceonce.litmusAre two successive WRITE_ONCE()s of the same variable ordered? Can the final value be the first one written?Never

Coherence, as a checkable property. What it shows: the two smallest litmus tests in the kernel’s corpus, each using exactly one variable. The insight: because they use a single variable, coherence alone forbids the bad outcome — no barrier appears anywhere in either test. Every other litmus test in this note uses two or more variables, and that is exactly when the guarantee evaporates.

There is one honest asterisk. Perfbook records that Flur et al. found “surprisingly simple litmus tests that demonstrate that such guarantees can be violated on real hardware” when different-sized overlapping accesses hit one region of memory — the sort of thing a C union invites. The rule that follows is: restrict shared variables to “non-overlapping same-sized aligned accesses” (§15.3.6). memory-barriers.txt gives the same restriction in kernel terms: the guarantees “apply only to properly aligned and sized scalar variables”, meaning the size of char, short, int or long at natural alignment — and explicitly not to bitfields, because compilers implement bitfield updates with non-atomic read-modify-write sequences.


MESI: How Coherence Is Actually Maintained

To see why coherence alone is insufficient, you first need the protocol that provides it. McKenney walks through the classic four-state MESI protocol, named for the four states each cache line can be in. Each cache keeps a two-bit state tag per line alongside the line’s physical address and data.

StateCopies elsewhere?Clean or dirty?May this CPU write it without asking?
M — ModifiedNo, this is the only copyDirty; memory is staleYes — it already owns the line
E — ExclusiveNo, this is the only copyClean; matches memoryYes — silently transitions to M
S — SharedPossibly, in several cachesClean; matches memoryNo — “not permitted to store to the line without first consulting with other CPUs”
I — InvalidIrrelevantHolds no data at allNo — must fetch first

The four MESI states. What it shows: the two axes that actually matter are ownership (am I the only holder?) and cleanliness (does memory agree with me?). The insight to take: E exists purely as an optimisation — a line that is clean and unshared can be written with no bus traffic at all, which is why the E→M transition is the only one in the whole protocol that sends no messages. Everything expensive in this protocol is the cost of leaving S or I.

CPUs drive lines between these states by exchanging coherence messages on the interconnect. McKenney enumerates six:

MessageSent byMeaningReply required
ReadA CPU that wants to load a line it lacks“Send me the line at this physical address”Read Response
Read ResponseMemory, or whichever cache holds the line ModifiedCarries the data
InvalidateA CPU about to write a Shared line“Drop your copy of this line”Invalidate Acknowledge, from every other cache
Invalidate AcknowledgeEvery cache that received an Invalidate“My copy is gone”
Read InvalidateA CPU that wants to write a line it does not holdCombined Read + Invalidate: fetch and evict all other copiesRead Response and a full set of Invalidate Acknowledges
WritebackA cache ejecting a Modified lineAddress plus data, pushed to memory (possibly snooped by others on the way)

The MESI message set. What it shows: every state change that costs anything is a request-and-wait over the interconnect, and the expensive ones — Invalidate and Read Invalidate — require a reply from every other cache, not just one. The insight to take: as McKenney puts it, “a shared-memory multiprocessor system really is a message-passing computer under the covers.” That framing is the key to the rest of this note: the store buffer and the invalidate queue are nothing more than outbound and inbound message queues bolted onto that hidden message-passing machine, and a memory barrier is an instruction that drains one of them.

The full state machine has twelve transitions. Drawing it out is the fastest way to internalise which operations are cheap and which stall:

stateDiagram-v2
    direction LR
    M: M - Modified<br/>sole copy and dirty
    E: E - Exclusive<br/>sole copy and clean
    S: S - Shared<br/>possibly replicated and clean
    I: I - Invalid<br/>holds no data

    M --> E: a. writeback to memory<br/>keeps the right to modify
    E --> M: b. store to an owned line<br/>NO MESSAGES AT ALL
    M --> I: c. read-invalidate arrives<br/>reply read response + inv ack
    I --> M: d. atomic RMW on absent line<br/>send read-invalidate<br/>wait for response + all acks
    S --> M: e. atomic RMW on read-only line<br/>send invalidate<br/>wait for all acks
    M --> S: f. remote read arrives<br/>supply data and keep read-only copy
    E --> S: g. remote read arrives<br/>keep read-only copy
    S --> E: h. this CPU plans to write<br/>send invalidate and wait for all acks
    E --> I: i. remote atomic RMW<br/>read-invalidate arrives
    I --> E: j. store to absent line<br/>send read-invalidate<br/>then reach M via b
    I --> S: k. load of absent line<br/>send read and await response
    S --> I: l. remote store<br/>invalidate arrives and is acknowledged

The MESI state machine, all twelve transitions, transcribed from McKenney’s Figure 3 and the accompanying transition descriptions (a)–(l). What it shows: which state changes are local and free, and which require a round trip plus acknowledgements from every other cache. The insight to take: trace transition (j), I→E, the one that matters for the rest of this note. To store to a line it does not hold, a CPU must send a Read Invalidate and then wait for both a Read Response and a full set of Invalidate Acknowledges from every other cache in the machine. That wait is hundreds of cycles. Everything that follows — the store buffer, store forwarding, the invalidate queue, and by extension every memory barrier in the Linux kernel — exists to avoid stalling the core on transition (j).

MOESI adds a fifth state, Owned (O): a line may be dirty and shared simultaneously, with one designated owner responsible for eventual writeback. This avoids a memory round trip when a dirty line is shared between caches. MESIF adds Forward (F): among several Shared copies exactly one is marked F and is the one that answers a Read, so a line request gets exactly one cache-to-cache response instead of a storm of them. The extra states change performance, not semantics — none of them adds any cross-location ordering.

Uncertain

Verify: the specific claim that AMD64 uses MOESI and that Intel uses MESIF in current shipping parts. Reason: this is a widely repeated attribution, but it was not confirmed against the AMD64 Architecture Programmer’s Manual or the Intel SDM during this research pass — Documentation/memory-barriers.txt cites AMD64 APM Volume 2 chapters 7.1 and 7.4 in its reference list but does not name the protocol, and neither vendor’s architecture manual is obliged to specify a coherence protocol at all (the protocol is a microarchitectural implementation choice, and different parts from the same vendor may differ). To resolve: read AMD64 APM Vol. 2 §7 and Intel SDM Vol. 3A §9 (Memory Ordering) / §12 (Memory Cache Control) and quote them directly, or drop the vendor attribution and keep only the structural point that O and F are performance refinements over MESI. uncertain

The crucial point for ordering is that MESI by itself is enough to keep every individual line consistent. If MESI were all there were — if every store waited for its line and every invalidate were applied on arrival — memory would be sequentially consistent and barriers would be unnecessary. The trouble starts when CPU designers add buffers to hide the latency of these messages.


The Store Buffer: Why Coherence Stops Being Ordering

A write to a line not currently owned by the writing CPU is slow. As transition (j) above shows, the CPU must send a Read Invalidate, wait for the line to arrive and for every other cache to acknowledge invalidation, and only then write. McKenney quantifies the gap: “the time required to transfer a cache line from one CPU’s cache to another’s is typically a few orders of magnitude more than that required to execute a simple register-to-register instruction.” Stalling the core for hundreds of cycles on every first write to a line is intolerable, and worse, it is pure waste: as perfbook observes, “regardless of what data happens to be in the cache line that CPU 1 sends it, CPU 0 is going to unconditionally overwrite it.”

The fix is the store buffer: a small FIFO between the core and the cache. The CPU “can simply record its write in its store buffer and continue executing. When the cache line does finally make its way from CPU 1 to CPU 0, the data will be moved from the store buffer to the cache line.” The core never stalls; the slow coherence dance happens in the background.

flowchart TB
  subgraph CPU0["CPU 0"]
    direction TB
    C0["Core — issues loads and stores<br/>in program order"]
    SB0["STORE BUFFER<br/>outbound FIFO of writes<br/>not yet in the coherent domain<br/>invisible to every other CPU"]
    IQ0["INVALIDATE QUEUE<br/>inbound FIFO of invalidations<br/>acknowledged but not yet applied<br/>this cache is knowingly stale"]
    L0["L1 and L2 cache<br/>lines tagged M E S I"]
    C0 -->|"1. store lands here first"| SB0
    SB0 -->|"2. drains once the line is owned"| L0
    C0 -->|"1b. load snoops the store buffer first<br/>store forwarding"| SB0
    C0 -->|"1c. otherwise reads the cache"| L0
    IQ0 -->|"3. applied later, lazily"| L0
  end
  CC["COHERENCE FABRIC<br/>MESI / MOESI / MESIF messages<br/>Read - Read Response - Invalidate<br/>Invalidate Ack - Read Invalidate - Writeback"]
  MEM[("Shared memory")]
  L0 <-->|"coherence messages"| CC
  CC -->|"invalidates arrive here,<br/>acked immediately"| IQ0
  CC <--> MEM
  WMB["smp_wmb and smp_store_release<br/>fence THIS"]:::b --> SB0
  RMB["smp_rmb and smp_load_acquire<br/>fence THIS"]:::b --> IQ0
  MB["smp_mb fences BOTH"]:::b --> SB0
  MB --> IQ0
  classDef b fill:#ffe9c7,stroke:#c47f17,color:#4a3000

Where the reordering hides, and which barrier drains which queue. What it shows: the store buffer and the invalidate queue sit between the core and the coherent cache, i.e. outside the domain that MESI keeps consistent. A store sitting in the store buffer has not entered the coherent domain at all and no other CPU can see it; an invalidation sitting in the invalidate queue has been acknowledged but not applied, so this CPU is knowingly reading a stale line. The insight to take: every apparent violation of memory ordering in this note is one of those two queues, and every barrier in the kernel is an instruction that marks and drains one or both. Name the queue and the barrier’s purpose becomes obvious. Note the 1b edge — store forwarding — which is a third path and is discussed next; it fixes self-consistency and creates none of the inter-CPU problems.

This single optimisation breaks two guarantees, and the fixes for each define two barrier families.

Complication 1: Store Forwarding — a self-consistency fix, not an ordering one

Consider a = 1; b = a + 1; assert(b == 2); where a is owned by another CPU. The CPU puts a=1 in its store buffer, sends a Read Invalidate, and continues. When it computes b = a + 1, the line for a arrives still holding the old value 0 — the store is in the buffer, not yet applied to the cache. If the load reads from the cache, it gets a == 0, computes b == 1, and the assertion fails. McKenney is blunt about how bad this is: “this example breaks a very important guarantee, namely that each CPU will always see its own operations as if they happened in program order.”

Hardware designers “took pity” and added store forwarding: a load checks (snoops) the store buffer as well as the cache, so a CPU sees its own pending stores. With forwarding, the load of a finds 1 in the store buffer and the assertion passes. The x86-TSO paper states the same rule formally as part of its abstract machine: “a reading thread must read its most recent buffered write, if there is one, to that address; otherwise reads are satisfied from shared memory” (Sewell, Sarkar, Owens, Zappa Nardelli and Myreen, CACM final version dated 2010-05-17).

memory-barriers.txt states the resulting guarantee from the kernel’s side: “it is guaranteed that a CPU will be self-consistent: it will see its own accesses appear to be correctly ordered, without the need for a memory barrier”, and separately, “Overlapping loads and stores within a particular CPU will appear to be ordered within that CPU.”

Store forwarding fixes self-consistency. It does nothing at all for other CPUs — and that is where the real problem lives.

Complication 2: Global reordering — the bug that barriers exist to fix

Now two CPUs. CPU 0 runs a = 1; b = 1; (a data write, then a flag) where a lives only in CPU 1’s cache and b is owned by CPU 0. CPU 1 spins while (b == 0); assert(a == 1);. The lethal sequence is McKenney’s worked foo()/bar() example, drawn here as a timeline because that is the only way this kind of bug becomes obvious:

sequenceDiagram
    autonumber
    participant C0 as CPU 0 core
    participant B0 as CPU 0<br/>store buffer
    participant K0 as CPU 0 cache<br/>owns b in E
    participant K1 as CPU 1 cache<br/>holds a
    participant C1 as CPU 1 core
    Note over K0,K1: initial a == 0 and b == 0.<br/>a lives only in CPU 1's cache. CPU 0 owns b.
    C0->>B0: a = 1 — line for a not owned, so buffer it
    B0-->>K1: Read Invalidate for a's line
    C0->>K0: b = 1 — line already owned in E, write straight to cache
    Note over K0: b is now globally visible IMMEDIATELY
    C1->>K1: while (b == 0) — reload b
    K1-->>C1: b == 1, exit the loop
    C1->>K1: assert(a == 1) — read a
    K1-->>C1: a == 0 — STILL STALE
    Note over C0,C1: ASSERTION FAILS.<br/>a = 1 is STILL sitting in CPU 0's store buffer.
    B0->>K1: Read Invalidate finally completes, a = 1 drains

The message-passing (MP) failure caused by the store buffer alone. What it shows: CPU 0’s second store overtook its first one as seen by CPU 1, because the first store went into a buffer and the second went straight into an already-owned cache line. Nothing here violated coherence — a and b were each individually consistent at every step. The insight to take: the reordering is not caused by the core executing instructions out of order; it is caused by the two stores taking different-length paths to the coherent domain. That is why “the hardware designers cannot help directly here, since the CPUs have no idea which variables are related” — only the programmer knows that b is a flag for a, so only the programmer can insert the barrier.

The fix is a write memory barrier. McKenney: “The memory barrier smp_mb() will cause the CPU to flush its store buffer before applying subsequent stores.” More precisely — and this precision matters, because it explains why a barrier costs less than a full drain-and-wait — the CPU marks the store-buffer entries present at the barrier and holds back later stores until those marked entries have drained. Inserting smp_wmb() between a = 1 and b = 1 forces a = 1 to leave the store buffer, and thus become globally visible, before b = 1 may be published. The store buffer is the hardware reason smp_wmb() and smp_store_release() exist.

The kernel’s own litmus test for this is MP+fencewmbonceonce+fencermbonceonce.litmus, Result: Never (v6.12), and the reason it needs a barrier on both sides is the subject of two sections’ time.


The Store-Buffering Litmus Test: The One Reordering Even x86 Performs

The MP pattern above needs weak hardware to misbehave. There is a second, subtler pattern that misbehaves on every mainstream CPU including x86, and it is the reason smp_mb() cannot be optimised away anywhere. It is called store buffering (SB), and it is the pattern the store buffer is literally named for.

The kernel ships it as SB+poonceonces.litmus, Result: Sometimes (v6.12):

C SB+poonceonces

(*
 * Result: Sometimes
 *
 * This litmus test demonstrates that at least some ordering is required
 * to order the store-buffering pattern, where each process writes to the
 * variable that the preceding process reads.
 *)

{}

P0(int *x, int *y)
{
	int r0;

	WRITE_ONCE(*x, 1);      /* store to MY variable   */
	r0 = READ_ONCE(*y);     /* load from THEIR variable */
}

P1(int *x, int *y)
{
	int r0;

	WRITE_ONCE(*y, 1);
	r0 = READ_ONCE(*x);
}

exists (0:r0=0 /\ 1:r0=0)   /* can BOTH loads return 0? */

Reading it line by line: each CPU stores 1 to “its own” variable and then loads “the other’s”. Intuitively, at least one of the two stores must have happened first, so at least one of the two loads should see a 1. The exists clause asks whether both can see 0. The answer is yes — and here is exactly why:

sequenceDiagram
    autonumber
    participant C0 as CPU 0 core
    participant B0 as CPU 0<br/>store buffer
    participant K0 as CPU 0 cache<br/>holds y
    participant K1 as CPU 1 cache<br/>holds x
    participant B1 as CPU 1<br/>store buffer
    participant C1 as CPU 1 core
    Note over K0,K1: initial x == 0 and y == 0.<br/>CPU 0 has y cached. CPU 1 has x cached.
    C0->>B0: WRITE_ONCE(x, 1) — x's line not owned, buffer it
    C1->>B1: WRITE_ONCE(y, 1) — y's line not owned, buffer it
    Note over B0,B1: both stores are now in flight and INVISIBLE to the other CPU
    C0->>K0: r0 = READ_ONCE(y) — y IS in my cache
    K0-->>C0: y == 0
    C1->>K1: r0 = READ_ONCE(x) — x IS in my cache
    K1-->>C1: x == 0
    Note over C0,C1: 0:r0 == 0 AND 1:r0 == 0 — the exists clause TRIGGERS
    B0->>K1: read-invalidate x, then drain x = 1
    B1->>K0: read-invalidate y, then drain y = 1
    Note over K0,K1: afterwards both variables hold 1 and nobody ever saw a 0 that was not real

The store-buffering anomaly, drawn as the two-column timeline it is always taught as. What it shows: each CPU’s store is parked in its own private, unsnoopable store buffer while each CPU’s load hits a line it already holds and returns immediately. Neither load is stale in the coherence sense — the value 0 was genuinely the last value that reached the coherent domain. The insight to take: this is a store-then-load reordering on each CPU, and it is the only relaxation that x86-TSO permits (see the architecture table below). It is why smp_mb() is the one barrier that is a real, expensive instruction on x86, and it is why a release/acquire pair — which never orders store-then-load — cannot fix this pattern. That last point is the most common acquire/release mistake, and it is treated in detail in Acquire Release and Fence Semantics.

The step-by-step state, taken from perfbook’s Table 15.1, makes the invisibility concrete:

StepCPU 0 instructionCPU 0 store bufferCPU 0 cacheCPU 1 instructionCPU 1 store bufferCPU 1 cache
1(initial state)y == 0(initial state)x == 0
2x = 2;x == 2y == 0y = 2;y == 2x == 0
3r2 = y;0x == 2y == 0r2 = x;0y == 2x == 0
4(read-invalidate)x == 2x == 0(read-invalidate)y == 2y == 0
5(finish store)x == 2(finish store)y == 2

Perfbook Table 15.1, the store-buffering sequence of events (variables named x0/x1 there, and the stored value is 2 rather than 1). What it shows: on row 2, x simultaneously holds two values — 0 in CPU 1’s cache and 2 in CPU 0’s store buffer. The insight to take: perfbook flags this deliberately as the moment intuition dies (“But wait!!! On row 2 both x0 and x1 each have two values at the same time, namely zero and two. How can that possibly work???”). It works because the store buffer is not part of the coherent domain, so the two values are never simultaneously observable, only simultaneously extant. On rows 4 and 5 the two CPUs trade cache lines and each drains its buffer, and the variables settle.

This is not theory. McKenney ran it. Using the litmus7 tool, “the counter-intuitive ordering happened 314 times out of 100,000,000 trials on an x86 laptop”. More instructive still, the perfectly legal outcome where both loads return 2 occurred “less frequently, in this case, only 167 times”, prompting the lesson: “Increased counter-intuitivity does not necessarily imply decreased probability!” With smp_mb() on both sides (SB+fencembonceonces.litmus, Result: Never) the counter-intuitive outcome vanished across 100,000,000 trials, while the both-loads-see-2 outcome jumped to more than 800,000 occurrences — the barrier’s own latency widened the window in which the other CPU’s store could land.

Take three things from those numbers. First, a rate of roughly three in a million means an SB bug will not show up in a unit test but will show up in production. Second, the barrier does not merely fix ordering, it measurably changes timing, which is why “adding a barrier made the race go away” is never evidence that the barrier was the right fix. Third, this is x86 — the strongly-ordered architecture.

The Linux kernel’s most important use of the SB pattern is not Dekker’s algorithm but wakeup ordering, and recipes.txt reproduces the canonical comment from waitqueue_active() in include/linux/wait.h:

 *      CPU0 - waker                    CPU1 - waiter
 *
 *                                      for (;;) {
 *      @cond = true;                     prepare_to_wait(&wq_head, &wait, state);
 *      smp_mb();                         // smp_mb() from set_current_state()
 *      if (waitqueue_active(wq_head))         if (@cond)
 *        wake_up(wq_head);                      break;
 *                                        schedule();
 *                                      }
 *                                      finish_wait(&wq_head, &wait);

Line by line: the waker stores to @cond and then loads the wait-queue state; the waiter stores its task state (inside prepare_to_wait()) and then loads @cond. Store-then-load on both sides — textbook SB. Without the two full barriers, the waiter can put itself to sleep having read a stale @cond, while the waker reads a stale wait-queue and decides nobody needs waking. The result is a hang, and it is a rare hang. set_current_state() supplies the waiter’s barrier implicitly by expanding to smp_store_mb(), which stores and then emits a full barrier (memory-barriers.txt, v6.12).


The Invalidate Queue: Why the Reader Also Needs a Barrier

Fixing the writer is only half the story, because the reader’s side has a symmetric buffer with a symmetric failure.

Invalidate Acknowledge messages can be slow to produce. A CPU busy “intensively loading and storing data” may not get around to actually evicting a line promptly, and McKenney warns that “if a large number of invalidate messages arrive in a short time period, a given CPU might fall behind in processing them, thus possibly stalling all the other CPUs.” Recall from the message table that an Invalidate needs an acknowledgement from every cache: one slow acknowledger throttles every writer in the machine.

The fix is the invalidate queue: “A CPU with an invalidate queue may acknowledge an invalidate message as soon as it is placed in the queue, instead of having to wait until the corresponding line is actually invalidated.” The CPU promises to process the queued invalidation before it sends any further coherence message about that line — but in the meantime, it keeps reading the stale line.

This re-breaks the reader even when the writer is fully barriered:

sequenceDiagram
    autonumber
    participant C0 as CPU 0 core
    participant B0 as CPU 0<br/>store buffer
    participant C1 as CPU 1 core
    participant Q1 as CPU 1<br/>invalidate queue
    participant K1 as CPU 1 cache<br/>a is SHARED here
    Note over C0,K1: writer already has smp_wmb between a = 1 and b = 1
    C0->>B0: a = 1 — buffered, send Invalidate for a
    B0->>Q1: Invalidate a
    Q1-->>B0: Invalidate Acknowledge — IMMEDIATELY, from the queue
    Note over Q1,K1: a's OLD value is STILL in CPU 1's cache.<br/>The invalidation is queued, not applied.
    Note over B0: ack received, so smp_wmb is satisfied and the store buffer drains
    C0->>C0: b = 1 published
    C1->>K1: while (b == 0) — sees b == 1, exits
    C1->>K1: assert(a == 1)
    K1-->>C1: a == 0 — reads the STALE line
    Note over C1: ASSERTION FAILS — again, with no coherence violation anywhere
    Q1->>K1: (later) queued invalidation finally applied

The invalidate-queue failure — the reader’s mirror image of the store-buffer failure. What it shows: CPU 1 acknowledged the invalidation from a queue and then went on reading the old line. The writer’s smp_wmb() was satisfied, because from the writer’s point of view every acknowledgement arrived. The insight to take: the invalidation is not lost, only deferred — coherence is still intact and will be honoured eventually. What is broken is the timing relative to the reader’s other loads, which is precisely an ordering problem. Fixing the writer alone is never enough: MP needs a barrier on both sides because two independent queues, one per direction, are doing the reordering.

The fix is a read memory barrier that acts on the invalidate queue. McKenney: “when a given CPU executes a memory barrier, it marks all the entries currently in its invalidate queue, and forces any subsequent load to wait until all marked entries have been applied to the CPU’s cache.” Inserting smp_rmb() before the assertion forces CPU 1 to drain the queued invalidation of a before loading a; the load then misses, re-fetches the fresh line, and the assertion passes. The invalidate queue is the hardware reason smp_rmb() and smp_load_acquire() exist.

The symmetry is exact, and McKenney states it as a rule: “Roughly speaking, a ‘read memory barrier’ marks only the invalidate queue and a ‘write memory barrier’ marks only the store buffer, while a full-fledged memory barrier does both.”

Hardware queueDirectionWhat it defersApparent reordering it causesBarrier that fences itKernel one-way form
Store bufferOutboundThis CPU’s own stores entering the coherent domainMy stores reach other CPUs out of program ordersmp_wmb()smp_store_release()
Invalidate queueInboundOther CPUs’ invalidations being applied to my cacheMy loads return stale values after I have already seen a newer flagsmp_rmb()smp_load_acquire()
BothStore-then-load reordering; loss of a global ordersmp_mb()(no one-way form exists — see below)

The queue-to-barrier correspondence. What it shows: each barrier family maps onto exactly one queue, and the full barrier is the one that drains both. The insight to take: the publish-then-flag pattern needs smp_wmb() on the writer and smp_rmb() on the reader precisely because two independent queues reorder the two sides — neither barrier alone is enough. Notice the empty cell in the last row: there is no one-way “acquire” or “release” that fixes store-then-load, which is the single most consequential fact carried into Acquire Release and Fence Semantics.

There is a wrinkle the diagram above elides, and memory-barriers.txt is careful about it: a read barrier does not only drain queued invalidations, it also has to defeat load speculation. “Many CPUs speculate with loads: that is they see that they will need to load an item from memory, and they find a time where they’re not using the bus for any other loads, and so do the load in advance.” A read barrier “will force any value speculatively obtained to be reconsidered”: if no update to that location is pending, the speculated value is simply used; if an update or invalidation is pending, “the speculation is discarded and an updated value is retrieved”. Both mechanisms — queued invalidation and speculated load — produce the same symptom, and smp_rmb() addresses both.


x86-TSO versus Weak ARM, POWER and RISC-V: Same Coherence, Different Queue Discipline

Why, then, are smp_wmb() and smp_rmb() real fence instructions on ARM64 and mere compiler barriers on x86? Because architectures expose different amounts of this queue-induced reordering to software. The coherence protocol is essentially the same everywhere; what differs is how much of the store buffer and invalidate queue the programmer is allowed to see.

x86 is Total Store Order

Sewell et al. formalised x86 as the x86-TSO abstract machine: “a shared memory … a global lock … and one store buffer per hardware thread”. Store buffers are FIFO, and the only observable relaxation from sequential consistency is store buffering. The paper’s opening example is the SB litmus test in x86 assembly, with the verdict stated as an architectural fact:

SB
        Proc 0                    Proc 1
   MOV [x] <- 1              MOV [y] <- 1
   MOV EAX <- [y]            MOV EBX <- [x]
   Allowed Final State: Proc 0:EAX = 0  /\  Proc 1:EBX = 0

and the microarchitectural explanation: “Microarchitecturally, one can view this particular example as a visible consequence of store buffering: if each processor effectively has a FIFO buffer of pending memory writes (to avoid the need to block while a write completes), then the reads from y and x could occur before the writes have propagated from the buffers to main memory.”

Two further properties of x86-TSO matter. First, x86 does not reorder load-load, store-store, or a store with its own earlier load. Second, x86-TSO forbids IRIW (independent reads of independent writes): “different processors or hardware threads do not observably share store buffers”, so apart from each core’s own buffer, all cores share one view of memory. It is worth being precise about the paper’s own framing here: it was written because “the public vendor architectures … are often in ambiguous informal prose”, and the authors found that “all contain serious ambiguities, some are arguably too weak to program above, and some are simply unsound with respect to actual hardware.” x86-TSO is the model that replaced that prose, and it is now the reference used to verify low-level code — the paper itself verifies a Linux spinlock implementation.

flowchart TB
  T0["Thread 0"] --> SB0["Store buffer 0<br/>FIFO"]
  T1["Thread 1"] --> SB1["Store buffer 1<br/>FIFO"]
  T2["Thread 2"] --> SB2["Store buffer 2<br/>FIFO"]
  SB0 --> LK{"Global lock<br/>held by a LOCKed instruction<br/>or by MFENCE"}
  SB1 --> LK
  SB2 --> LK
  LK --> MEM[("Shared memory<br/>one single agreed view")]
  MEM -.->|"loads read here"| T0
  MEM -.->|"loads read here"| T1
  MEM -.->|"loads read here"| T2
  SB0 -.->|"store forwarding:<br/>my own newest buffered<br/>write to this address"| T0
  SB1 -.->|"store forwarding"| T1
  SB2 -.->|"store forwarding"| T2

The x86-TSO abstract machine, after Sewell et al. What it shows: exactly one relaxation is modelled — a private FIFO store buffer per thread, plus store forwarding out of it. Everything downstream of the buffers is a single shared memory with a single global order. The insight to take: because there is only one shared memory and no per-pair buffering, x86 gives you load-load and store-store ordering for free, which is why __smp_rmb() and __smp_wmb() need emit no instruction at all on x86 — only the compiler needs restraining. The one thing this picture does allow is a load overtaking an older store to a different address, and that is the SB anomaly. smp_mb() must therefore be a real instruction, because it is the instruction that takes the global lock and flushes the buffer.

ARM, POWER and RISC-V are weakly ordered

These architectures expose the invalidate queue and far more aggressive buffering directly. Load-load and store-store are not ordered by default. The Arm Architecture Reference Manual makes the formal position explicit: the model is defined by relations such as Barrier-ordered-before, and only the presence of a barrier or an ordered access puts two memory effects into that relation at all (Arm ARM, DDI 0487 M.c, §B2.3). It also pins down where Arm sits on multicopy atomicity: “The Arm memory model is Other-multi-copy atomic” (§B2.2.4) — a point returned to below.

The clearest evidence of the difference is the kernel’s own per-architecture lowering. These are read verbatim from v6.12:

Kernel primitivex86 (arch/x86/include/asm/barrier.h)ARM64 (arch/arm64/…)RISC-V (arch/riscv/…)
__smp_mb()lock; addl $0,-4(%_ASM_SP) — a real, expensive locked instructiondmb(ish)RISCV_FENCE(rw, rw)
__smp_rmb()dma_rmb()barrier()no instructiondmb(ishld)RISCV_FENCE(r, r)
__smp_wmb()barrier()no instructiondmb(ishst)RISCV_FENCE(w, w)
__smp_store_release(p, v)barrier(); WRITE_ONCE(*p, v)stlrb / stlrh / stlr %w1 / stlr %x1 by sizeRISCV_FENCE(rw, w); WRITE_ONCE(*p, v)
__smp_load_acquire(p)READ_ONCE(*p); barrier()ldarb / ldarh / ldar %w0 / ldar %0 by sizeREAD_ONCE(*p); RISCV_FENCE(r, rw)
__smp_mb__before_atomic() / __after_atomic()do { } while (0) — atomics are already serialising(generic: smp_mb())(generic)
Mandatory (device) barriers__mb() = mfence, __rmb() = lfence, __wmb() = sfence__mb() = dsb(sy), __rmb() = dsb(ld), __wmb() = dsb(st)__mb() = RISCV_FENCE(iorw, iorw)

What each smp_*() macro actually compiles to, per architecture, in Linux 6.12. What it shows: the same C source produces nothing on x86 and a real fence instruction on ARM64 and RISC-V for two of the three barriers. The insight to take: two details reward a second look. First, ARM64’s smp_mb() is dmb(ish) — the inner-shareable domain, i.e. “the other CPUs in this coherency domain” — while the mandatory mb() is the far heavier dsb(sy), system-wide and waiting for completion rather than merely ordering; using mb() where smp_mb() would do is a real, measurable mistake on ARM. Second, x86’s smp_mb() is a lock-prefixed dummy add on the stack, not mfence; the locked instruction is used because on many x86 parts it is cheaper than mfence while providing the same store-buffer flush. mfence still appears, but as the mandatory mb() used for device ordering.

RISC-V deserves a direct statement because it is now shipping hardware rather than a curiosity. The v6.12 header lowers __smp_rmb() to RISCV_FENCE(r, r) and __smp_wmb() to RISCV_FENCE(w, w) — genuine fence instructions ordering reads against reads and writes against writes. Since the kernel only emits an instruction where the architecture requires one, this settles the question directly: RISC-V’s baseline memory model (RVWMO) is weak in the same load-load and store-store sense as Arm, not a TSO variant. The same header also notes that RISC-V requires a full RISCV_FENCE(iorw, iorw) for smp_mb__after_spinlock() because “the ‘critical section is RCsc’ guarantee mandates a barrier on RISC-V”, the acquire/release pair on the lock word providing only RCpc.

Resolved

An earlier revision of this note carried an uncertainty flag asking whether RVWMO is weak in the load-load / store-store sense. It is: verified 2026-09-04 against arch/riscv/include/asm/barrier.h, v6.12, which emits real fence r,r and fence w,w instructions for __smp_rmb() and __smp_wmb(). The optional Ztso extension does define a TSO variant of RISC-V, so a specific implementation may be stronger than the baseline; the kernel’s generic RISC-V build assumes RVWMO.

The full reordering matrix

Perfbook’s Table 15.5 is the single most useful reference in this area, and it is worth reproducing because it answers “will this bug reproduce on my machine?” directly. Y means the architecture permits the reordering (that is, Y is bad news for the programmer):

PropertyAlphaArmv7-A/RArmv8ItaniumMIPSPOWERSPARC TSOx86z Systems
Loads reordered after loads or stores?YYYYYY
Stores reordered after stores?YYYYYY
Stores reordered after loads?YYYYYYYYY
Atomic instructions reordered with loads or stores?YYYYY
Dependent loads reordered?Y
Dependent stores reordered?
Non-sequentially-consistent?YYYYYYYYY
Non-multicopy-atomic?YYYYYYYY
Non-other-multicopy-atomic?YYYYY
Non-cache-coherent?Y
Load-acquire / store-release implemented as…FFiIFb
Atomic RMW instruction typeLLLCLLCCC
Incoherent instruction cache or pipeline?YYYYYYYYY

Key for the instruction rows: F = full memory barrier; i = instruction with lightweight ordering; I = instruction with heavyweight ordering; b = lightweight memory barrier; L = load-linked/store-conditional; C = compare-and-exchange. Reproduced from perfbook v2026.06.21a Table 15.5; RISC-V is not in that table (see the row-by-row discussion above and the kernel header for its position).

The per-architecture reordering matrix. What it shows: four rows carry almost all the practical weight. The insight to take: (1) The store-then-load row is Y for every architecture — that is the SB anomaly, universal, and the reason smp_mb() never optimises away. (2) The first two rows split the world in half: the load-load and store-store reorderings that make MP fail are permitted on Alpha/Arm/Itanium/MIPS/POWER and forbidden on SPARC TSO, x86 and z Systems — this row is “correct on my laptop, broken on the server”. (3) Dependent loads reordered is Y for Alpha alone, which is why Alpha shaped the whole Linux memory model and why READ_ONCE() gained an smp_mb() on Alpha in v4.15. (4) Non-cache-coherent is Y for Itanium alone: it is the only Linux-supported architecture that can reorder ordinary loads to the same variable, which the kernel dodges because READ_ONCE() emits a volatile load that compiles to ld.acq there. Everywhere else, coherence really is free.

Notice also that Armv8 is the one weakly-ordered entry that is other-multicopy atomic — matching the Arm ARM’s own §B2.2.4 statement quoted above, and a genuine strengthening over Armv7 and POWER.


Multicopy Atomicity: When Two CPUs Disagree About One Order

There is a third failure mode beyond “my stores are reordered” and “my loads are stale”, and it only appears with three or more CPUs. Multicopy atomicity is, in memory-barriers.txt’s words, “a deeply intuitive notion about ordering that is not always provided by real computer systems, namely that a given store becomes visible at the same time to all CPUs, or, alternatively, that all CPUs agree on the order in which all stores become visible.”

Full multicopy atomicity would forbid valuable hardware optimisations, so most vendors provide the weaker other-multicopy atomicity, which, as perfbook explains, “excludes the CPU doing a given store from the requirement that all CPUs agree on the order of all stores. This means that if only a subset of CPUs are doing stores, the other CPUs will agree on the order of stores.” The exclusion is precisely store forwarding: “the CPU doing the store is permitted to observe its store early, which allows its later loads to obtain the newly stored value directly from the store buffer, which improves performance.” Store forwarding, seen from three CPUs away, is the loss of full multicopy atomicity.

The canonical test is IRIW — independent reads of independent writes. The kernel ships both variants:

flowchart TB
  subgraph W["Two independent writers"]
    P0["P0<br/>WRITE_ONCE(x, 1)"]
    P2["P2<br/>WRITE_ONCE(y, 1)"]
  end
  subgraph R["Two independent readers"]
    P1["P1<br/>r0 = READ_ONCE(x) → 1<br/>smp_mb()<br/>r1 = READ_ONCE(y) → 0<br/>'x happened first'"]
    P3["P3<br/>r0 = READ_ONCE(y) → 1<br/>smp_mb()<br/>r1 = READ_ONCE(x) → 0<br/>'y happened first'"]
  end
  P0 -->|"rf"| P1
  P2 -->|"rf"| P3
  P0 -.->|"P3 has not seen it yet"| P3
  P2 -.->|"P1 has not seen it yet"| P1
  V{"Do P1 and P3 agree<br/>on which store came first?"}
  R --> V
  V -->|"no barriers:<br/>IRIW+poonceonces+OnceOnce<br/>Result: SOMETIMES"| BAD["They can DISAGREE.<br/>Neither is wrong.<br/>There is no global order."]
  V -->|"smp_mb between each pair of reads:<br/>IRIW+fencembonceonces+OnceOnce<br/>Result: NEVER"| GOOD["Forced to agree —<br/>this is LKMM's<br/>propagation rule at work"]

Independent reads of independent writes, in both kernel variants. What it shows: two writers touch two different variables; two readers each read both, in opposite orders. The exists clause asks whether P1 can conclude “x first” while P3 concludes “y first”. The insight to take: without barriers this is allowed on non-multicopy-atomic hardware, meaning there is genuinely no single global order of stores for the two readers to disagree about — disagreement is not a bug in one of them. Only a full barrier restores agreement (smp_rmb() would not suffice, because the problem is propagation between CPUs, not the local order of two loads). Recall that x86-TSO forbids IRIW outright, which is one more class of bug that is structurally invisible on an x86 laptop. This is also the reason memory-barriers.txt says a release-acquire chain orders only “those CPUs on the chain”: see Acquire Release and Fence Semantics, which develops that limit at length.

A closely related test, WRC (write-read-causality), shows the same effect with a causal chain rather than a fork: WRC+poonceonces+Once.litmus is Result: Sometimes, while WRC+pooncerelease+fencermbonceonce+Once.litmus is Result: Never because — as the test’s own comment says — “smp_store_release() is A-cumulative in LKMM”. A-cumulativity is the formal property that makes propagation work across a chain: for a release fence on CPU C, “any store which propagates to C before a release fence is executed (including all po-earlier stores executed on C) is forced to propagate to [every other CPU] before the store associated with the release fence does” (explanation.txt, v6.12). Critically, smp_wmb() is not A-cumulative: “they only affect the propagation of stores that are executed on C”, not stores that merely arrived at C. That asymmetry is a concrete reason to prefer smp_store_release() over smp_wmb(), and it is developed in the sibling note.

memory-barriers.txt gives the load-side version of the same lesson with a three-CPU example, and states the rule flatly: “if this example runs on a non-multicopy-atomic system where CPUs 1 and 2 share a store buffer or a level of cache, CPU 2 might have early access to CPU 1’s writes. General barriers are therefore required to ensure that all CPUs agree on the combined order of multiple accesses.”


The Practical Consequence: Correct on x86, Broken on ARM

The gap between the two halves of the reordering matrix stopped being an academic point somewhere around the time Graviton, Ampere Altra and Apple Silicon became ordinary deployment targets. Code that is correct by accident on x86 is incorrect on ARM64, and the failure is silent, rare, and data-dependent.

flowchart TB
  START["I wrote lock-free code<br/>touching 2+ shared variables"] --> Q1{"Does the pattern<br/>involve a STORE followed by<br/>a LOAD of a different variable<br/>on the same CPU?"}
  Q1 -->|"yes — SB pattern"| SB["BROKEN EVERYWHERE,<br/>x86 included.<br/>Needs smp_mb.<br/>~3 in 1,000,000 on an x86 laptop"]
  Q1 -->|"no"| Q2{"Does it involve two<br/>STORES that must be seen<br/>in order, or two LOADS<br/>that must be seen in order?"}
  Q2 -->|"yes — MP pattern"| MP["PASSES ON x86 FOREVER.<br/>FAILS ON ARM / POWER / RISC-V.<br/>Needs wmb+rmb, or release+acquire"]
  Q2 -->|"no"| Q3{"Do 3+ CPUs need to agree<br/>on one global order?"}
  Q3 -->|"yes — IRIW / WRC / Z6.0"| IR["PASSES ON x86 (IRIW forbidden by TSO).<br/>FAILS ON ARM / POWER.<br/>Needs FULL barriers, not one-way ones"]
  Q3 -->|"no"| Q4{"Only ONE shared variable,<br/>or only one thread?"}
  Q4 -->|"yes"| OK["Coherence alone suffices.<br/>Still use READ_ONCE and WRITE_ONCE<br/>to restrain the compiler"]
  Q4 -->|"no"| RETHINK["Draw the litmus test.<br/>Run it under herd7."]
  MP:::bad
  IR:::bad
  SB:::bad
  classDef bad fill:#ffdede,stroke:#b03030,color:#5a0000

Which patterns hide on x86, and which do not. What it shows: a triage path from “what shape is my code” to “on which hardware will it fail”. The insight to take: the middle two branches are the dangerous ones — they are the patterns that pass every test you can run on an x86 development machine and then fail in production on ARM64. The SB branch is, perversely, the safer class of bug, because it can at least be reproduced locally. Note the last branch too: “only one variable” is the one case where coherence genuinely saves you, and even there READ_ONCE()/WRITE_ONCE() are needed against the compiler, which is a second, independent reordering engine — see Compiler Barriers and READ_ONCE WRITE_ONCE.

The kernel’s own doctrine, stated in memory-barriers.txt, is to assume the worst: “It has to be assumed that the conceptual CPU is weakly-ordered but that it will maintain the appearance of program causality with respect to itself. Some CPUs (such as i386 or x86_64) are more constrained than others (such as powerpc or frv), and so the most relaxed case (namely DEC Alpha) must be assumed outside of arch-specific code.” That is why an architecture “can provide more than the minimum requirement for any particular barrier, but if the architecture provides less than that, that architecture is incorrect” — the barrier API is defined by the weakest machine, and any strengthening an architecture provides must never be relied upon in generic code.

Two secondary consequences are worth spelling out. First, a UP build hides everything: “SMP memory barriers are reduced to compiler barriers on uniprocessor compiled systems because it is assumed that a CPU will appear to be self-consistent.” Second, a virtual machine guest can be bitten even when built without SMP support, “an artifact of interfacing with an SMP host while running an UP kernel”; the kernel provides virt_mb() and friends, which “have the same effect as smp_mb() etc when SMP is enabled, but generate identical code for SMP and non-SMP systems”, precisely for guests synchronising against a possibly-SMP host.


Common Misunderstandings

“If my caches are coherent, I don’t need barriers.” The most common false intuition, and the whole point of this note: coherence is a per-location property, ordering is a cross-location property, and the store buffer and invalidate queue sit below coherence and reorder the cross-location view. A fully coherent machine still requires barriers. The one-variable litmus tests (CoRR, CoWW) are Never with no barriers at all; every two-variable test in this note is Sometimes without them.

“The store buffer is just another level of cache.” No — it is an ordering structure, not a coherence structure. The store buffer holds writes that have not yet entered the coherent domain at all, and other CPUs cannot snoop it (in x86-TSO terms, store buffers are not observably shared). That invisibility is precisely why it reorders. A cache participates in MESI; a store buffer does not.

“x86 has no reordering, so memory models don’t matter there.” x86 reorders store-before-load — the one case that breaks Dekker-style mutual exclusion, seqlock writers, and wakeup ordering. It is less reordering than ARM, not no reordering, and the gap between “less” and “none” is exactly the class of bug that reproduces once every few million iterations.

“Store forwarding causes the inter-CPU bug.” Store forwarding fixes single-CPU self-consistency; it has no effect across CPUs. The inter-CPU reordering comes from the store buffer’s invisibility to other cores, which forwarding does not address. (Seen from a third CPU, however, forwarding is exactly what costs a machine its full multicopy atomicity — so the two are related, just not in the direction the misconception assumes.)

“A barrier makes my store visible faster.” Perfbook lists this among its rules of thumb, and inverts it: “Ordering operations almost never speed things up. If you find yourself tempted to add a memory barrier in an attempt to force a prior store to be flushed to memory faster, resist! Adding ordering usually slows things down.” A barrier does not accelerate propagation; it delays your own subsequent operations until propagation has happened. memory-barriers.txt says the same in guarantee form: “There is no guarantee that any of the memory accesses specified before a memory barrier will be complete by the completion of a memory barrier instruction; the barrier can be considered to draw a line in that CPU’s access queue.”

“A barrier on my side is enough.” It never is, for exactly the two-queue reason this note develops. memory-barriers.txt: “There is no guarantee that a CPU will see the correct order of effects from a second CPU’s accesses, even if the second CPU uses a memory barrier, unless the first CPU also uses a matching memory barrier.” Perfbook states it as a rule of thumb: “Ordering operations must be paired.”

“The hardware is the only thing reordering my code.” Perfbook is emphatic that this understates the problem: “compilers reorder much more aggressively than hardware ever dreamed of doing.” A plain C store can be invented, elided, split into two smaller stores, or hoisted out of an if. That is a separate layer with separate defences, covered in Compiler Barriers and READ_ONCE WRITE_ONCE.


Production Notes

This buffer model is not a teaching abstraction — it is the operative mental model kernel and runtime engineers actually use, and the litmus tests in this note are shipped in the kernel tree precisely so that proposed lock-free algorithms can be checked before they ship. tools/memory-model/README (v6.12) explains the workflow: the model is written in the cat language and executed by the external herd7 simulator, which “exhaustively explores the state space of small litmus tests”; a companion tool, klitmus7, converts a litmus test into a loadable kernel module so the same test can be exercised on real hardware. Both come from herdtools7, version 7.52 or higher, and the README carries a compatibility table pinning target kernel ranges to herdtools versions (5.17 and later require 7.56.1 or higher). Several thousand further litmus tests live in github.com/paulmckrcu/litmus and in the perfbook repository.

The foo()/bar() publish-then-flag example is not a toy either; it is the shape of real driver code. memory-barriers.txt gives the descriptor-ring version verbatim, using the DMA-specific barriers:

	if (desc->status != DEVICE_OWN) {
		/* do not read data until we own descriptor */
		dma_rmb();
 
		/* read/modify data */
		read_data = desc->data;
		desc->data = write_data;
 
		/* flush modifications before status update */
		dma_wmb();
 
		/* assign ownership */
		desc->status = DEVICE_OWN;
 
		/* Make descriptor status visible to the device followed by
		 * notify device of new descriptor
		 */
		writel(DESC_NOTIFY, doorbell);
	}

Reading it against this note: the dma_rmb() is the invalidate-queue barrier — it stops the CPU reading descriptor payload out of a stale cache line before it has confirmed ownership. The dma_wmb() is the store-buffer barrier — it stops the ownership flag reaching the device before the payload does. The dma_*() family exists because a DMA engine is a coherence participant but not a CPU, so the ordering required is against consistent memory rather than against another core; on ARM64 they lower to the outer-shareable dmb(oshld) and dmb(oshst) rather than the inner-shareable forms used by smp_rmb()/smp_wmb(). And memory-barriers.txt is careful to add that “the dma_*() barriers do not provide any ordering guarantees for accesses to MMIO regions” — MMIO needs the readX()/writeX() accessors, whose five ordering guarantees are enumerated separately.

Real in-tree examples of the same patterns, all cited by recipes.txt (v6.12): fs/xfs/xfs_log.c’s xlog_state_switch_iclogs() uses smp_wmb() between updating l_curr_block and incrementing l_curr_cycle, paired with smp_rmb() in xlog_valid_lsn() in fs/xfs/xfs_log_priv.h; kernel/events/ring_buffer.c’s perf_output_put_handle() carries a comment documenting a four-way A/B/C/D barrier pairing across the kernel-user ring-buffer boundary, where B pairs with C as MP and A pairs with D as load buffering; and lib/stackdepot.c’s init_stack_slab() uses release-acquire for the same publish-then-flag job.

Finally, the historical arc is worth knowing, because it explains why the kernel’s rules are shaped as they are. DEC Alpha — the only architecture that reorders dependent loads, thanks to a split data cache in which “one cache bank processes even-numbered cache lines and the other bank processes odd-numbered cache lines” — defined the Linux memory model for two decades. In v4.15 the kernel added an smp_mb() to READ_ONCE() on Alpha, which, in the documentation’s words, “greatly reduced its impact on the memory model”; in v5.9 the explicit address-dependency barrier API (smp_read_barrier_depends()) was removed entirely, its semantics now implicit in all marked accesses. The formal successor to this whole line of work is the Linux Kernel Memory Model, whose design principles were published by Alglave, Maranget, McKenney, Parri and Stern in LWN, 14 April 2017 — “the first realistic automated representation of Linux-kernel memory ordering” — and are the subject of The Linux Kernel Memory Model.

The bottom line for practitioners: when you write smp_wmb() you are marking a FIFO of pending stores, when you write smp_rmb() you are marking a queue of deferred invalidations, and when you write smp_mb() you are doing both and paying for it on every architecture including x86. Name the queue and the barrier’s purpose becomes obvious.


See Also