xv6, line by line
tour 18
Tours18 Lock ordering: how xv6 avoids deadlock

Tour 18 · Concurrency primitives · about 47 minutes · 23 steps

Lock ordering: how xv6 avoids deadlock

One lock can never deadlock a correct program. Two locks can. If hart 1 holds lock A and waits for B while hart 2 holds B and waits for A, both wait forever, and soon every other hart that touches A or B joins them. Nothing crashes, nothing prints. The machine simply stops making progress. This is a deadlock, and it is the price of having more than one lock.

xv6 has dozens of locks: one per process, one per inode, one per buffer, plus wait_lock, itable.lock, bcache.lock, log.lock, pipe locks, the disk lock and more. It has no deadlock detector. It avoids deadlock the classic way: almost every code path that holds two locks at once takes them in the same order, so a cycle of waiting can never close; the few exceptions (iput's, step 16) only ever take the second lock when it cannot be held by anyone else. The order is written down in exactly one comment (kernel/proc.c:26). Everywhere else it lives in the code, as a release placed before an acquire, a name refused, a lock dropped early.

This tour is a pilgrimage through those places. At each stop you see the rule, the line that obeys it, and a timeline of the deadlock that would happen if the line were written the other way. It ends with the whole partial order on one page.

Best after: 15. Spinlocks from the hardware up, 16. sleep and wakeup, and the lost-wakeup problem, 17. Sleep-locks

Who is running where

This tour visits many short scenes rather than following one process. The machine has three harts, and in every scene at least two of them are inside the kernel at once, usually on behalf of processes you would meet at an ordinary shell prompt: sh, the children of a pipeline cat README | wc, rm, ln, a program writing a file.

Hart Typical role in the scenes
0 Often a parent: the shell waiting for children, or a process walking a path
1 The process whose code path we are reading
2 The competitor: another process trying to take the same locks in the other order

Each step names its processes. Pids follow a freshly booted system: init is 1, sh is 2, the first command’s processes are 3, 4, 5, and so on.

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. 1Two locks, two harts, one cycle kernel/spinlock.c
  2. 2Rule 1: wait_lock before any p->lock kernel/proc.c
  3. 3kfork lets go of the child before touching the family tree kernel/proc.c
  4. 4kexit takes wait_lock, then wakes the parent kernel/proc.c
  5. 5kexit becomes a zombie holding both kernel/proc.c
  6. 6kwait holds wait_lock, then each child's lock kernel/proc.c
  7. 7Rule 2: begin_op before any inode lock kernel/file.c
  8. 8Why begin_op cannot come second kernel/log.c
  9. 9Rule 3: parent directory before child, in unlink kernel/sysfile.c
  10. 10unlink refuses "." and ".." kernel/sysfile.c
  11. 11create, the new-file path, parent then child kernel/sysfile.c
  12. 12create, the existing-name path, lets go of the parent first kernel/sysfile.c
  13. 13Rule 4: namex holds one inode lock at a time kernel/fs.c
  14. 14A sleep-lock cannot catch you waiting for yourself kernel/sleeplock.c
  15. 15Rule 5: an inode lock may be held while taking itable.lock kernel/fs.c
  16. 16iput bends the rule, carefully kernel/fs.c
  17. 17Rule 6: never hold bcache.lock while waiting for a buffer kernel/bio.c
  18. 18brelse lets go in the same spirit kernel/bio.c
  19. 19Rule 7: sys_link lets go of the file before finding the new directory kernel/sysfile.c
  20. 20Rule 8: a pipe's lock before any p->lock kernel/pipe.c
  21. 21sched enforces "only p->lock" kernel/proc.c
  22. 22Leaves at the bottom of the order kernel/log.c
  23. 23The whole order on one page kernel/proc.c

Keys: ← → step · Home start