16.2 XV6 File system logging回顾

首先来回顾一下XV6的logging系统。我们有一个磁盘用来存储XV6的文件系统,你可以认为磁盘分为了两个部分:

  • 首先是文件系统目录的树结构,以root目录为根节点,往下可能有其他的目录,我们可以认为目录结构就是一个树状的数据结构。假设root目录下有两个子目录D1和D2,D1目录下有两个文件F1和F2,每个文件又包含了一些block。除此之外,还有一些其他并非是树状结构的数据,比如bitmap表明了每一个data block是空闲的还是已经被分配了。inode,目录内容,bitmap block,我们将会称之为metadata block(注,Frans和Robert在这里可能有些概念不统一,对于Frans来说,目录内容应该也属于文件内容,目录是一种特殊的文件,详见14.3;而对于Robert来说,目录内容是metadata。),另一类就是持有了文件内容的block,或者叫data block。

  • 除了文件系统之外,XV6在磁盘最开始的位置还有一段log。XV6的log相对来说比较简单,它有header block,之后是一些包含了有变更的文件系统block,这里可以是metadata block也可以是data block。header block会记录之后的每一个log block应该属于文件系统中哪个block,假设第一个log block属于block 17,第二个属于block 29。

在计算机上,我们会有一些用户程序调用write/create系统调用来修改文件系统。在内核中存在block cache,最初write请求会被发到block cache。block cache就是磁盘中block在内存中的拷贝,所以最初对于文件block或者inode的更新走到了block cache。

在write系统调用的最后,这些更新都被拷贝到了log中,之后我们会更新header block的计数来表明当前的transaction已经结束了。在文件系统的代码中,任何修改了文件系统的系统调用函数中,某个位置会有begin_op,表明马上就要进行一系列对于文件系统的更新了,不过在完成所有的更新之前,不要执行任何一个更新。在begin_op之后是一系列的read/write操作。最后是end_op,用来告诉文件系统现在已经完成了所有write操作。所以在begin_op和end_op之间,所有的write block操作只会走到block cache中。当系统调用走到了end_op函数,文件系统会将修改过的block cache拷贝到log中。

在拷贝完成之后,文件系统会将修改过的block数量,通过一个磁盘写操作写入到log的header block,这次写入被称为commit point。在commit point之前,如果发生了crash,在重启时,整个transaction的所有写磁盘操作最后都不会应用。在commit point之后,即使立即发生了crash,重启时恢复软件会发现在log header中记录的修改过的block数量不为0,接下来就会将log header中记录的所有block,从log区域写入到文件系统区域。

这里实际上使得系统调用中位于begin_op和end_op之间的所有写操作在面对crash时具备原子性。也就是说,要么文件系统在crash之前更新了log的header block,这样所有的写操作都能生效;要么crash发生在文件系统更新log的header block之前,这样没有一个写操作能生效。

在crash并重启时,必须有一些恢复软件能读取log的header block,并判断里面是否记录了未被应用的block编号,如果有的话,需要写(也有可能是重写)log block到文件系统中对应的位置;如果没有的话,恢复软件什么也不用做。

这里有几个超级重要的点,不仅针对XV6,对于大部分logging系统都适用:

  • 包括XV6在内的所有logging系统,都需要遵守write ahead rule。这里的意思是,任何时候如果一堆写操作需要具备原子性,系统需要先将所有的写操作记录在log中,之后才能将这些写操作应用到文件系统的实际位置。也就是说,我们需要预先在log中定义好所有需要具备原子性的更新,之后才能应用这些更新。write ahead rule是logging能实现故障恢复的基础。write ahead rule使得一系列的更新在面对crash时具备了原子性。

  • 另一点是,XV6对于不同的系统调用复用的是同一段log空间,但是直到log中所有的写操作被更新到文件系统之前,我们都不能释放或者重用log。我将这个规则称为freeing rule,它表明我们不能覆盖或者重用log空间,直到保存了transaction所有更新的这段log,都已经反应在了文件系统中。

所以在XV6中,end_op做了大量的工作,首先是将所有的block记录在log中,之后是更新log header。在没有crash的正常情况,文件系统需要再次将所有的block写入到磁盘的文件系统中。磁盘中的文件系统更新完成之后,XV6文件系统还需要删除header block记录的变更了的block数量,以表明transaction已经完成了,之后就可以重用log空间。

在向log写入任何新内容之前,删除header block中记录的block数量也很重要。因为你不会想要在header block中记录的还是前一个transaction的信息,而log中记录的又是一个新的transaction的数据。可以假设新的transaction对应的是与之前不同的block编号的数据,这样的话,在crash重启时,log中的数据会被写入到之前记录的旧的block编号位置。所以我们必须要先清除header block。

freeing rule的意思就是,在从log中删除一个transaction之前,我们必须将所有log中的所有block都写到文件系统中。

这些规则使得,就算一个文件系统更新可能会复杂且包含多个写操作,但是每次更新都是原子的,在crash并重启之后,要么所有的写操作都生效,要么没有写操作能生效。

要介绍Linux的logging方案,就需要了解XV6的logging有什么问题?为什么Linux不使用与XV6完全一样的logging方案?这里的回答简单来说就是XV6的logging太慢了。

XV6中的任何一个例如create/write的系统调用,需要在整个transaction完成之后才能返回。所以在创建文件的系统调用返回到用户空间之前,它需要完成所有end_op包含的内容,这包括了:

  • 将所有更新了的block写入到log

  • 更新header block

  • 将log中的所有block写回到文件系统分区中

  • 清除header block

之后才能从系统调用中返回。在任何一个文件系统调用的commit过程中,不仅是占据了大量的时间,而且其他系统调用也不能对文件系统有任何的更新。所以这里的系统调用实际上是一次一个的发生,而每个系统调用需要许多个写磁盘的操作。这里每个系统调用需要等待它包含的所有写磁盘结束,对应的技术术语被称为synchronize。XV6的系统调用对于写磁盘操作来说是同步的(synchronized),所以它非常非常的慢。在使用机械硬盘时,它出奇的慢,因为每个写磁盘都需要花费10毫秒,而每个系统调用又包含了多个写磁盘操作。所以XV6每秒只能完成几个更改文件系统的系统调用。如果我们在SSD上运行XV6会快一些,但是离真正的高效还差得远。

另一件需要注意的更具体的事情是,在XV6的logging方案中,每个block都被写了两次。第一次写入到了log,第二次才写入到实际的位置。虽然这么做有它的原因,但是ext3可以一定程度上修复这个问题。

Last updated