xv6, line by line
lab 19
Lab 1919 A hashed buffer cache

Lab 19 · reveal · 15 steps · 5 commits

A hashed buffer cache: the reference solution

In this tree every bread and brelse, on every hart, takes one spinlock: bcache.lock (kernel/bio.c:26). While one hart scans the 30 buffers for a block, every other hart that wants any block spins. In this lab you split that lock: the buffers go into a hash table of 13 buckets, each with its own lock, so that harts working on different blocks stop waiting for each other.

Splitting a lock sounds like a mechanical change. It is not, because the old lock did more than one job. What exactly did it protect, and which of those jobs can be cut into pieces? The cache recycles the least recently used buffer: what does “least recently used” mean once there is no single list? A miss has to find a free buffer that may sit in another bucket and move it: which locks does that take, in which order, while two other harts do the same? What happens when two harts miss on the same block at the same instant? And afterwards, how do you show that the split helped, and where did the waiting go? The think section asks these questions in the order a designer meets them, and the clinic shows what the tempting wrong answers did on real runs.

The reference solution is five small commits. Measured on three harts with bcachetest, the cache’s locks made a hart wait about 105 times per run instead of about 360, with the same hit rate as before; and the measurement turned up one surprise on the way, which the last commit fixes.

Each step shows one change on the branch ext/19-bcache-hash, the code around it, and the state of the machine when that code runs.

The route
  1. 1A test that makes three harts miss on one block user/bcachetest.c
  2. 2A time stamp in every buffer kernel/buf.h
  3. 3brelse stamps the buffer instead of moving it kernel/bio.c
  4. 4A miss picks the oldest stamp kernel/bio.c
  5. 5Thirteen buckets kernel/bio.c
  6. 6A miss moves a buffer between buckets kernel/bio.c
  7. 7A lock in every bucket, and a lock for misses kernel/bio.c
  8. 8A hit takes one bucket lock kernel/bio.c
  9. 9A miss: let go, take evict, look again kernel/bio.c
  10. 10The search holds at most two buckets, in order kernel/bio.c
  11. 11Unlink, relabel, insert kernel/bio.c
  12. 12brelse finds the bucket from blockno kernel/bio.c
  13. 13bpin under log.lock, and bunpin kernel/bio.c
  14. 14The clock, read without a lock kernel/bio.c
  15. 15The lock graph after the split kernel/bio.c

Keys: ← → step · Home start