Lock order
Every thread that acquires two or more ranked locks must take them in strictly increasing rank order. Ranks are per lock class and spaced by ten, so a new lock can be slotted in without renumbering.
The authoritative table lives in
doc/invariants/lock-order.md
and is updated in the same commit that adds or moves a lock.
Picking the primitive
Section titled “Picking the primitive”Order is only half the problem. A spin lock is bounded only while its holder keeps
running, and preemption is involuntary, so a bare spin::Mutex guard can be held by a
descheduled thread while every other CPU burns cycles behind it.
| Primitive | Use for |
|---|---|
IrqSpinlock |
state an interrupt handler can reach; disables interrupts |
PreemptSpinlock / PreemptRwLock |
everything else shared between threads |
BlockingMutex / BlockingRwLock |
anything held across I/O or a park |
Preempt* suppresses preemption rather than interrupts, which is what a lock held across
real work needs: walking page tables or the VMA tree with interrupts off would charge that
work to interrupt latency. Nothing reachable under a Preempt* guard may park, and
thread_park, thread_sleep and thread_yield debug-assert on it.
The ladder
Section titled “The ladder” 10 VFS mount registry 30 inode.lock 35 dentry cache 40 page cache 42 in-flight 50 dirty keys 60 inode.mappers 70 VMAs 80 memory manager 90 shared memory registry 100 dirty inodes 110 block page cache shards 120 journal map 130 checkpoint tracker 140 block write lock 150 journal state 160 EFS mutable state 170 AHCI legacy 180 slot/NCQ waiters 190 MMIO 200 PCI config 204 mailbox queue 206 mailbox response 210 TTY 220 pipe 230 pty 240 net stack 250 port table 260 socket 270 connection 280 window registry 290 window events 300 mouse buttons 310 broadcasters 320 device poller lists 330 HDA playback 340 devfs registry 900 kernel mapper 910 frame allocatorAcquiring
Section titled “Acquiring”Go through the macros, so the debug tracker sees the acquisition:
let g = ranked_lock!(RANK_EFS_MUTABLE, "efs::alloc_block", driver.mutable);let g = ranked_read!(RANK_VFS, "VFS", VFS);let g = ranked_write!(RANK_VFS, "vfs::mount", VFS);let g = ranked_lock_same!(RANK_INODE, "vfs::rename", inode.lock);ranked_lock_same! is for two locks of the same class, where the caller orders them by
key, renaming across two directories for instance.
Rules that keep coming back as bugs
Section titled “Rules that keep coming back as bugs”- Page-cache locks (40) and block-cache shards (110) are never held across disk I/O. Drop them before calling a fill function.
inode.lockis released before any I/O the VFS performs on behalf of a write.- Same-rank locks of different classes are never co-held.
- Some locks are deliberately unranked (
owned_ops,WaitQueue.inner, the scheduler’s run queue) with the rationale written down. Do not rank them without reading it: the scheduler’s own locks are taken in short interrupt-disabled sections, and wrapping them would recurse into the preemption counter.
Ranking is not only about deadlock
Section titled “Ranking is not only about deadlock”A guard on an unranked lock is invisible to assert_no_guards_held, which
thread_exit calls. So ranking an otherwise-leaf lock is worth doing purely to make a
dying thread’s held guards detectable.
Checking it
Section titled “Checking it”The tracker counts violations at /proc/lock_order_stats. Two build features exercise it:
cargo build --features lock-order-self-testcargo build --features lock-order-self-test-inversion # panics by designmake run-single # run the inversion on one corelockordertest in /bin drives it from userspace.