文件系统的目的是组织和存储数据。文件系统通常支持在用户和应用之间共享数据,同时具备持久化
的能力能够在重启之后保证数据可用。
xv6 提供类 unix 的文件系统,有文件,目录,路径名称和在虚拟硬盘上进行数据存储。文件系统需要解决以下几个挑战:
- 文件系统需要以一种数据结构存储在硬盘上,用来表示目录和文件名,记录保存每个文件内容的块的标识,并记录磁盘的哪些区域是空闲的
- 文件系统必须支持
崩溃恢复
。因为一旦崩溃发生,文件系统在重启后依旧能正常工作。风险点在崩溃可能中断连续更新并留下不一致的数据结构。例如一个块同时被文件使用的同时标记为 free。 - 不同进程可能同时操作文件系统,所以文件系统代码必须协调保持不变。
- 访问硬盘比访问内存要慢好几个数量级,所以文件系统必须维护常用块的内存缓存。
本章接下来的内容将会阐述 xv6 是如何解决这些挑战的。
8.1 概览
xv6 的文件系统由一个 7 层组织实现。
image.png
硬盘层在虚拟硬件驱动上读写块。
缓存层缓存磁盘块并同步,并保证统一时间只有一个内核进程可以修改任意一个特定的块。
日志层允许高级别的层以事务的形式同时更新多个块。确保这些块以原子的方式更新以应对崩溃。
索引层提供单独的文件,每个文件都表示为一个唯一的标号和一些拥有该文件数据的块。
目录层将每个目录实现为一种特殊的索引节点其内容是一系列的目录条目每个目录条目包含文件名和标号。
路径层提供分级式的路径,例如/usr/rtm/xv6/fs.c
,并通过递归查找进行解析。
描述层用文件系统抽象了许多 unix 资源,简化程序员的工作。
文件系统必须有一个方案来决定将 inode 和块内容存储在哪里。为此,xv6 将磁盘分为许多节。
文件系统不使用 block0 (该快被用于启动)。Block 1 被称为超级块,包含文件系统的元信息(以块为单位的文件系统大小,inode 数量,log 中块数量)。blocks 2 开始保存日志,日志之后是inode,每个块中有多个 inodes。之后是 bitmap 块,跟踪那些块在使用。剩余的块是数据块,每个数据块要么被标记为 free,要么保存着文件或者目录的内容。超级块的内容由一个独立的程序完成,称为mkfs
这个程序被用于构建一个初始化的文件系统。
剩下的章节会讨论每一层,从 buffer cache 开始。研究在较低层进行精心设计的抽象并让高层设计变得简单。
8.2 buffer cache layer
缓存层有两个工作(1)同步磁盘访问,确保内存中一个块只有一个拷贝,并且同时仅有一个内核线程可以使用该块。(2)缓存常用的数据块以不需要从更慢的磁盘中读取。代码位于 bio.c。
缓存层的主要导出接口有两个,bread
和 bwrite
。前者获得一个可以在内存里进行读或者修改的缓冲区。后者将修改的缓冲区写到磁盘上合适的块上。当内核完成工作时,必须调用 brelse
来释放缓冲区。缓冲使用每个缓冲区一个休眠锁,来确保同时只有一个线程使用同一个缓冲区。bread
返回一个加锁的缓冲区,brelse
释放锁。
缓冲层有一个固定数量对应磁盘块的缓冲区,这意味着如果文件系统请求一个不在缓冲中的块,缓存层必须回收一个被其他块持有的缓冲区。缓冲层回收一个最近最少使用的层给新的块。这是基于最近最少使用的缓冲区是最不可能将被使用的。
image.png
8.3 Code: Buffer cache
缓存是一个缓冲的双向链表。binit
函数由 main
函数调用,初始化一个长度为 NBUF
的缓冲数组。所有其他对缓冲区的访问将通过 bcache.head
这个链表头指针进行,而不是 buf
数组。
一个缓冲区有两个关联的状态字段。valid
标识 buffer 是否包含一个块的拷贝。disk
标识该缓冲区已经交给磁盘,可能会改变缓冲区。
bread
调用 bget
获取给定块的缓冲区。如果话缓冲区需要从磁盘读取,bread 调用 virtio_disk_rw
进行读取后返回。
bget
用给定的设备和块号扫描链表。如果有这样一个缓冲块,bget
获取这个缓冲块的休眠锁,返回这个缓冲区。
如果给定的扇区没有缓冲区,bget
需要创造一个,可能复用一个被其他扇区持有的缓冲区。它进行两次缓冲区链表的扫描,查找没有被使用的缓冲区(b->refcnt = 0);任何一个这样的缓冲区都可以被使用。bget
修改缓冲区的元数据信息以获得他的休眠锁。注意设置 b->valid =0
确保 bread
重新从磁盘上读取块数据而不是错误的使用缓冲区上的旧数据。
每个磁盘扇区最多有一个缓冲区很重要,并且在使用缓冲区锁的情况下进行同步,可以确保读者能看见写入。bget
通过在两次循环中持续的持有锁来保持这个不变量,这两次循环是第一次循环中检查缓冲区是否缓存,第二次循环描述扇区已经被缓存了(通过重新设置 dev,blockno和 refcnt)。这可以保证检查缓冲块是否存在和指定一个缓冲区是原子的。
对 bget
来说在 bcache .lock
临界区外获取缓冲区的休眠锁是安全的。因为非零的b->refcnt
阻止了缓冲区被不同的块使用。(为什么安全?当一个 buffer 被持有时,其 refcnt 一定大于0,此时其他cpu不可能占有该 buffer 从而导致错误)休眠锁保护块缓冲区内容的读写,而bcache.lock
保护哪些块被缓存。
如果所有的缓冲区都很忙,并且很多进程并发的进行系统调用,bget
会 panic。一个更优雅的做法可能是休眠并等待缓冲区空闲,然而这样做可能出现死锁。(为什么出现死锁???)
一旦 bread
从磁盘读取了数据并返回缓冲区给调用者,调用着就独占缓冲区进行读写。如果调用者修改了buffer,它必须在释放缓冲区之前调用 bwrite
来将修改的数据写到磁盘上。bwrite
调用 virtio_disk_rw
来告诉磁盘设备。
当调用者对 buffer 操作完成时,他必须调用 brelse
来释放(brelse 是 b-release
的缩写,很奇怪但值得学习,它起源与 Unix并且在 BSD,Linux 和 Solaris 上都是这样使用的)。brelse
使用休眠锁并将缓冲区移动到链表头。移动缓冲区会造成链表按照缓冲区最近使用时间排序。链表中的第一个缓冲区是最近使用的,最后一个是最少使用的。bget
中的两个循环利用了这一点,最坏的情况下,扫描器需要处理链表中所有存在的缓冲区,但是优先扫描最近使用的缓冲区会在局部性好的情况下减少扫描时间。而选择最少使用的缓冲区时,扫描器会从后开始往前扫描。
logging layer
文件系统设计中一个最有趣的问题就是崩溃恢复。这个问题是由于多个文件操作设计多个磁盘写入,并在多个写入的一部分完成后发生崩溃可能留下不一致的文件状态。例如,假设崩溃发生在文件截断的时候(将文件长度设置为0并释放文件块的内容)。取决与写入磁盘的顺序,崩溃可能留下一个被标记为 free 但却被引用的 inode,或者分配了空间但未被引用文件块。
后者相对好一些,但是指向被释放的块在重启后会产生严重的问题。重启后,内核可能将这个块分配给其他文件,此时不经意的就有两个文件指向同一个块。如果 xv6 支持多用户,这种情况会有安全问题,因为旧的文件拥有者能够在新的文件中读写这些被其他用户拥有的块。
xv6 通过一种简单的日志形式来解决在文件系统操作中产生的崩溃问题。xv6 系统调用并不直接将文件系统数据写入磁盘。取而代之的是它在磁盘上放置了一个希望写入所有日志的描述。一旦系统调用记录了所有的写入,它向磁盘写入一个特殊的提交记录表明文件包含了所有完成的操作。那时,文件系统复制写入到磁盘上的文件数据中。写入完成后,系统调用清除磁盘上的日志。
如果系统崩溃或者重启,在运行任何进程前,文件系统如下恢复。如果日志被标记为包含完整的操作,恢复代码会拷贝这些写入到磁盘中。如果日志没有标记为完整操作,恢复代码忽略日志。恢复代码最终清除掉日志。
为什么 xv6 的日志解决了文件系统操作崩溃的问题?如果崩溃发生在提交前,磁盘上的日志不会被标记为完整的,恢复代码会忽略它,磁盘上的状态就是这些操作从来没有发生。如果崩溃发生在提交之后,恢复代码会重放这些操作,或许会重新执行如果以前磁盘上已经有这些数据了。在任一情况下,日志对于崩溃的操作都是原子的,恢复之后,所有的操作都会出现在磁盘上,或者都不会出现。
8.5 log design
日志位于超级块中一个已知的固定位置。它由一个标头部块后跟一系列更新块副本组成。标头快由一组每个日志块的扇区号和日志块个数组成。标头块中计数是零,表明日志中没有事务,非零表示该日志日志包含一个完成的提交事务并有指示该日志块的数字。
xv6 在事务提交时写标头块,而不是之前,并在将日志块拷贝到文件系统后将计数置0。因此在事务中途发生崩溃日志头为 0。事务提交后崩溃是一个非 0 的计数?(这怎么理解,回头再看)
在崩溃面前,每个对于写入的系统调用从前到后都应该是原子的。为了允许不同进程并发的进行文件操作,日志系统能够累积不同的系统调用的写操作到一个事物中。因此一个提交可能包含多个完成的系统调用的写操作。为了避免跨事务拆分系统调用,文件系统仅在没有文件系统调用的情况下提交。怎么理解跨事务拆分
翻译
- cached buffer 缓冲区
- sector 扇区
问题
- 什么叫文件内容的块标识?
- 为什么直接 panic,为什么让 bget 陷入休眠会死锁?休眠之前应该先释放 bcache.lock。
注释
跨事务拆分
可能是这样一种情况,一部分系统调用有很多写操作,只完成了一部分,如果其他系统调用做了提交,而剩下一部分没提交,如果崩溃了,就会造成一个系统调用下的文件不一致,因为产生了部分修改
网友评论