xv6, line by line
test yourself

Test yourself · category 16 of 20

The disk driver and the buffer cache

How bread and brelse share 30 cached blocks between harts with a spinlock, a sleep-lock and a reference count, and how virtio_disk_rw hands a request to the disk through descriptors, rings and a doorbell, then sleeps until the interrupt.

2solidType a number

The driver has NUM = 8 descriptors, and every request uses exactly three. At most how many disk requests can be in flight (handed to the device and not yet freed) at once?

kernel/virtio_disk.c
199// allocate three descriptors (they need not be contiguous).
200// disk transfers always use three descriptors.
201static int
204 for (int i = 0; i < 3; i++) {
206 if (idx[i] < 0) {
207 for (int j = 0; j < i; j++)
209 return -1;
210 }
211 }
212 return 0;
decimal, 0x hex or 0b binary
3warm-upChoose one

In bread, what does it mean when bget returns a buffer with valid == 0?

kernel/bio.c
90// Return a locked buf with the contents of the indicated block.
91struct buf *
94 struct buf *b;
97 if (!b->valid) {
99 b->valid = 1;
100 }
101 return b;
4warm-upChoose one

Why is each buffer’s lock (b->lock) a sleep-lock rather than a spinlock?

kernel/buf.h
1struct buf {
2 int valid; // has data been read from disk?
3 int disk; // does disk "own" buf?
6 struct sleeplock lock;
8 struct buf *prev; // LRU cache list
9 struct buf *next;
11};
5warm-upTrue or false, and why

True or false: virtio_disk_rw holds disk.vdisk_lock while it sleeps waiting for the disk to finish.

kernel/virtio_disk.c
286 // Wait for virtio_disk_intr() to say request has finished.
287 while (b->disk == 1) {
292 }
294 disk.info[idx[0]].b = 0;

Why?

6solidPut in order

Put the steps of a disk read in virtio_disk_rw in order.

  1. fill in the header, data and status descriptors
  2. get three free descriptors with alloc3_desc
  3. compute the sector and acquire disk.vdisk_lock
  4. record b in disk.info[idx[0]] and set b->disk = 1
  5. write idx[0] into the avail ring, fence, then increment avail->idx
  6. store to the QUEUE_NOTIFY register
  7. sleep until b->disk == 0, then free the chain
7solidClick the line

Most of the driver talks to the device through ordinary RAM that both share. Click the one line in this excerpt that writes to a device register (memory-mapped I/O): the doorbell.

kernel/virtio_disk.c
270 // record struct buf for virtio_disk_intr().
271 b->disk = 1;
272 disk.info[idx[0]].b = b;
274 // tell the device the first index in our chain of descriptors.
279 // tell the device another avail ring entry is available.
280 disk.avail->idx += 1; // not % NUM ...
284 *R(VIRTIO_MMIO_QUEUE_NOTIFY) = 0; // value is queue number
286 // Wait for virtio_disk_intr() to say request has finished.
287 while (b->disk == 1) {
292 }

Your pick: none yet (click a line in the code)

8solidDecode the bits

While a request is in flight, gdb shows one of its three descriptors with flags = 0x3. Decode it (VRING_DESC_F_NEXT = 1, VRING_DESC_F_WRITE = 2).

Value: 0x3

9solidFill in the machine state

Hart 1 is idle in its scheduler loop when the disk interrupt arrives. It traps through kernelvec and kerneltrap into devintr, which calls virtio_disk_intr. What is hart 1’s state just after acquire(&disk.vdisk_lock) returns?

kernel/virtio_disk.c
300void
305 // the device won't raise another interrupt until we tell it
306 // we've seen this interrupt, which the following line does.
307 // this may race with the device writing new entries to
308 // the "used" ring, in which case we may process the new
309 // completion entries in this interrupt, and have nothing to do
310 // in the next interrupt, which is harmless.
10solidFill in the machine state

cat’s read system call has reached virtio_disk_rw (through fileread, readi and bread). It holds README’s inode lock and the buffer’s sleep-lock, and is at line 284, about to ring the doorbell. What is the state of its hart?

