Section 8.1

Hardware, Coherence & OS Models

Multiprocessor architectures, UMA versus NUMA topologies, bus-snooping coherence protocols, operating system concurrency models, and hardware atomics.

8.1.1 Multiprocessor Hardware & Cache Coherence

Modern multiprocessing systems organize multiple execution cores around shared physical memory. Depending on how memory is interconnected and how bus bandwidth scales, architectures fall into two primary hardware paradigms:

1. Uniform Memory Access (UMA)

In a Uniform Memory Access (UMA) architecture, all processors share a common system bus or crossbar switch connected to a centralized memory bank. Every CPU core experiences identical memory latency regardless of which core initiates the read or write request. While simple to program, UMA suffers from severe scalability limits: as core counts scale beyond 8–16 processors, the shared interconnect bus saturates entirely under memory bandwidth contention.

2. Non-Uniform Memory Access (NUMA)

To bypass UMA bus saturation, modern multi-socket servers and high-performance clusters utilize Non-Uniform Memory Access (NUMA). Physical memory is partitioned and distributed locally across processor nodes connected via a high-speed interconnect fabric (such as AMD Infinity Fabric or Intel Ultra Path Interconnect - UPI).

3. Cache-Coherent NUMA (ccNUMA) & Directory-Based Protocols

While snooping protocols (like MESI) broadcast every cache event across a shared bus, snooping does not scale beyond a small number of cores because the broadcast bus itself becomes a bottleneck. To solve this for large-scale NUMA systems, computer architects developed directory-based cache coherence (ccNUMA).

Pioneered academically by Anant Agarwal and his research team with the Stanford DASH (Directory Architecture for Shared Memory) project in the early 1990s, directory-based protocols eliminate broadcast snooping. Instead, a centralized or distributed directory maintains the sharing status of every memory block. When a core requests a cache line, it queries the directory managing that specific physical address block, which then directs point-to-point invalidation messages solely to the nodes actively caching that line.

The Cache Coherence Problem: Because CPU execution cores operate orders of magnitude faster than main memory, each core relies on high-speed private caches (L1 and L2). If CPU 0 modifies a variable in its private cache, CPU 1's private cache immediately becomes stale. To preserve memory consistency without routing every access back to slow main RAM, hardware controllers monitor memory lines using coherence protocols.

Guided Walkthrough: MESI Cache Coherence Lifecycle Stage 1 of 4

Concept Overview: This walkthrough animates how snooping hardware tracks state transitions across the 4 MESI states (Modified, Exclusive, Shared, Invalid) and forces cache-to-cache interventions when dirty lines are referenced.

Action Required: Click 'Execute Step 1: CPU 0 Reads X' below.
Initially, neither CPU cache holds Variable X (both are in the Invalid state). When CPU 0 reads X, it emits a BusRd broadcast. Because no other cache holds the line, it is loaded from RAM directly into CPU 0's cache in the Exclusive (E) state.
CPU Core 0 State: I Var X: -- Private L1/L2 Cache Array CPU Core 1 State: I Var X: -- Private L1/L2 Cache Array Shared System Memory Bus (Snoop Line Idle) Shared Main Memory (DRAM) Address 0x1000 [Var X] = 42 (Clean)
CPU 0 Cache: Invalid (I)
CPU 1 Cache: Invalid (I)
Bus Activity: Idle
RAM State: Clean (X = 42)
-- Main Memory initialized with Variable X = 42. Both caches are empty (Invalid) --

8.1.2 Multiprocessor Operating System Models

Structuring an operating system to manage multiple concurrent execution cores requires fundamental architectural decisions regarding kernel data structure placement, reentrancy, and synchronization boundaries. Computer architects have historically organized multiprocessor operating systems into three distinct models:

1. Each CPU Has Its Own Operating System (Static Partitioning)

Partitions physical memory into N disjoint regions for N processors. Each core executes an independent, private copy of the operating system kernel with its own run queue, process table, and buffer cache, eliminating kernel locking contention but preventing load balancing across cores.

2. Master-Slave Multiprocessors (Asymmetric Multiprocessing - ASMP)

