xv6, line by line
tour 16
Tours16 sleep and wakeup, and the lost-wakeup problem

Tour 16 · Concurrency primitives · about 40 minutes · 22 steps

sleep and wakeup, and the lost-wakeup problem

You type cat README | wc at the shell. Two programs start at almost the same moment on two different harts. wc asks the pipe for data before cat has written anything, so wc must wait. A few microseconds later cat, on another hart, puts 512 bytes into the pipe, and wc must notice.

Waiting sounds simple. It is one of the most treacherous things a kernel does. The waiter checks a condition (“is the pipe empty?”), decides to sleep, and goes to sleep. If the waker makes the condition true and calls wakeup between the check and the sleep, the wakeup finds nobody asleep and is lost. The waiter then sleeps forever, next to a pipe full of data. This is the lost-wakeup problem (sleep and wakeup).

This tour follows wc into piperead and to sleep, and cat through pipewrite to the wakeup. Along the way you will see how this version of xv6 closes the gap with a two-step sleep_prepare / sleep protocol, how older xv6 closed it by handing a lock to sleep, why the UART driver can sleep with no condition lock at all, and why every caller wraps its sleep in a while loop.

Best after: 5. Life of a system call, 13. swtch and the lock handed across a context switch, 15. Spinlocks from the hardware up

Who is running where

The machine has three harts. The shell (pid 2) has forked pid 3 to run the pipeline, and pid 3 has forked cat (pid 4) and wc (pid 5). This is the first command since boot, so those are the pids. pid 2 and pid 3 are asleep in kwait.

Hart What it is doing
0 Idle: its scheduler finds nothing runnable and waits in wfi
1 Running cat (pid 4), which is reading README from the disk
2 Running wc (pid 5) in user mode, about to read from the empty pipe: the process this tour follows first

Both children share one struct pipe: cat’s file descriptor 1 is its write end, wc’s file descriptor 0 its read end.

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. 1wc asks for its first 512 bytes user/wc.c
  2. 2piperead takes the pipe lock and finds it empty kernel/pipe.c
  3. 3Register on the channel while still holding the condition lock kernel/proc.c
  4. 4How older xv6 did it, by handing the lock to sleep kernel/proc.c
  5. 5The gap between release and sleep kernel/pipe.c
  6. 6What the gap would do without registration kernel/pipe.c
  7. 7sleep, for real this time kernel/proc.c
  8. 8sched insists on exactly one lock kernel/proc.c
  9. 9Hart 2's scheduler lets go of wc kernel/proc.c
  10. 10cat arrives with data and takes the pipe lock kernel/pipe.c
  11. 11512 bytes, one copyin each kernel/pipe.c
  12. 12wakeup, with the condition lock still held kernel/pipe.c
  13. 13wakeup visits every process, under each one's lock kernel/proc.c
  14. 14cat's next write finds the pipe full kernel/pipe.c
  15. 15Hart 0 picks up wc kernel/proc.c
  16. 16sleep returns on a different hart kernel/proc.c
  17. 17Re-check the condition, then take the bytes kernel/pipe.c
  18. 18Why every sleep sits in a while loop kernel/pipe.c
  19. 19A kill is a wakeup with no event kernel/proc.c
  20. 20One channel, two conditions in begin_op kernel/log.c
  21. 21The UART sleeps with no condition lock kernel/uart.c
  22. 22What it took user/wc.c

Keys: ← → step · Home start