8.3 Code: Buffer cache
The buffer cache is a doubly-linked list of buffers.
The function
binit
,
called by
main
(kernel/main.c:27),
initializes the list with the
NBUF
buffers in the static array
buf
(kernel/bio.c:43-52).
All other access to the buffer cache refer to the linked list via
bcache.head
,
not the
buf
array.
A buffer has two state fields associated with it.
The field
valid
indicates that the buffer contains a copy of the block.
The field disk
indicates that the buffer content has been handed to
the disk, which may change the buffer (e.g., write
data from the disk into data
).
bread
(kernel/bio.c:93)
calls
bget
to get a buffer for the given sector
(kernel/bio.c:97).
If the buffer needs to be read from disk,
bread
calls
virtio_disk_rw
to do that before returning the buffer.
bget
(kernel/bio.c:59)
scans the buffer list for a buffer with the given device and sector numbers
(kernel/bio.c:65-73).
If there is such a buffer,
bget
acquires the sleep-lock for the buffer.
bget
then returns the locked buffer.
If there is no cached buffer for the given sector,
bget
must make one, possibly reusing a buffer that held
a different sector.
It scans the buffer list a second time, looking for a buffer
that is not in use (b->refcnt = 0
);
any such buffer can be used.
bget
edits the buffer metadata to record the new device and sector number
and acquires its sleep-lock.
Note that the assignment
b->valid = 0
ensures that
bread
will read the block data from disk
rather than incorrectly using the buffer’s previous contents.
It is important that there is at most one cached buffer per
disk sector, to ensure that readers see writes, and because the
file system uses locks on buffers for synchronization.
bget
ensures this invariant by holding the
bache.lock
bcache.lock
continuously from the first loop’s check of whether the
block is cached through the second loop’s declaration that
the block is now cached (by setting
dev
,
blockno
,
and
refcnt
).
This causes the check for a block’s presence and (if not
present) the designation of a buffer to hold the block to
be atomic.
It is safe for
bget
to acquire the buffer’s sleep-lock outside of the
bcache.lock
critical section,
since the non-zero
b->refcnt
prevents the buffer from being re-used for a different disk block.
The sleep-lock protects reads
and writes of the block’s buffered content, while the
bcache.lock
protects information about which blocks are cached.
If all the buffers are busy, then too many processes are
simultaneously executing file system calls;
bget
panics.
A more graceful response might be to sleep until a buffer became free,
though there would then be a possibility of deadlock.
Once
bread
has read the disk (if needed) and returned the
buffer to its caller, the caller has
exclusive use of the buffer and can read or write the data bytes.
If the caller does modify the buffer, it must call
bwrite
to write the changed data to disk before releasing the buffer.
bwrite
(kernel/bio.c:107)
calls
virtio_disk_rw
to talk to the disk hardware.
When the caller is done with a buffer,
it must call
brelse
to release it.
(The name
brelse
,
a shortening of
b-release,
is cryptic but worth learning:
it originated in Unix and is used in BSD, Linux, and Solaris too.)
brelse
(kernel/bio.c:117)
releases the sleep-lock and
moves the buffer
to the front of the linked list
(kernel/bio.c:128-133).
Moving the buffer causes the
list to be ordered by how recently the buffers were used (meaning released):
the first buffer in the list is the most recently used,
and the last is the least recently used.
The two loops in
bget
take advantage of this:
the scan for an existing buffer must process the entire list
in the worst case, but checking the most recently used buffers
first (starting at
bcache.head
and following
next
pointers) will reduce scan time when there is good locality of reference.
The scan to pick a buffer to reuse picks the least recently used
buffer by scanning backward
(following
prev
pointers).