xv6, line by line
tour 15
Tours15 Spinlocks from the hardware up

Tour 15 · Concurrency primitives · about 34 minutes · 18 steps

Spinlocks from the hardware up

Two harts want the same page of memory at the same moment. There is one list of free pages in the whole machine, kmem.freelist, and if both harts pop it at once they can both walk away with the same page. Two processes would then scribble over each other’s memory, and nothing would crash until much later, somewhere unrelated.

This tour follows one call to acquire(&kmem.lock) on hart 1 while hart 2 wants the same lock, down to the individual instructions the compiler produced (quoted from kernel/kernel.asm of this build). You will see interrupts switched off with one csrrci, a nesting counter that remembers whether to switch them back on, the amoswap.w.aq that decides who wins, the spin of the loser, and the fence rw,w; sw zero that hands the lock over.

Every other synchronization mechanism in xv6 (sleep-locks, sleep and wakeup, the context switch) is built on these two functions. If you understand why each instruction in them is there, the rest of the concurrency tours (Tour 16: sleep and wakeup, and the lost-wakeup problem to Tour 19: Memory ordering across harts) are variations on a theme.

Best after: 5. Life of a system call, 11. From a timer tick to a context switch

Who is running where

The machine has three harts. You pasted two lines at once, forktest & and echo hi, so forktest (pid 4) is still forking child after child in the background when the shell starts on echo hi. (If the process table were completely full at that instant, the shell’s own fork would fail.) When the tour starts:

Hart What it is doing
0 Running whatever else is runnable, or idle in its scheduler
1 The shell’s new child is parsing echo hi. Its first malloc needs heap, so it is in sbrk: the thread this tour follows
2 forktest is inside fork, setting up a new child: it will need a page too

Neither hart holds any spinlock when the tour begins, except that hart 2 is about to take one (the new child’s p->lock) before it asks for memory.

Three harts are running. This tour follows one path through the code, but the machine has three CPUs executing at the same time. Watch the locks held display at the top of each step, and read the Meanwhile, on other harts boxes: they show what the other CPUs could be doing at that very moment.
The route
  1. 1The shell's child needs sixteen pages kernel/vm.c
  2. 2One free list for the whole machine kernel/kalloc.c
  3. 3A lock is one word kernel/spinlock.h
  4. 4acquire starts by switching interrupts off kernel/spinlock.c
  5. 5push_off: one instruction reads and clears SIE kernel/spinlock.c
  6. 6Meanwhile hart 2 nests a second lock kernel/proc.c
  7. 7The deadlock push_off prevents: an interrupt on the same hart kernel/virtio_disk.c
  8. 8holding(): a hart waiting for itself kernel/spinlock.c
  9. 9amoswap.w.aq decides who wins kernel/spinlock.c
  10. 10Hart 2 pops the list while hart 1 spins kernel/kalloc.c
  11. 11Hart 2 releases: fence rw,w, then sw zero kernel/spinlock.c
  12. 12Hart 2's pop_off: noff 2 → 1, interrupts stay off kernel/spinlock.c
  13. 13Hart 1 gets the lock: what .aq promises kernel/spinlock.c
  14. 14Hart 1's turn at the list kernel/kalloc.c
  15. 15Release, interrupts back on, junk outside the lock kernel/kalloc.c
  16. 16Hart 2 releases the child, and its interrupts come back kernel/proc.c
  17. 17The one lock held across a context switch kernel/proc.c
  18. 18What a spinlock is made of kernel/spinlock.c

Keys: ← → step · Home start