Good to see you again. In the previous lesson, you selected synchronization primitives by asking who participates, whether any context may sleep, and whether the protocol is ownership or notification. That protects which execution contexts may touch shared state. This lesson goes one layer lower: when ownership crosses a CPU, DMA engine, or MMIO boundary, what prevents the compiler, CPU, caches, and peripheral fabric from exposing the steps in the wrong order?
By the end, you should be able to annotate a descriptor-ring or DMA handoff with the right barrier or API, explain why it belongs there, and distinguish ordering bugs from cache-coherency bugs. These are common interview topics because a driver can appear stable on x86 or one board revision while failing on Arm or an incoherent DMA platform.
Four mechanisms, four different jobs
“Add a barrier” is not a complete diagnosis. First identify the observer that could see an invalid intermediate state.
| Mechanism | Primary observer controlled | What it guarantees | What it does not guarantee |
|---|---|---|---|
| Compiler barrier | The compiler | Prevents compiler reordering, merging, or elision across a point in source code | CPU execution order, cache clean/invalidate, MMIO completion |
| CPU memory barrier | Other CPUs or a DMA-capable observer of shared memory | Orders normal-memory reads and/or writes as observed externally | Cache coherency or MMIO ordering by itself |
| DMA barrier | A device sharing DMA-coherent memory | Orders descriptor data relative to an ownership or validity field | MMIO register access ordering |
| I/O accessor or I/O barrier | A peripheral and I/O fabric | Gives documented ordering between normal memory and MMIO accesses | Cache maintenance for non-coherent DMA; necessarily completing a posted write |
| DMA mapping/synchronization API | CPU caches and a non-coherent DMA device | Performs required cache maintenance and establishes CPU/device access phases | Ordering between independent descriptor fields unless the protocol includes it |
The key distinction is:
Ordering says: “observer B must not see operation 2 before operation 1.”
Coherency says: “observer B can obtain the current contents rather than a stale cached copy.”
A memory barrier addresses the first statement. It does not, by itself, push dirty cache lines to RAM or invalidate stale CPU cache lines.
Why volatile is still insufficient
In C, volatile tells the compiler that an access itself is observable and generally must not be removed or casually combined. It can be appropriate in low-level bare-metal MMIO code when your platform’s access definitions require it. But it does not establish:
- ordering of normal-memory accesses as observed by another CPU or device;
- a release of descriptor contents before an ownership bit;
- acquire semantics after reading a completion bit;
- cache cleaning before a non-coherent device reads RAM;
- cache invalidation before the CPU consumes device-written data.
In Linux drivers, use the kernel’s MMIO accessors such as readl() and writel() rather than dereferencing an MMIO pointer directly. Use the DMA and synchronization APIs rather than open-coding cache operations.
The following excerpts are the central reference for the rest of the lesson.
Linux kernel memory barriers — The Linux Kernel documentation
Read the Linux Kernel documentation to connect descriptor ownership, DMA barriers, MMIO accessors, and cache coherency into one model. Focus on the guarantees each API actually makes; many real bugs come from assuming one type of barrier provides another type's guarantee.
First, find the passage beginning “These are for use with consistent memory” and read the descriptor example. Trace why the CPU must not read descriptor data before observing returned ownership, and why it must write descriptor data before giving ownership to the device. Next, in the section “KERNEL I/O BARRIER EFFECTS,” read from “For drivers intending to be portable between multiple architectures and bus implementations” through the standard accessor guarantees. Compare readX() and writeX() with their _relaxed forms. Finally, in “CACHE COHERENCY VS DMA,” read the coherency discussion. Keep a clear boundary in mind: flushing and invalidating caches are different operations from ordering accesses.
A transmit descriptor: data, ownership, then doorbell
Consider a NIC, MAC-PHY, or DMA engine using a coherent descriptor ring. The CPU fills a descriptor; setting an ownership field gives the device permission to fetch it. Finally, the driver rings a doorbell register to tell the device there is new work.
/* The CPU currently owns desc. Descriptor ring is DMA coherent. */
desc->dma_addr = dma_addr;
desc->length = length;
desc->flags = TX_CSUM | TX_INTERRUPT;
/* Descriptor contents must precede the ownership handoff. */
dma_wmb();
WRITE_ONCE(desc->status, DESC_DEVICE_OWN);
/*
* Notify the device. With the normal accessor and default MMIO mapping,
* prior normal-memory writes are ordered before this MMIO trigger.
*/
writel(next_tail, priv->regs + TX_DOORBELL);
This sequence has two distinct boundaries.
1. Descriptor fields before the ownership bit
The device treats DESC_DEVICE_OWN as a release of ownership from CPU to hardware. Once it observes that status, it is allowed to read dma_addr, length, and flags.
Without the dma_wmb(), a weakly ordered CPU could make the ownership store externally visible before one or more preceding descriptor writes. The device could then DMA from an old address, use a stale length, or observe incomplete flags.
dma_wmb() is appropriate because the shared descriptor is memory jointly visible to the CPU and DMA device. It also prevents the compiler from moving the preceding stores past the barrier, so a separate compiler barrier is not needed here.
WRITE_ONCE() is not a replacement for the barrier. It makes the status access a single deliberate compiler-visible access; it does not give the surrounding data release ordering on its own.
2. Descriptor publication before the MMIO doorbell
The doorbell is MMIO, not ordinary RAM. The DMA barrier above orders accesses within shared memory, but the documentation explicitly warns that dma_wmb() does not provide MMIO ordering guarantees.
The normal writel() accessor supplies the needed portable relationship for the common case: earlier writes to a coherent outbound DMA buffer are visible before the device sees the MMIO command that triggers the transfer.
That gives a concise interview explanation:
“I use
dma_wmb()before the descriptor ownership field because the device can inspect the descriptor as soon as ownership changes. I then use a normalwritel()to ring the doorbell because that accessor orders preceding memory writes before the MMIO trigger. A DMA barrier alone would not order the doorbell.”
Why not rely only on writel()?
If the hardware contract guarantees that the device cannot inspect the descriptor until it receives the doorbell, a normal writel() provides ordering from prior memory stores to the trigger. But many descriptor protocols make the ownership field independently meaningful: hardware can prefetch or scan descriptors once ownership changes. In that general protocol, the descriptor fields must be ordered before the ownership field itself, which is exactly the job of dma_wmb().
Treat ownership transfer and notification as separate protocol events:
- Publish a valid descriptor and give device ownership.
- Notify the device that new work exists.
The reverse path: device completion, then CPU consumption
Now reverse the direction. The device writes receive data and completion metadata, then returns a descriptor to CPU ownership.
if (READ_ONCE(desc->status) & DESC_CPU_OWN) {
/*
* The device's completion/ownership update must be observed
* before the CPU reads associated descriptor fields or payload.
*/
dma_rmb();
length = desc->length;
flags = desc->flags;
process_received_data(desc, length, flags);
}
The ordering argument is the mirror image of transmit:
- The device writes payload and descriptor fields.
- The device updates a status or ownership field to indicate completion.
- The CPU observes the status.
dma_rmb()prevents later CPU reads of the descriptor or payload from being performed or observed as if they happened before that status observation.
Without the read barrier, speculative loads could fetch descriptor fields or packet data before the CPU has established that the device completed its writes. The result can be a packet with a stale length, stale flags, or data from a previous buffer use.
This is an acquire-like ownership handoff:
The barrier belongs after observing completion and before consuming the associated data. Placing it before polling the status does not establish the required relationship.
Coherent DMA still needs ordering; non-coherent DMA also needs cache maintenance
“Coherent DMA” is frequently misunderstood as “no barriers required.” Coherency means the CPU and device can obtain each other’s updated contents without explicit cache flushes in the normal use case. It does not force the CPU to expose the stores in your desired order.
For example, both of these may be true:
- A device can eventually see every store to a coherent descriptor.
- The device can see the ownership bit before it sees the preceding descriptor fields.
The first is a coherency property. The second is an ordering failure. dma_wmb() fixes the latter.
For streaming mappings, such as reusable packet buffers, the DMA API establishes the cache-maintenance handoff in addition to the ownership protocol.
Read these parts of the Linux DMA API guide for the distinction between coherent descriptor memory and streaming packet buffers. The guide makes explicit that coherency and ordering are separate requirements.
In “Types of DMA mappings,” read from “Coherent DMA memory does not preclude the usage of proper memory barriers” through the ordering warning. Notice that coherent descriptor rings are a stated use case, yet barriers are still required when field order matters. Then find the paragraph beginning “If you need to use the same streaming DMA region multiple times” and read the streaming reuse rule, continuing through the following API guidance. Focus on the ownership phases implied by synchronization for the CPU and synchronization for the device.
A practical classification is:
| Memory and operation | What usually handles coherency | What still handles ordering |
|---|---|---|
| Coherent descriptor ring | Coherent DMA allocation and hardware interconnect | dma_wmb() before publishing ownership; dma_rmb() after observing completion |
| Streaming TX payload | DMA map or synchronization for device access | Descriptor or index publication ordering |
| Streaming RX payload | DMA synchronization for CPU access after completion | Completion observation before payload consumption |
| Non-coherent bare-metal buffer | Explicit cache clean before device reads; invalidate before CPU reads device writes | Architecture-appropriate CPU/device barrier |
| MMIO register | Device-memory mapping and I/O accessor | readl(), writel(), or a specifically justified I/O barrier |
For a streaming buffer reused across transfers, the high-level sequence is:
- CPU owns the buffer and fills it.
- The driver maps it, or synchronizes it for the device.
- The driver publishes a descriptor and hands ownership to the device.
- Hardware completes DMA and returns ownership.
- The driver synchronizes it for the CPU before reading device-written contents.
- CPU processes or refills the buffer.
Do not directly clean or invalidate cache lines in a Linux driver unless you are working in a tightly architecture-specific environment with a documented reason. The DMA mapping API exists so that driver code can remain correct across coherent and non-coherent platforms.
Also avoid CPU-written fields sharing a cache line with a device-written streaming buffer on an incoherent system. Cache maintenance occurs at cache-line granularity; cleaning a CPU field or invalidating a DMA buffer can unintentionally affect adjacent bytes in the same line.
MMIO ordering: ordinary accessors first, relaxed accessors deliberately
MMIO is not ordinary C memory. A peripheral register region can be mapped with different attributes, uses a bus fabric with its own ordering behavior, and may contain posted writes.
For portable Linux driver code mapped with ordinary ioremap()-style attributes:
| Accessor | Useful guarantee | Typical use |
|---|---|---|
writel() |
Orders previous normal-memory writes before the MMIO write; orders accesses to the same peripheral | Doorbell, DMA start command, control update that depends on prepared memory |
readl() |
Completes before subsequent normal-memory reads begin | Read DMA-completion register, then inspect a coherent inbound buffer |
writel_relaxed() |
Retains ordering to the same peripheral, but does not supply normal-memory or locking ordering guarantees | Independent setup registers where only peripheral-local order matters |
readl_relaxed() |
Retains ordering to the same peripheral, but not relative to normal memory | High-frequency status reads only when no subsequent memory consumption depends on the read |
A common efficient pattern is:
/* These writes configure registers on the same device. */
writel_relaxed(dma_addr, regs + DMA_ADDR);
writel_relaxed(stride, regs + DMA_STRIDE);
writel_relaxed(length, regs + DMA_LEN);
/* This is the command that starts DMA. */
writel(DMA_START, regs + DMA_COMMAND);
The relaxed configuration writes remain ordered relative to accesses to that same peripheral. The final normal writel() is intentionally stronger because it starts an operation that depends on the earlier memory and register setup.
The reverse completion pattern is equally important:
if (readl(regs + DMA_STATUS) & DMA_DONE)
consume_coherent_rx_buffer();
A normal readl() ensures the MMIO status read completes before later normal-memory reads begin. If you changed it to readl_relaxed(), you would need to independently establish the needed ordering before consuming DMA memory. Do not make that substitution merely because a relaxed accessor sounds faster.
This video segment explains why normal and relaxed accessors differ, then ties the rules directly to DMA descriptor processing.
Uh-oh, It's I/O Ordering! - Will Deacon, Arm
Watch “Uh-oh, It's I/O Ordering!” by Will Deacon from The Linux Foundation for a concise, systems-level explanation of Linux MMIO accessor guarantees. It is particularly useful for correcting the instinct to use a generic write barrier around every register access.
Watch standard accessors for the ordering supplied by readl() and writel(), especially their relationship to prior writes and later reads of DMA memory. Then watch relaxed accessors to see what is retained and what is deliberately dropped by _relaxed operations. Watch DMA read barriers for the valid-bit-before-payload pattern. Finish with the receive example, focusing on why a branch that checks a register value does not itself prevent speculative memory reads.
Ordering is not completion
A normal writel() orders the write, but a bus write may still be posted. If the device specification requires a write to have actually reached the peripheral before you continue, a read-back is a conventional completion mechanism:
writel(RESET_ASSERT, regs + CONTROL);
readl(regs + CONTROL); /* forces the preceding write to reach the device */
udelay(10);
writel(RESET_DEASSERT, regs + CONTROL);
The read-back matters when the delay must begin after the peripheral has observed the first write. A delay after a posted write alone may only delay the CPU while the write remains buffered somewhere in the interconnect.
Use this only when the device contract requires completion or a timing separation. A read-back can be expensive, so it is not a default substitute for understanding the ordering requirement.
Barrier selection: a compact driver review checklist
When reviewing an MMIO or DMA sequence, identify the handoff first, then select the narrowest correct mechanism.
CPU gives a coherent descriptor to a device
- Write descriptor address, length, flags, and other contents.
- Use
dma_wmb(). - Write the descriptor ownership or valid field.
- Use normal
writel()to ring the device doorbell.
Justification: data must precede device ownership; the ownership publication must precede device notification.
CPU consumes a coherent descriptor completed by a device
- Read the device-written completion or ownership field.
- If it shows CPU ownership, use
dma_rmb(). - Read descriptor contents and payload.
Justification: completion must be observed before any data consumption.
CPU hands a streaming buffer to a device
- Fill the buffer while CPU-owned.
- Use the DMA mapping API or synchronize for device.
- Publish the descriptor using the ownership protocol above.
- Do not touch the payload while device-owned.
Justification: the mapping API handles cache coherency; the descriptor protocol handles ordering and ownership.
CPU consumes a device-written streaming buffer
- Establish device completion through the status or ownership protocol.
- Synchronize for CPU as required by the DMA API.
- Read the data only while CPU-owned.
Justification: invalidating or synchronizing makes the device writes visible to the CPU; completion ordering prevents consuming a buffer before the device has finished publishing it.
Driver triggers a device through MMIO
- Use
writel()if the command depends on prior RAM writes or normal-memory visibility. - Use
writel_relaxed()only when peripheral-local ordering is enough. - Read back only if the hardware needs the previous write to have reached the endpoint before the next action.
Justification: MMIO ordering and posted-write completion are device-I/O concerns, not generic SMP synchronization concerns.
Common interview traps
“I put volatile on the descriptor fields.”
That constrains compiler treatment of those accesses, but does not provide the CPU-to-device ordering or cache-maintenance contract.
“The descriptor ring is coherent, so I do not need dma_wmb().”
Coherency makes updated contents observable; it does not require the device to observe fields in source order. Ownership publication still needs ordering.
“I used smp_wmb() before the doorbell.”
SMP barriers address CPU-to-CPU shared-memory ordering. They are not a substitute for the documented I/O semantics of writel() or another deliberate I/O-ordering mechanism.
“A DMA barrier flushes the cache.”
No. Barriers order accesses. On a non-coherent path, cache clean or invalidate operations, normally hidden behind DMA mapping and synchronization APIs, are needed as well.
“The completion if statement makes the payload read safe.”
No. A weakly ordered processor may speculatively begin independent reads. The completion observation needs the appropriate acquire-side ordering before data consumption.
Key takeaways
- A compiler barrier constrains generated code, not CPU or device behavior.
- A DMA memory barrier orders shared descriptor memory around an ownership or validity handoff.
- A normal MMIO accessor such as
writel()orreadl()provides documented ordering between normal memory and a device register;_relaxedaccessors intentionally omit much of that ordering. - Ordering is not cache coherency. Coherent memory can still require barriers; non-coherent DMA also requires cache maintenance, normally through DMA mapping and synchronization APIs.
- A transmit handoff is typically: fill descriptor
dma_wmb()publish ownershipwritel()doorbell. - A receive handoff is typically: observe completion
dma_rmb()consume descriptor and payload. - A read-back is for MMIO completion, not merely ordering, when the device specification requires a write to have reached hardware before proceeding.
You have now completed the concurrency and memory-ordering module. Next, the course moves into real-time design with priority inversion: how a low-priority task holding a resource can delay high-priority work, and how priority inheritance or priority ceiling bounds that delay.