kernel/virtio_disk.c
270 // record struct buf for virtio_disk_intr().
271 b->disk = 1;
272 disk.info[idx[0]].b = b;
274 // tell the device the first index in our chain of descriptors.
279 // tell the device another avail ring entry is available.
280 disk.avail->idx += 1; // not % NUM ...
284 *R(VIRTIO_MMIO_QUEUE_NOTIFY) = 0; // value is queue number
286 // Wait for virtio_disk_intr() to say request has finished.
287 while (b->disk == 1) {
292 }
11solidType a number

Right after boot, binit has built the LRU list and every buffer has refcnt == 0. The first bget miss scans from bcache.head.prev. Which index i of bcache.buf[i] does it recycle?

kernel/bio.c
35void
36binit(void)
38 struct buf *b;
40 initlock(&bcache.lock, "bcache");
42 // Create linked list of buffers
45 for (b = bcache.buf; b < bcache.buf + NBUF; b++) {
48 initsleeplock(&b->lock, "buffer");
51 }
decimal, 0x hex or 0b binary
12solidChoose all that apply

Which fields of a struct buf are protected by bcache.lock (the spinlock), rather than by the buffer’s own sleep-lock or by the disk lock?

kernel/buf.h
1struct buf {
2 int valid; // has data been read from disk?
3 int disk; // does disk "own" buf?
6 struct sleeplock lock;
8 struct buf *prev; // LRU cache list
9 struct buf *next;
11};
13warm-upMatch the pairs

Match each lock with what it protects.

14deepChoose one

On a cache hit, bget increments b->refcnt before releasing bcache.lock and calling acquiresleep. What could go wrong if it released bcache.lock first and incremented refcnt afterwards?

kernel/bio.c
57static struct buf *
60 struct buf *b;
64 // Is the block already cached?
65 for (b = bcache.head.next; b != &bcache.head; b = b->next) {
66 if (b->dev == dev && b->blockno == blockno) {
67 b->refcnt++;
70 return b;
71 }
72 }
74 // Not cached.
75 // Recycle the least recently used (LRU) unused buffer.
76 for (b = bcache.head.prev; b != &bcache.head; b = b->prev) {
77 if (b->refcnt == 0) {
78 b->dev = dev;
80 b->valid = 0;
81 b->refcnt = 1;
84 return b;
85 }
86 }
87 panic("bget: no buffers");
15deepChoose one

When bget recycles a buffer, it overwrites dev, blockno and valid without ever writing the old data back to disk. Why can’t that lose a modification?

kernel/bio.c
74 // Not cached.
75 // Recycle the least recently used (LRU) unused buffer.
76 for (b = bcache.head.prev; b != &bcache.head; b = b->prev) {
77 if (b->refcnt == 0) {
78 b->dev = dev;
80 b->valid = 0;
81 b->refcnt = 1;
84 return b;
85 }
86 }
87 panic("bget: no buffers");
16solidChoose one

log_write calls bpin the first time a block joins the current transaction. What is the pin for?

kernel/log.c
223void
224log_write(struct buf *b)
226 int i;
229 if (log.lh.n >= LOGBLOCKS)
230 panic("too big a transaction");
232 panic("log_write outside of trans");
234 for (i = 0; i < log.lh.n; i++) {
235 if (log.lh.block[i] == b->blockno) // log absorption
236 break;
237 }
239 if (i == log.lh.n) { // Add new block to log?
241 log.lh.n++;
242 }
17deepChoose all that apply

Child A (hart 1) and child B (hart 2) both need block 46 at once. A’s bget misses, relabels a buffer as 46 with valid = 0, and releases bcache.lock; then B’s bget runs while A is reading the block from disk. Which statements are true?

kernel/bio.c
57static struct buf *
60 struct buf *b;
64 // Is the block already cached?
65 for (b = bcache.head.next; b != &bcache.head; b = b->next) {
66 if (b->dev == dev && b->blockno == blockno) {
67 b->refcnt++;
70 return b;
71 }
72 }
74 // Not cached.
75 // Recycle the least recently used (LRU) unused buffer.
76 for (b = bcache.head.prev; b != &bcache.head; b = b->prev) {
77 if (b->refcnt == 0) {
78 b->dev = dev;
80 b->valid = 0;
81 b->refcnt = 1;
84 return b;
85 }
86 }
87 panic("bget: no buffers");
90// Return a locked buf with the contents of the indicated block.
91struct buf *
94 struct buf *b;
97 if (!b->valid) {
99 b->valid = 1;
100 }
101 return b;
18deepChoose one

