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 multicore Multiple CPU cores on one chip β the reason multiprocessors went mainstream when single CPUs hit the power wall. defined in ch. 10 β open in glossary 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:
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 temporal locality Data accessed now is likely accessed again soon (loop variables, instructions). defined in ch. 10 β open in glossary
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 spatial locality After touching address x, nearby addresses are likely next (array streaming, sequential code). defined in ch. 10 β open in glossary
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:
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.
That failure is the problem of cache coherence cache coherence The problem of keeping per-CPU caches consistent with the shared-memory illusion (stale-D vs updated-Dβ²); solved in hardware. defined in ch. 10 β open in glossary β 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 bus snooping Each cache watches the shared bus and invalidates/updates its copy when it sees a write to data it holds. defined in ch. 10 β open in glossary : 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?