8.3 代码:Buffer cache
Buffer cache是以双链表表示的缓冲区。main
(kernel/main.c:27)调用的函数binit
使用静态数组buf
(kernel/bio.c:43-52)中的NBUF
个缓冲区初始化列表。对Buffer cache的所有其他访问都通过bcache.head
引用链表,而不是buf
数组。
缓冲区有两个与之关联的状态字段。字段valid
表示缓冲区是否包含块的副本。字段disk
表示缓冲区内容是否已交给磁盘,这可能会更改缓冲区(例如,将数据从磁盘写入data
)。
Bread
(kernel/bio.c:93)调用bget
为给定扇区(kernel/bio.c:97)获取缓冲区。如果缓冲区需要从磁盘进行读取,bread
会在返回缓冲区之前调用virtio_disk_rw
来执行此操作。
Bget
(kernel/bio.c:59)扫描缓冲区列表,查找具有给定设备和扇区号(kernel/bio.c:65-73)的缓冲区。如果存在这样的缓冲区,bget
将获取缓冲区的睡眠锁。然后Bget
返回锁定的缓冲区。
如果对于给定的扇区没有缓冲区,bget
必须创建一个,这可能会重用包含其他扇区的缓冲区。它再次扫描缓冲区列表,查找未在使用中的缓冲区(b->refcnt = 0
):任何这样的缓冲区都可以使用。Bget
编辑缓冲区元数据以记录新设备和扇区号,并获取其睡眠锁。注意,b->valid = 0
的布置确保了bread
将从磁盘读取块数据,而不是错误地使用缓冲区以前的内容。
每个磁盘扇区最多有一个缓存缓冲区是非常重要的,并且因为文件系统使用缓冲区上的锁进行同步,可以确保读者看到写操作。Bget
的从第一个检查块是否缓存的循环到第二个声明块现在已缓存(通过设置dev
、blockno
和refcnt
)的循环,一直持有bcache.lock
来确保此不变量。这会导致检查块是否存在以及(如果不存在)指定一个缓冲区来存储块具有原子性。
bget
在bcache.lock
临界区域之外获取缓冲区的睡眠锁是安全的,因为非零b->refcnt
防止缓冲区被重新用于不同的磁盘块。睡眠锁保护块缓冲内容的读写,而bcache.lock
保护有关缓存哪些块的信息。
如果所有缓冲区都处于忙碌,那么太多进程同时执行文件系统调用;bget
将会panic
。一个更优雅的响应可能是在缓冲区空闲之前休眠,尽管这样可能会出现死锁。
一旦bread
读取了磁盘(如果需要)并将缓冲区返回给其调用者,调用者就可以独占使用缓冲区,并可以读取或写入数据字节。如果调用者确实修改了缓冲区,则必须在释放缓冲区之前调用bwrite
将更改的数据写入磁盘。Bwrite
(kernel/bio.c:107)调用virtio_disk_rw
与磁盘硬件对话。
当调用方使用完缓冲区后,它必须调用brelse
来释放缓冲区(brelse
是b-release
的缩写,这个名字很隐晦,但值得学习:它起源于Unix,也用于BSD、Linux和Solaris)。brelse
(kernel/bio.c:117)释放睡眠锁并将缓冲区移动到链表的前面(kernel/bio.c:128-133)。移动缓冲区会使列表按缓冲区的使用频率排序(意思是释放):列表中的第一个缓冲区是最近使用的,最后一个是最近使用最少的。bget
中的两个循环利用了这一点:在最坏的情况下,对现有缓冲区的扫描必须处理整个列表,但首先检查最新使用的缓冲区(从bcache.head
开始,然后是下一个指针),在引用局部性良好的情况下将减少扫描时间。选择要重用的缓冲区时,通过自后向前扫描(跟随prev
指针)选择最近使用最少的缓冲区。