When alloc3_desc gets only one or two descriptors, it gives them back before virtio_disk_rw sleeps. Why not keep them and wait only for the missing ones?

kernel/virtio_disk.c
199// allocate three descriptors (they need not be contiguous).
200// disk transfers always use three descriptors.
201static int
204 for (int i = 0; i < 3; i++) {
206 if (idx[i] < 0) {
207 for (int j = 0; j < i; j++)
209 return -1;
210 }
211 }
212 return 0;
215void
218 uint64 sector = b->blockno * (BSIZE / 512);
222 // the spec's Section 5.2 says that legacy block operations use
223 // three descriptors: one for type/reserved/sector, one for the
224 // data, one for a 1-byte status result.
226 // allocate the three descriptors.
227 int idx[3];
228 while (1) {
229 if (alloc3_desc(idx) == 0) {
230 break;
231 }
236 }
19warm-upChoose one

cat issued a request on hart 0 and is asleep. The completion interrupt is taken on hart 1. How does virtio_disk_intr know which buffer, and so which sleeper, the completion is for?

kernel/virtio_disk.c
300void
305 // the device won't raise another interrupt until we tell it
306 // we've seen this interrupt, which the following line does.
307 // this may race with the device writing new entries to
308 // the "used" ring, in which case we may process the new
309 // completion entries in this interrupt, and have nothing to do
310 // in the next interrupt, which is harmless.
315 // the device increments disk.used->idx when it
316 // adds an entry to the used ring.
318 while (disk.used_idx != disk.used->idx) {
322 if (disk.info[id].status != 0)
323 panic("virtio_disk_intr status");
325 struct buf *b = disk.info[id].b;
326 b->disk = 0; // disk is done with buf
330 }
20deepChoose one

virtio_disk_rw waits with while (b->disk == 1) { sleep_prepare(b); release; sleep(); acquire; } instead of sleeping once. When can sleep() return while the disk is still working on the request?

kernel/virtio_disk.c
286 // Wait for virtio_disk_intr() to say request has finished.
287 while (b->disk == 1) {
292 }
21warm-upChoose one

During a disk read, who copies the 1024 bytes of the block into b->data?

22warm-upClick the line

Process B is asleep in acquiresleep, waiting for a buffer that process A holds. Click the line of A’s brelse that lets B continue.

kernel/bio.c
114// Release a locked buffer.
115// Move to the head of the most-recently-used list.
116void
117brelse(struct buf *b)
120 panic("brelse");
126 if (b->refcnt == 0) {
127 // no one is waiting for it.
134 }

Your pick: none yet (click a line in the code)

23deepTrue or false, and why

True or false: panic("bget: no buffers") can only happen while a log transaction has pinned buffers; with no transaction running, every buffer is eventually free.

kernel/bio.c
74 // Not cached.
75 // Recycle the least recently used (LRU) unused buffer.
76 for (b = bcache.head.prev; b != &bcache.head; b = b->prev) {
77 if (b->refcnt == 0) {
78 b->dev = dev;
80 b->valid = 0;
81 b->refcnt = 1;
84 return b;
85 }
86 }
87 panic("bget: no buffers");

Why?

24solidChoose all that apply

Which of these are done by the hart that takes the disk completion interrupt (in devintr and virtio_disk_intr), rather than by the process that issued the request?

kernel/virtio_disk.c
300void
305 // the device won't raise another interrupt until we tell it
306 // we've seen this interrupt, which the following line does.
307 // this may race with the device writing new entries to
308 // the "used" ring, in which case we may process the new
309 // completion entries in this interrupt, and have nothing to do
310 // in the next interrupt, which is harmless.
315 // the device increments disk.used->idx when it
316 // adds an entry to the used ring.
318 while (disk.used_idx != disk.used->idx) {
322 if (disk.info[id].status != 0)
323 panic("virtio_disk_intr status");
325 struct buf *b = disk.info[id].b;
326 b->disk = 0; // disk is done with buf
330 }
25solidChoose one

Why is there an io_fence between writing avail->ring[...] and incrementing avail->idx?

kernel/virtio_disk.c
274 // tell the device the first index in our chain of descriptors.
279 // tell the device another avail ring entry is available.
280 disk.avail->idx += 1; // not % NUM ...
284 *R(VIRTIO_MMIO_QUEUE_NOTIFY) = 0; // value is queue number