8.3 代码:Buffer cache

Buffer cache是以双链表表示的缓冲区。mainkernel/main.c:27)调用的函数binit使用静态数组bufkernel/bio.c:43-52)中的NBUF个缓冲区初始化列表。对Buffer cache的所有其他访问都通过bcache.head引用链表,而不是buf数组。

缓冲区有两个与之关联的状态字段。字段valid表示缓冲区是否包含块的副本。字段disk表示缓冲区内容是否已交给磁盘,这可能会更改缓冲区(例如,将数据从磁盘写入data)。

Breadkernel/bio.c:93)调用bget为给定扇区(kernel/bio.c:97)获取缓冲区。如果缓冲区需要从磁盘进行读取,bread会在返回缓冲区之前调用virtio_disk_rw来执行此操作。

Bgetkernel/bio.c:59)扫描缓冲区列表,查找具有给定设备和扇区号(kernel/bio.c:65-73)的缓冲区。如果存在这样的缓冲区,bget将获取缓冲区的睡眠锁。然后Bget返回锁定的缓冲区。

如果对于给定的扇区没有缓冲区,bget必须创建一个,这可能会重用包含其他扇区的缓冲区。它再次扫描缓冲区列表,查找未在使用中的缓冲区(b->refcnt = 0):任何这样的缓冲区都可以使用。Bget编辑缓冲区元数据以记录新设备和扇区号,并获取其睡眠锁。注意,b->valid = 0的布置确保了bread将从磁盘读取块数据,而不是错误地使用缓冲区以前的内容。

每个磁盘扇区最多有一个缓存缓冲区是非常重要的,并且因为文件系统使用缓冲区上的锁进行同步,可以确保读者看到写操作。Bget的从第一个检查块是否缓存的循环到第二个声明块现在已缓存(通过设置devblocknorefcnt)的循环,一直持有bcache.lock来确保此不变量。这会导致检查块是否存在以及(如果不存在)指定一个缓冲区来存储块具有原子性。

bgetbcache.lock临界区域之外获取缓冲区的睡眠锁是安全的,因为非零b->refcnt防止缓冲区被重新用于不同的磁盘块。睡眠锁保护块缓冲内容的读写,而bcache.lock保护有关缓存哪些块的信息。

如果所有缓冲区都处于忙碌,那么太多进程同时执行文件系统调用;bget将会panic。一个更优雅的响应可能是在缓冲区空闲之前休眠,尽管这样可能会出现死锁。

一旦bread读取了磁盘(如果需要)并将缓冲区返回给其调用者,调用者就可以独占使用缓冲区,并可以读取或写入数据字节。如果调用者确实修改了缓冲区,则必须在释放缓冲区之前调用bwrite将更改的数据写入磁盘。Bwritekernel/bio.c:107)调用virtio_disk_rw与磁盘硬件对话。

当调用方使用完缓冲区后,它必须调用brelse来释放缓冲区(brelseb-release的缩写,这个名字很隐晦,但值得学习:它起源于Unix,也用于BSD、Linux和Solaris)。brelsekernel/bio.c:117)释放睡眠锁并将缓冲区移动到链表的前面(kernel/bio.c:128-133)。移动缓冲区会使列表按缓冲区的使用频率排序(意思是释放):列表中的第一个缓冲区是最近使用的,最后一个是最近使用最少的。bget中的两个循环利用了这一点:在最坏的情况下,对现有缓冲区的扫描必须处理整个列表,但首先检查最新使用的缓冲区(从bcache.head开始,然后是下一个指针),在引用局部性良好的情况下将减少扫描时间。选择要重用的缓冲区时,通过自后向前扫描(跟随prev指针)选择最近使用最少的缓冲区。