xv6, line by line
tour 49
Tours49 Every lock in one ls | wc

Tour 49 · Locks and interrupt state · about 36 minutes · 21 steps

Every lock in one ls | wc

Tour 40: Capstone: the shell running ls | wc drove ls | wc from the keyboard to the answer. This tour drives it again and counts every lock on the way: every acquire of a spinlock, every acquiresleep of a sleep-lock, and every extra level of push_off that is not a lock at all. The result is a census of one real command.

We booted the unmodified kernel of this build on three harts in QEMU, waited for the $ prompt, attached gdb, and put a logging breakpoint on acquire, release, acquiresleep, releasesleep, myproc, uartputc_sync, devintr, syscall, swtch, sleep, sleep_prepare and wakeup. Each hit recorded the hart, the process, noff, the caller and the lock. Then we typed ls | wc, and cut the log at the shell’s write of the next $ : 313,522 events, of which 153,156 are spinlock acquisitions, for 1,227 system calls.

Lock Acquired Harts In interrupt handlers Deepest noff
p->lock (64 of them) 148,476 0, 1, 2 7,616 2
lk->lk inside sleep-locks 1,900 0, 1, 2 0 1
pi->lock 1,100 0, 1, 2 0 1
bcache.lock 914 0, 1, 2 0 1
kmem.lock 204 0, 1, 2 0 3
log.lock 164 0, 1, 2 0 1
itable.lock 162 0, 1, 2 0 2
tickslock 97 0 only 97 1
ftable.lock 84 0, 1, 2 0 2
disk.vdisk_lock 24 0, 1, 2 8 1
cons.lock 16 1, 2 8 1
wait_lock 12 0, 1, 2 0 1
pid_lock 3 0, 1 0 2
pr.lock 0

Sleep-locks: 637 acquiresleep calls, 457 on buffers, 168 on inodes, 12 on tx_lock. Exactly one found its lock taken.

Three facts stand out. 97% of the acquisitions are p->lock, four in five of them from wakeup, which locks all 64 process slots every time. Only four kinds of lock were taken inside an interrupt handler. And the deepest nesting is three, reached 47 times, in one function. The steps below show where each row comes from. All numbers are from one run; a second run, counted to the same point, gave 142,882 acquisitions, the difference almost all p->lock (ticks and idle scans vary). Tracing slowed the machine a great deal (208 seconds of wall time from Enter to the next prompt), so everything driven by the timer is inflated.

Best after: 13. swtch and the lock handed across a context switch, 15. Spinlocks from the hardware up, 16. sleep and wakeup, and the lost-wakeup problem, 17. Sleep-locks, 18. Lock ordering: how xv6 avoids deadlock, 40. Capstone: the shell running ls | wc

Who is running where

When the log starts, the shell has printed $ and nothing is running:

Hart What it is doing
0 Idle in its scheduler, in wfi between timer ticks
1 Idle in its scheduler
2 Idle in its scheduler

sh (pid 2) is asleep in consoleread on &cons.r; init (pid 1, slot proc[0]) is asleep in kwait. The cast, with the slot whose p->lock guards each one:

pid Program Slot p->lock address
2 sh proc[1] 0x8000ff38
3 sh (runs the pipeline) proc[2] 0x800100a0
4 sh → ls proc[3] 0x80010208
5 sh → wc proc[4] 0x80010370
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. 1153,156 acquisitions, and who made them kernel/spinlock.c
  2. 2You press Enter, and cons.lock is taken in an interrupt kernel/console.c
  3. 3The shell drains the line, and killed() takes p->lock twice per call kernel/console.c
  4. 4fork: allocproc locks slots until it finds an empty one kernel/proc.c
  5. 5kfork keeps the child locked through the copy kernel/proc.c
  6. 6wait: wait_lock, each child's lock, then sleep kernel/proc.c
  7. 7The scheduler takes a lock that someone else will release kernel/proc.c
  8. 8pid 3: a pipe, and a lock that did not exist a moment ago kernel/pipe.c
  9. 9begin_op and end_op: log.lock, and 54 empty commits kernel/log.c
  10. 10A sleep-lock is a spinlock, a flag and a pid kernel/sleeplock.c
  11. 11wc meets ls on the root directory: the one contended sleep-lock kernel/sleeplock.c
  12. 12bget: bcache.lock, then a buffer's sleep-lock, 418 times on one block kernel/bio.c
  13. 13A disk read, and a wakeup that arrives before the sleep kernel/virtio_disk.c
  14. 14The disk interrupt on an idle hart, and p->lock in interrupt context kernel/virtio_disk.c
  15. 15ls writes one byte into the pipe, under pi->lock kernel/pipe.c
  16. 16wc reads, and sleeps 53 times kernel/pipe.c
  17. 17wc prints: tx_lock, eleven times, never slept on kernel/uart.c
  18. 18A tick on hart 0, taken the moment pi->lock was released kernel/trap.c
  19. 19ls exits: wait_lock, then its own p->lock, then sched kernel/proc.c
  20. 20kwait frees ls: noff 3, the deepest of the run kernel/proc.c
  21. 21Where the acquisitions went kernel/proc.c

Keys: ← → step · Home start