xv6, line by line
tour 17
Tours17 Sleep-locks

Tour 17 · Concurrency primitives · about 41 minutes · 23 steps

Sleep-locks

You type ls | grep README. The shell’s child forks two processes, and a moment later both call exec at almost the same instant, on two different harts: ls (pid 4) on hart 1 and grep (pid 5) on hart 2. To load a program, kexec must first read the program file’s inode from disk. In this build ls is inode 10 and grep is inode 6, and with 16 inodes per 1024-byte block, both on-disk inodes live in the same disk block, block 33 (checked against fs.img: the inode area starts at block 33).

So two processes on two harts want the same disk block at the same time, and (we will suppose) the block is not in memory yet. One of them must read it from the disk, which takes a long time by CPU standards, while the other waits. The waiting must not burn a CPU, and the reader must be allowed to go to sleep in the middle of holding the block. A spinlock can do neither. This tour is about the lock that can: the sleep lock.

You will see a sleep-lock built out of a spinlock, a flag and sleep and wakeup; a process holding two sleep-locks while it sleeps; an interrupt on a third hart ending the wait; and the second process getting the block without touching the disk at all.

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

Who is running where

The machine has three harts. When the tour starts:

Hart What it is doing
0 Idle in its scheduler, with nothing to run
1 Running ls (pid 4), which has just entered exec("ls", ...)
2 Running grep (pid 5), which has just entered exec("grep", ...)

The shell (pid 2) waits for its child (pid 3), which runs the pipeline and waits for pids 4 and 5 (user/sh.c:121). The pids assume this is the first command since boot.

A supposition for this tour: block 33 is not in the 30-buffer buffer cache right now. In this build it usually is (an instrumented run of ls | grep README as the first command after boot finds block 33 already cached for both processes), so suppose other file activity has evicted it. That makes the interesting case happen: a real disk read while the other process waits. (If the block were cached, the two would still take turns on its sleep-lock, but nobody would touch the disk.)

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 programs start loading at once kernel/exec.c
  2. 2First collision, on the root directory kernel/fs.c
  3. 3Locking inode 10, which nobody has read yet kernel/fs.c
  4. 4bread, the buffer cache's front door kernel/bio.c
  5. 5bget searches under bcache.lock kernel/bio.c
  6. 6Claiming a buffer, then letting go of bcache.lock kernel/bio.c
  7. 7What a sleep-lock is made of kernel/sleeplock.h
  8. 8Hart 1 takes block 33's sleep-lock kernel/sleeplock.c
  9. 9Meanwhile, grep finds block 33 already claimed kernel/bio.c
  10. 10grep registers and lets go of the inner spinlock kernel/sleeplock.c
  11. 11grep goes to sleep, holding a sleep-lock of its own kernel/proc.c
  12. 12ls starts the disk read kernel/virtio_disk.c
  13. 13ls sleeps, holding two sleep-locks kernel/virtio_disk.c
  14. 14Why a spinlock holder may not sleep kernel/proc.c
  15. 15The disk interrupt, on hart 0 kernel/virtio_disk.c
  16. 16ls resumes, on a different hart kernel/virtio_disk.c
  17. 17ls copies its inode out of the buffer kernel/fs.c
  18. 18brelse checks that the caller really owns the buffer kernel/sleeplock.c
  19. 19releasesleep wakes grep kernel/sleeplock.c
  20. 20brelse puts the buffer back in the list kernel/bio.c
  21. 21grep wakes and checks again kernel/sleeplock.c
  22. 22grep gets the block without touching the disk kernel/bio.c
  23. 23Two kinds of lock, side by side kernel/sleeplock.h

Keys: ← → step · Home start