Lec13 Crash Recovery

文件系统
Published

2024-04-25

Modified

2024-10-23

1 Lecture

logging 主要是为了解决故障恢复的问题。logging 确保了文件系统的系统调用是原子性的。其次,它支持快速恢复。最后从原则上来说他应该非常高效。

将磁盘分割为两个部分,其中一个部分是 log,另一个部分是文件系统。logging 主要分为以下四个步骤:

  1. log write。当需要更新文件系统时,我们并不更新系统本身,而是将数据写入到 log 中,log 中记录了这个更新的内容,比如写入到 block 45 等等。
  2. commit op。之后某个时间,当文件系统的操作都结束了,我们会 commit 文件系统的操作。这意味着我们需要在 log 的某个位置记录属于同一个文件系统的操作的个数。
  3. install log。当我们在 log 中存储了所有写 block 的内容时,我们想要真正执行这些操作,只需将 block 从 log 分区移到文件系统分区。
  4. clean log。一旦完成,就可以清除 log。实际上就是将属于同一个文件系统的操作的个数设为 0。

xv6 的 log 结构比较简单,我们最开始有一个 header block,也就是 commit record,里面包含了:

  1. n 代表有效的 log block 数量;
  2. 每个 log block 实际对应的 block 编号。

之后就是 log 的数据,也就是每个 block 的数据。

我们用事务作为磁盘操作原子性的保证。在 xv6 中,所有的事务操作会在开头和结尾分别被 begin_opend_op 保护。其中 end_op 会实现 commit 操作。

  1. begin_op() 函数在等待日志系统当前没有在执行提交操作,并且有足够的未保留的日志空间来容纳本次调用的写操作。log.outstanding 记录了已经保留日志空间的系统调用数量;总共保留的空间是 log.outstanding 乘以 MAXOPBLOCKS。每增加一个 log.outstanding 既保留了空间,也阻止了在此系统调用期间的提交。这段代码假设每个系统调用可能写入最多 MAXOPBLOCKS 个不同的块。

  2. log_write() 函数作为 bwrite 的代理。它在内存中记录了块的扇区号,为其在磁盘上的日志中预留了一个位置,并将缓冲区固定在块缓存中,以防止块缓存将其驱逐。直到提交之前,该块必须保留在缓存中:在此期间,缓存中的副本是修改的唯一记录;只有在提交之后才能将其写入磁盘上的位置;同一事务中的其他读取必须看到修改。log_write 注意到在单个事务中多次写入块时,并将该块分配给日志中的同一位置。这种优化通常称为吸收。通常情况下,例如,事务中可能多次写入包含多个文件的 inode 的磁盘块。通过将几个磁盘写入吸收到一个中,文件系统可以节省日志空间,并且可以获得更好的性能,因为只需要将磁盘块的一个副本写入磁盘。

  3. end_op() 函数首先减少了未完成的系统调用的计数。如果计数现在为零,则通过调用 commit() 提交当前事务。这个过程有四个阶段。write_log() 函数将事务中修改的每个块从缓冲区缓存复制到日志中的相应位置。write_head() 函数将头块写入磁盘:这是提交点,如果在写入后发生崩溃,则会导致恢复从日志中重新执行事务的写入。install_trans 函数从日志中读取每个块,并将其写入文件系统中的适当位置。最后,end_op 写入日志头,并将计数设为零;这必须在下一个事务开始写入日志块之前发生,以防止在恢复时使用一个事务的头与后续事务的日志块。

  4. recover_from_log() 函数是从 initlog() 函数中调用的,而 initlog() 函数在引导过程中在第一个用户进程运行之前(在 fsinit() 中调用)调用。它读取日志头,并在日志指示包含已提交事务时模拟 end_op 的操作。