xv6, line by line
lab 9
Lab 99 User threads: clone, join and futex

Lab 9 · reveal · 20 steps · 10 commits

User threads: clone, join and futex: the reference solution

Every xv6 process has exactly one thread: one page table, one trapframe, one kernel stack, one stream of instructions. In this lab you add threads. clone(fn, arg, stack) starts a new thread that runs in the caller’s address space, on a stack the caller allocated; join() waits for one to finish; futex_wait and futex_wake let threads sleep and wake each other on a word of shared memory; and a small user library builds thread_create, thread_join and a mutex on top of them.

Sharing one page table between several harts at once breaks assumptions that hold so quietly in this tree that nobody wrote them down. The trampoline finds the trapframe at a fixed address: where does the second thread’s trapframe go, and how does uservec, with every register full of user data, find it? Who frees a page table that four threads use, and when? What happens when two threads call sbrk, or touch the same lazily allocated page, on two harts at the same moment? What does exit mean now, and wait, and kill? And how do you put a thread to sleep until some user memory changes without losing the wakeup that arrives a moment too early?

The reference solution is ten small commits. With it, creating and joining a thread costs no page allocations at all (a fork of a 1 MiB process costs 266), and 2000 create-and-join pairs take 3 timer ticks where 2000 fork-and-wait pairs take 69.

Each step shows one change on the branch ext/09-threads, the code around it, and the state of the machine when that code runs.

The route
  1. 1The address space gets a structure of its own kernel/proc.h
  2. 2Allocate an address space, drop a reference kernel/proc.c
  3. 3uservec: one instruction to find this thread's trapframe kernel/trampoline.S
  4. 4userret: read it back, and leave it for next time kernel/trampoline.S
  5. 5The kernel decides which slot, just before leaving kernel/trap.c
  6. 6One trapframe page, eight slots kernel/memlayout.h
  7. 7The trapframe page is allocated and freed with the address space kernel/proc.c
  8. 8sharevm: one more user, in a free slot kernel/proc.c
  9. 9allocproc makes a process or a thread kernel/proc.c
  10. 10kclone sets up the new thread's registers kernel/proc.c
  11. 11wait and join each reap their own kind kernel/proc.c
  12. 12sbrk becomes one critical section kernel/proc.c
  13. 13A lazy fault that another thread already handled kernel/vm.c
  14. 14exec and fork see a still address space kernel/exec.c
  15. 15The first thread's exit ends them all kernel/proc.c
  16. 16futex_wait: register, then look kernel/futex.c
  17. 17futex_wake: wake up to n, including those not asleep yet kernel/futex.c
  18. 18A mutex in user space, the kernel only under contention user/uthread.c
  19. 19thread_create puts fn and arg on the new stack user/uthread.c
  20. 20The test that makes faults race user/threadtest.c

Keys: ← → step · Home start