Dedicates a single processor (the master, CPU 0) to executing the kernel, device drivers, and I/O interrupts. All other processors (slaves) execute user-space applications exclusively, creating a severe bottleneck when slave cores scale and flood the master with system call traps.

3. Symmetric Multiprocessing (SMP)

A single unified operating system image resides in shared physical memory. Every core can execute kernel code, service hardware interrupts, and schedule threads concurrently. Modern SMP kernels use fine-grained subsystem locking (per-core run queues, lockless RCU lists) to achieve true parallel performance.

Guided Walkthrough: Multiprocessor OS Concurrency Models Model 1 of 3

Concept Overview: This walkthrough visually contrasts Partitioned Silos, Master-Slave bottlenecks, and Symmetric Multiprocessing (SMP) reentrancy.

Action Required: Click 'Inspect Model 1: Partitioned Silos' below.
Observe 4 CPUs running independent operating system copies. Notice that CPU 0 is severely backlogged with 4 runnable processes, while CPUs 1, 2, and 3 are completely idle because run queues cannot cross partition boundaries.
CPU 0 Independent OS Queue: 4 Tasks CPU 1 Independent OS Queue: [Idle] CPU 2 Independent OS Queue: [Idle] CPU 3 Independent OS Queue: [Idle] Private RAM 0 Private Kernel Code Private Run Queue Severe Backlog (4) Private RAM 1 Private Kernel Code Private Run Queue Starving (Idle) Private RAM 2 Private Kernel Code Private Run Queue Starving (Idle) Private RAM 3 Private Kernel Code Private Run Queue Starving (Idle)
Active Architecture: Partitioned OS Silos
Kernel Reentrancy: None (Separate Kernels)
Synchronization: No Locks Required
Load Balance: Severe Imbalance
-- Partitioned OS model active. CPU 0 is overloaded while peer cores sit idle. Click button above to step --

8.1.3 Multiprocessor Synchronization & Cache Contention

On a uniprocessor system, mutual exclusion can be achieved simply by disabling hardware interrupts to prevent timer preemption. On a multiprocessor, disabling interrupts on Core 0 has zero effect on Core 1, which continues executing instructions concurrently.

1. Hardware Atomic Primitives

If an operating system attempts to implement a mutual exclusion lock using standard, non-atomic software read and write instructions, a race condition occurs whenever two cores attempt to claim the lock simultaneously:

// NON-ATOMIC SOFTWARE FAILURE: Race window between read and write int Naive_Acquire(volatile int *lock) { if (*lock == 0) { // Core 0 and Core 1 can both read 0 simultaneously *lock = 1; return 1; // Both cores believe they acquired the lock } return 0; }

To eliminate this vulnerability, processor architectures provide Atomic Read-Modify-Write (RMW) Instructions. These instructions guarantee that reading the old value and writing the new value occur as an indivisible, single-cycle operation across the entire memory hierarchy:

// 1. ATOMIC TEST-AND-SET (x86-64: LOCK XCHG) // Atomically writes 1 to *lock and returns the previous value in a single bus transaction int Atomic_TestAndSet(volatile int *lock) { int old_val; __asm__ volatile("lock xchgl %0, %1" : "=r"(old_val), "+m"(*lock) : "0"(1) : "memory"); return old_val; // Returns 0 on success, 1 on busy } // 2. ATOMIC COMPARE-AND-SWAP (x86-64: LOCK CMPXCHG) // Updates *addr to new_val ONLY IF it currently holds expected_val bool Atomic_CompareAndSwap(volatile int *addr, int expected_val, int new_val) { unsigned char success; __asm__ volatile("lock cmpxchgl %3, %1;\n\t" "sete %0" : "=q"(success), "+m"(*addr), "+a"(expected_val) : "r"(new_val) : "memory"); return (bool)success; }
Guided Walkthrough: Software Race Conditions vs. Hardware Atomics Stage 1 of 3

Concept Overview: This walkthrough visually demonstrates the race window between CPU cores. Observe why ordinary load-and-store instructions fail to prevent simultaneous lock acquisition, and how hardware atomic serialization enforces mutual exclusion.

