xv6, line by line
tour 27
Tours27 The physical page allocator

Tour 27 · Memory · about 27 minutes · 15 steps

The physical page allocator

Every page table, every page of user memory, every kernel stack, trapframe and pipe buffer in xv6 comes from one place: a linked list of free 4096-byte pages managed by kalloc and kfree in kernel/kalloc.c. The whole allocator is about fifty lines. It has no size classes and no bitmaps, and it keeps no bookkeeping memory of its own: the list is stored inside the free pages themselves.

This tour watches the allocator twice. First at boot, when hart 0, alone and with paging still off, threads 32,735 pages onto the list one at a time (real numbers from this build’s kernel.sym). Then in a running system, at the moment when the shell’s fork on hart 1 asks for a page while a freshly exec’d echo on hart 2 gives pages back. Both harts reach for the same lock word and the same 8-byte head pointer, at the same instant.

The stakes are high and the failures are silent. If two harts interleave their list updates, nothing crashes right away: a page is lost forever, or it is handed to two owners who will overwrite each other’s data. A single spinlock, kmem.lock, is all that stands between the system and that bug, and the tour ends with what that one lock costs.

Best after: 3. main: one hart builds the kernel, the others wait, 15. Spinlocks from the hardware up, 24. The kernel page table and turning paging on

Who is running where

The tour has two acts.

Act 1, boot. Only hart 0 is doing real work. Harts 1 and 2 are spinning in main, waiting for hart 0 to set started.

Act 2, a running system. You typed echo hi &, then ls. When the act starts:

Hart What it is doing
0 Idle in its scheduler, or running whatever is runnable
1 The shell, sh (pid 2), inside fork for ls, copying its memory into the new child (pid 5)
2 The background echo (pid 4), finishing exec, freeing the copy of the shell’s memory it inherited

(This overlap is invented for illustration: in a real session echo hi finishes long before you can type ls. The tour pretends they coincide because this collision, one hart allocating while another frees, is exactly what kmem.lock exists for, and it happens all the time on a busy machine.)

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. 1Hart 0 sets up memory before anything else kernel/main.c
  2. 2The raw material: from end to PHYSTOP kernel/memlayout.h
  3. 3kinit hands every page to kfree kernel/kalloc.c
  4. 4The list is stored inside the free pages kernel/kalloc.c
  5. 5The checks in kfree kernel/kalloc.c
  6. 6Junk in, then push under the lock kernel/kalloc.c
  7. 7The first customer: the kernel's page table kernel/vm.c
  8. 8Act 2. The shell forks, with a lock already held kernel/proc.c
  9. 9uvmcopy asks for one page per page kernel/vm.c
  10. 10Meanwhile on hart 2, echo frees the shell's old image kernel/exec.c
  11. 11kfree on hart 2: junk first, lock second kernel/vm.c
  12. 12Both harts reach for kmem.lock kernel/spinlock.c
  13. 13The race the lock prevents kernel/kalloc.c
  14. 14kalloc pops, then fills with junk outside the lock kernel/kalloc.c
  15. 15The cost of one global lock kernel/kalloc.c

Keys: ← → step · Home start