1What does the checker reason about, a lock or a kind of lock?
There are 64 p->locks, 30 buffer locks, 50 inode locks, and a new lock for every pipe.
The checker will remember “B was acquired while A was held” and look for cycles. Should
A and B be individual locks, or groups of locks? If groups, how would you decide which
lock belongs to which group, using only what xv6 already has? What would each choice
catch, and what would it get wrong? Commit to an answer before the hints.
Look at the deadlock in Tour 52: Breaking the lock rules, break 6. kfork held one particular child’s
p->lock; kexit's wakeup went for that same slot. Would the same deadlock
need the same slot next time? And look at what initlock is given besides the lock.
A deadlock needs two paths with opposite orders. If they are judged only lock by lock,
both paths must have touched the same instances. How many instances are there of
p->lock, and how many runs would that take?
Groups. What does every lock already carry, from the moment it is created, that is
the same for all p->locks and different for wait_lock? Then check whether that
ever puts locks with different roles in one group.
The reference design
Groups, called classes. A class is the name given to initlock or
initsleeplock: proc for all 64 p->locks (one call in procinit), inode for all
50 inode sleep-locks, pipe for every pipe ever created. The reference keeps the class
number in the lock itself, in the four bytes of padding after locked, so no struct
changes size.
Classes are what make the checker useful. The kfork bug is “some p->lock, then
wait_lock” against “wait_lock, then some p->lock”. With classes, one fork and one
wait are enough to see both orders, whichever slots they used. Lock by lock, the checker
would need both paths to touch the same slot, and would also need a graph node for each of
156 spinlocks and 81 sleep-locks at boot, plus one for every pipe. Clinic 2 tried it: the
class table overflowed in procinit.
The price is over-approximation. A class graph says “inode before inode” when
create locks a directory and then a file in it; it cannot see that the two are always
a parent and its child. It says “itable before inode” for iput and “inode before
itable” for namex, and cannot see that iput only does this when no one else holds a
reference. Lock order is a property of instances; class graphs see less and report more.
Most of this lab is about what to do with those reports.
One more thing to notice: every sleep-lock’s inner spinlock is created by the same
initlock(&lk->lk, "sleep lock") call inside initsleeplock, so buffers, inodes and the
UART’s tx_lock share one inner class, sleep lock. Linux has the same issue with locks
initialised inside a helper, and fixes it by passing a class key in from the caller. In
this tree it causes no false report, so the reference leaves it.
Check yourself
The reference checker groups locks by the name passed to initlock. Which pair of
acquisitions would it treat as the same edge “proc before wait_lock”?
True or false: a graph of lock classes can report a cycle that can never deadlock.
Why?