xv6, line by line
tour 30
Tours30 The buffer cache

Tour 30 · File system · about 30 minutes · 19 steps

The buffer cache

xv6 keeps exactly 30 disk blocks in memory at a time. Every file-system operation, on every hart, goes through those 30 buffers: reading a directory, allocating a block, updating an inode, writing the log. The buffer cache in kernel/bio.c has two jobs. It saves disk reads, and, just as important, it guarantees that there is at most one copy of each block in memory, and that only one process at a time works on it.

This tour follows the moment that tests both jobs. You run logstress f1 f2, a test program whose children write two files in parallel. Both children need a new data block at the same time, so both call balloc, and both ask for block 46, the free-block bitmap of a fresh fs.img in this build, which nobody has read since boot. (The exact overlap is illustrative: it is one possible timing, chosen because it is the case the code must handle.) Child A on hart 1 gets there first and misses; child B on hart 2 arrives a moment later and finds A’s half-filled buffer. Two locks of different kinds and a reference count, updated in a carefully chosen order, make sure that the bitmap is read from disk once, that the two children take turns, and that the buffer cannot be recycled under either of them.

On the way you will see the LRU list, how the log pins buffers, and the one way the cache can fail: panic("bget: no buffers").

Best after: 17. Sleep-locks, 29. A disk read, end to end

Who is running where

logstress f1 f2 (pid 3) has forked two children. Each opened its file and is in its first write. Both are inside a log transaction (Tour 31: The log: begin_op, commit and group commit).

Hart What it is doing
0 Idle, or running logstress (pid 3), which is waiting for its children
1 Child A (pid 4), writing f1 (inode 24): needs a new block, about to read the bitmap
2 Child B (pid 5), writing f2 (inode 25): about to do the same, a moment later

(Simplified: in a traced run of this build, pid 4 did read block 46 from disk first; the exact overlap with pid 5 shown here is one possible timing, chosen because it is the one the code must handle.)

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. 1Both children need a free block kernel/fs.c
  2. 2Thirty buffers and a list kernel/bio.c
  3. 3What a buffer knows kernel/buf.h
  4. 4At boot, the list is built backwards kernel/bio.c
  5. 5Child A scans the cache kernel/bio.c
  6. 6A miss recycles the least recently used free buffer kernel/bio.c
  7. 7Child B finds A's buffer kernel/bio.c
  8. 8Child B sleeps on the buffer's lock kernel/sleeplock.c
  9. 9Child A reads the bitmap from disk kernel/bio.c
  10. 10Child A takes a block and pins the bitmap kernel/bio.c
  11. 11Child A releases the sleep-lock first kernel/sleeplock.c
  12. 12refcnt goes down, but the buffer stays put kernel/bio.c
  13. 13Child B wakes to a valid buffer kernel/bio.c
  14. 14Child B releases; the pin keeps the buffer kernel/bio.c
  15. 15Why a buffer with refcnt 0 is never locked kernel/bio.c
  16. 16The commit unpins and the buffer moves to the head kernel/log.c
  17. 17LRU in action kernel/bio.c
  18. 18When every buffer is busy kernel/bio.c
  19. 19What the cache bought kernel/bio.c

Keys: ← → step · Home start