Β§10.1–10.2Multiprocessor Architecture … Synchronization

Part I OSTEP pp. 103–106 Β· ~7 min read

  • multicore
  • cache coherence
  • bus snooping
  • temporal locality
  • spatial locality

The last stop in CPU virtualization: what changes when there’s more than one CPU? Plenty β€” and (per the book’s aside on advanced chapters) this material lands best after the concurrency piece, though it belongs here logically. The multicore era made the question unavoidable: architects couldn’t push single-CPU speed further without absurd power draw, so chips sprouted cores instead. One caveat up front: a typical single-threaded C program gets no faster on more CPUs β€” you must rewrite it to run in parallel (threads, Part II’s subject).

The Crux: How To Schedule Jobs On Multiple CPUs

How should the OS schedule jobs on multiple CPUs? What new problems arise? Do the same old techniques work, or are new ideas required?

10.1 Background: Multiprocessor Architecture

The fundamental difference between one CPU and many is what happens to hardware caches. A single-CPU system (Figure 10.1) keeps copies of popular data in small, fast caches, making the big slow memory appear fast β€” the first load of a value takes maybe 100 ns from memory; a repeat hit in cache takes a few:

Figure 10.1: single CPUCPUCacheMemoryFigure 10.2: two CPUs, shared memoryCPUCacheCPUCacheBusMemory

One cache is an optimization; two caches over one memory are a consistency problem waiting to happen.

Caches work because programs exhibit locality, in two flavors:

Temporal locality

Data touched now is likely touched again soon β€” think of a loop’s variables, or the loop’s own instructions, accessed over and over.

Spatial locality

After address x, addresses near x are likely next β€” a program streaming through an array, or executing instructions in sequence.

Now the tricky part (Figure 10.2): multiple CPUs, private caches, one shared memory. Step through what goes wrong:

The cache-coherence bug, step by step (write-back caches, no coherence protocol)
process CPU 1 cache A: D CPU 2 cache (empty) bus Memory address A: D

1Read D on CPU 1

A program on CPU 1 loads the value at address A. Cache miss β€” the system fetches D from main memory (slow: tens to hundreds of nanoseconds) and keeps a copy in CPU 1’s cache, betting on reuse.

step 1 / 4

That failure is the problem of cache coherence β€” a research field of its own. The basic solution lives in hardware: monitor memory accesses and preserve the illusion of a single shared memory. On a bus-based system, the classic technique is bus snooping : every cache watches the bus; when a CPU sees an update to data it holds, it either invalidates its copy or updates it in place. Write-back caches complicate the details β€” but the scheme’s spirit survives all the way to modern protocols.

10.2 Don’t Forget Synchronization

If hardware keeps caches coherent, can programs just access shared data freely? Alas, no β€” coherence makes reads see writes, but it does nothing to make multi-step updates safe. Concurrent access to shared structures still needs mutual exclusion (locks). Witness this innocent list deletion:

typedef struct __Node_t {
    int              value;
    struct __Node_t *next;
} Node_t;

int List_Pop() {
    Node_t *tmp = head;         // remember old head ...
    int value   = head->value;  // ... and its value
    head        = head->next;   // advance head to next pointer
    free(tmp);                  // free old head
    return value;               // return value at head
}

Figure 10.3: Simple List Delete Code

Two threads on two CPUs enter List_Pop() together. Both execute line 7 and capture the same head in their private tmp (it’s on each thread’s stack). Now both try to remove the same element: a double free of the head node, and possibly the same value returned twice β€” the same species of disaster as chapter 2’s broken counter, at data-structure scale. The fix: a mutex β€” lock(&m) at entry, unlock(&m) at exit β€” makes the routine correct. The cost: as CPU counts grow, synchronized structures get slow, a tension Part II confronts head-on.

Coherence handled by hardware, correctness handled by locks β€” one issue remains before we can build the scheduler itself: performance depends on where a process runs. That’s cache affinity, next.

Check yourself

1.Why did multiprocessors go mainstream β€” and why doesn't your single-threaded program benefit?

2.In the step-through: CPU 1 updates A to Dβ€² (write-back cache), the process migrates, and CPU 2 reads A. What does it get, and why?

3.How does bus snooping keep caches coherent?

4.Hardware keeps caches coherent. Why does List_Pop() still corrupt the list when two CPUs run it simultaneously?

5.A lock around List_Pop() fixes correctness. What worry does the chapter flag about this fix as CPU counts grow?

5 questions