xv6, line by line
lab 21
Lab 2121 Lockdep-lite: a runtime lock-order checker

Lab 21 · reveal · 20 steps · 12 commits

Lockdep-lite: a runtime lock-order checker: the reference solution

Two harts deadlock when each holds a lock the other wants. Tour 52: Breaking the lock rules built one on purpose: a copy of the kernel whose kfork takes wait_lock while it still holds the new child’s p->lock. The bug froze the machine, but only under a stress test, and when it froze all three harts spun with interrupts off and printed nothing. In this lab you build a checker that finds that bug at boot, on the first run, before any deadlock, and prints the two code paths that disagree.

The idea is Linux’s lockdep, cut down to fit xv6. Every time a lock is acquired, note which locks are already held: an order “this before that”, an edge in a graph. A deadlock needs two paths that take the same two locks in opposite orders (or a longer ring of them), so the first acquisition that would close a cycle in that graph is a warning, even if the two paths have never run at the same time. The checker does not wait for the unlucky timing. It needs the paths only to run once each, in any order.

Every design decision is a question about xv6’s locks. What is a class, and what does a class graph get wrong? Whose locks are “held”: a hart’s or a process’s, when p->lock is acquired by the scheduler and released by the process on the other side of swtch, and a sleep-lock’s holder may wake up on another hart? How can the checker protect its own graph when it runs inside acquire? What about interrupt handlers? And what do you do when the checker, on its first real run, reports cycles in code that has never deadlocked and never will? This tree has three of them, all of them known (the concept page measured them), and the lab handles each one honestly: by writing down, in code the checker can use, the rule that makes it safe, and checking that rule at run time where possible.

The reference solution is twelve commits. With it, boot and usertests -q run with no report while the checker examines about 27 million acquisitions, at a cost of 16 to 38 percent in run time (measured while the computer was busy with other work; your times will differ).

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

The route
  1. 1A class number in the lock's padding kernel/spinlock.h
  2. 2The class table and a lock the checker can use kernel/lockdep.c
  3. 3The hook goes before the spin kernel/spinlock.c
  4. 4Where was it acquired? Walking the frame pointers kernel/lockdep.c
  5. 5Release searches, because p->lock changes threads kernel/lockdep.c
  6. 6Edges, and a fast path of one load kernel/lockdep.c
  7. 7A cycle is a path back kernel/lockdep.c
  8. 8The first report, with spinlocks alone kernel/lockdep.c
  9. 9Sleep-locks belong to the process kernel/sleeplock.c
  10. 10The next report: two inodes kernel/lockdep.c
  11. 11Interrupt handlers act for no process kernel/trap.c
  12. 12The interrupt rule, and why xv6 keeps it for free kernel/lockdep.c
  13. 13Roles inside one class kernel/lockdep.c
  14. 14create and unlink lock the file as a child kernel/sysfile.c
  15. 15Buffers nest by role too kernel/log.c
  16. 16A trylock records no order kernel/spinlock.c
  17. 17iput's acquisition, a checked claim kernel/fs.c
  18. 18Panic at a report kernel/lockdep.c
  19. 19Self-tests, on locks of their own kernel/lockdep.c
  20. 20lockdeptest user/lockdeptest.c

Keys: ← → step · Home start