Action Required: Click 'Simulate Stage 1: Non-Atomic Software Race' below.
Watch Core 0 and Core 1 attempt to acquire a lock using standard read-then-write code. Notice that both cores read 0 before either can write 1, causing both to claim the lock simultaneously.
Core 0 Execution Pipeline Instruction: IDLE Register EAX: -- | Result: -- Core 1 Execution Pipeline Instruction: IDLE Register EAX: -- | Result: -- LOCK# Signal Inactive Memory Address 0x9000 (*lock) Current Value: 0 (Unlocked)
Execution Mode: Non-Atomic Software
Core 0 Outcome: Awaiting Run
Core 1 Outcome: Awaiting Run
Mutual Exclusion: Pending
-- Select a simulation stage above to evaluate atomicity across cores --

2. The Cache-Line Bouncing Hazard

While spinlocks avoid the expensive latency of a context switch for short critical sections, naive spinlock implementations cause severe hardware degradation through cache-line bouncing.

// NAIVE SPINLOCK: Blindly issues atomic writes on every iteration void Naive_Spinlock_Acquire(volatile int *lock) { // CRITICAL FLAW: TestAndSet is an atomic write instruction. // It forces the cache line to the Modified state on EVERY check! while (Atomic_TestAndSet(lock) == 1) { // Spins continuously, spamming BusUpgr invalidations across the bus } }
The Invalidation Storm Mechanics: When Core 0 holds the lock, its private cache holds the lock variable in the Modified (M) state. When Cores 1, 2, and 3 simultaneously execute Atomic_TestAndSet(), each core's cache controller must obtain the line in the Modified state, broadcasting a BusUpgr or BusRdX invalidation across the bus. The cache line bounces furiously between cores on every clock cycle, saturating memory bandwidth.

3. Optimization: Test-and-Test-and-Set (TTAS)

To prevent memory bus saturation, lock designers utilize Test-and-Test-and-Set (TTAS). Instead of blindly issuing atomic write instructions on every iteration, a spinning CPU first inspects the lock using ordinary, non-atomic memory read instructions:

// TEST-AND-TEST-AND-SET (TTAS) SPINLOCK: Silent local cache spinning void TTAS_Spinlock_Acquire(volatile int *lock) { for (;;) { // Inner Test Phase: Read-only check. Spins locally in the Shared (S) state. // Generates ZERO bus traffic while the lock remains held by another core! while (*lock == 1) { __asm__ volatile("pause" ::: "memory"); } // Outer Test-and-Set Phase: Only issued when the lock is observed to be 0 if (Atomic_TestAndSet(lock) == 0) { return; // Successfully acquired exclusive ownership } } } // RELEASE: Writing 0 invalidates peer cache lines and relinquishes ownership void TTAS_Spinlock_Release(volatile int *lock) { *lock = 0; }
Guided Walkthrough: Spinlocks & Cache-Line Bouncing Stage 1 of 3

Concept Overview: A spinlock is designed for low latency, but naive implementations can cripple multiprocessor performance through cache thrashing. This walkthrough demonstrates the physical reality of cache coherence during synchronization.

Action Required: Click 'Simulate Stage 1: Naive Spinlock Bouncing Storm' below.
CPU 0 holds the lock while executing a critical section. Watch CPUs 1, 2, and 3 simultaneously execute naive atomic TestAndSet writes. Notice how the lock cache line bounces constantly between Modified and Invalid across cores.
CPU 0 (Holder) Inside Lock (M) Critical Section Active CPU 1 (Contender) Cache: I Awaiting Run CPU 2 (Contender) Cache: I Awaiting Run CPU 3 (Contender) Cache: I Awaiting Run Shared Memory Bus: Idle Lock Variable (Mutex Memory Line): Address 0x9000 = 1 [LOCKED by CPU 0] Bus Arbitration Metric: 0% Traffic Select a simulation stage above to observe cache coherence during lock contention.
Lock Owner: CPU 0
Spin Strategy: Awaiting Simulation
Cache Line State: --
Bus Impact: --
-- Synchronization walkthrough ready. Click button above to begin --