16.4 ext3如何提升性能

ext3通过3种方式提升了性能:

  • 首先,它提供了异步的(asynchronous)系统调用,也就是说系统调用在写入到磁盘之前就返回了,系统调用只会更新缓存在内存中的block,并不用等待写磁盘操作。不过它可能会等待读磁盘。

  • 第二,它提供了批量执行(batching)的能力,可以将多个系统调用打包成一个transaction。

  • 最后,它提供了并发(concurrency)。

这些基本上就是ext3有的,而XV6没有的特性。接下来我将一一介绍这里的特性。

学生提问:有关batching,XV6不是也支持多个系统调用同时执行start_op和end_op,然后再一起commit吗?

Robert教授:是的,XV6具备有限能力的batching。

首先是异步的系统调用。这表示系统调用修改完位于缓存中的block之后就返回,并不会触发写磁盘。所以这里明显的优势就是系统调用能够快速的返回。同时它也使得I/O可以并行的运行,也就是说应用程序可以调用一些文件系统的系统调用,但是应用程序可以很快从系统调用中返回并继续运算,与此同时文件系统在后台会并行的完成之前的系统调用所要求的写磁盘操作。这被称为I/O concurrency,如果没有异步系统调用,很难获得I/O concurrency,或者说很难同时进行磁盘操作和应用程序运算,因为同步系统调用中,应用程序总是要等待磁盘操作结束才能从系统调用中返回。

另一个异步系统调用带来的好处是,它使得大量的批量执行变得容易。

异步系统调用的缺点是系统调用的返回并不能表示系统调用应该完成的工作实际完成了。举个例子,如果你创建了一个文件并写了一些数据然后关闭文件并在console向用户输出done,最后你把电脑的电给断了。尽管所有的系统调用都完成了,程序也输出了done,但是在你重启之后,你的数据并不一定存在。这意味着,在异步系统调用的世界里,如果应用程序关心可能发生的crash,那么应用程序代码应该更加的小心。这在XV6并不是什么大事,因为如果XV6中的write返回了,那么数据就在磁盘上,crash之后也还在。而ext3中,如果write返回了,你完全不能确定crash之后数据还在不在。所以一些应用程序的代码应该仔细编写,例如对于数据库,对于文本编辑器,我如果写了一个文件,我不想在我写文件过程断电然后再重启之后看到的是垃圾文件或者不完整的文件,我想看到的要么是旧的文件,要么是新的文件。

所以文件系统对于这类应用程序也提供了一些工具以确保在crash之后可以有预期的结果。这里的工具是一个系统调用,叫做fsync,所有的UNIX都有这个系统调用。这个系统调用接收一个文件描述符作为参数,它会告诉文件系统去完成所有的与该文件相关的写磁盘操作,在所有的数据都确认写入到磁盘之后,fsync才会返回。所以如果你查看数据库,文本编辑器或者一些非常关心文件数据的应用程序的源代码,你将会看到精心放置的对于fsync的调用。fsync可以帮助解决异步系统调用的问题。对于大部分程序,例如编译器,如果crash了编译器的输出丢失了其实没什么,所以许多程序并不会调用fsync,并且乐于获得异步系统调用带来的高性能。

学生提问:这是不是有时也被称为flush,因为我之前经常听到这个单词?

Robert教授:是的,一个合理的解释fsync的工作的方式是,它flush了所有文件相关的写磁盘操作到了磁盘中,之后再返回,所以flush也是针对这个场景的一个合理的单词。

以上就是异步系统调用,下一个ext3使用的技术是批量执行(batching)。在任何时候,ext3只会有一个open transaction。ext3中的一个transaction可以包含多个不同的系统调用。所以ext3是这么工作的:它首先会宣告要开始一个新的transaction,接下来的几秒所有的系统调用都是这个大的transaction的一部分。我认为默认情况下,ext3每5秒钟都会创建一个新的transaction,所以每个transaction都会包含5秒钟内的系统调用,这些系统调用都打包在一个transaction中。在5秒钟结束的时候,ext3会commit这个包含了可能有数百个更新的大transaction。

为什么这是个好的方案呢?

  • 首先它在多个系统调用之间分摊了transaction带来的固有的损耗。固有的损耗包括写transaction的descriptor block和commit block;在一个机械硬盘中需要查找log的位置并等待磁碟旋转,这些都是成本很高的操作,现在只需要对一批系统调用执行一次,而不用对每个系统调用执行一次这些操作,所以batching可以降低这些损耗带来的影响。

  • 另外,它可以更容易触发write absorption。经常会有这样的情况,你有一堆系统调用最终在反复更新相同的一组磁盘block。举个例子,如果我创建了一些文件,我需要分配一些inode,inode或许都很小只有64个字节,一个block包含了很多个inode,所以同时创建一堆文件只会影响几个block的数据。类似的,如果我向一个文件写一堆数据,我需要申请大量的data block,我需要修改表示block空闲状态的bitmap block中的很多个bit位,如果我分配到的是相邻的data block,它们对应的bit会在同一个bitmap block中,所以我可能只是修改一个block的很多个bit位。所以一堆系统调用可能会反复更新一组相同的磁盘block。通过batching,多次更新同一组block会先快速的在内存的block cache中完成,之后在transaction结束时,一次性的写入磁盘的log中。这被称为write absorption,相比一个类似于XV6的同步文件系统,它可以极大的减少写磁盘的总时间。

  • 最后就是disk scheduling。假设我们要向磁盘写1000个block,不论是在机械硬盘还是SSD(机械硬盘效果会更好),一次性的向磁盘的连续位置写入1000个block,要比分1000次每次写一个不同位置的磁盘block快得多。我们写log就是向磁盘的连续位置写block。通过向磁盘提交大批量的写操作,可以更加的高效。这里我们不仅通过向log中连续位置写入大量block来获得更高的效率,甚至当我们向文件系统分区写入包含在一个大的transaction中的多个更新时,如果我们能将大量的写请求同时发送到驱动,即使它们位于磁盘的不同位置,我们也使得磁盘可以调度这些写请求,并以特定的顺序执行这些写请求,这也很有效。在一个机械硬盘上,如果一次发送大量需要更新block的写请求,驱动可以对这些写请求根据轨道号排序。甚至在一个固态硬盘中,通过一次发送给硬盘大量的更新操作也可以稍微提升性能。所以,只有发送给驱动大量的写操作,才有可能获得disk scheduling。这是batching带来的另一个好处。

ext3使用的最后一个技术就是concurrency,相比XV6这里包含了两种concurrency。

  • 首先ext3允许多个系统调用同时执行,所以我们可以有并行执行的多个不同的系统调用。在ext3决定关闭并commit当前的transaction之前,系统调用不必等待其他的系统调用完成,它可以直接修改作为transaction一部分的block。许多个系统调用都可以并行的执行,并向当前transaction增加block,这在一个多核计算机上尤其重要,因为我们不会想要其他的CPU核在等待锁。在XV6中,如果当前的transaction还没有完成,新的系统调用不能继续执行。而在ext3中,大多数时候多个系统调用都可以更改当前正在进行的transaction。

  • 另一种ext3提供的并发是,可以有多个不同状态的transaction同时存在。所以尽管只有一个open transaction可以接收系统调用,但是其他之前的transaction可以并行的写磁盘。这里可以并行存在的不同transaction状态包括了:

    • 首先是一个open transaction

    • 若干个正在commit到log的transaction,我们并不需要等待这些transaction结束。当之前的transaction还没有commit并还在写log的过程中,新的系统调用仍然可以在当前的open transaction中进行。

    • 若干个正在从cache中向文件系统block写数据的transaction

    • 若干个正在被释放的transaction,这个并不占用太多的工作

通常来说会有位于不同阶段的多个transaction,新的系统调用不必等待旧的transaction提交到log或者写入到文件系统。对比之下,XV6中新的系统调用就需要等待前一个transaction完全完成。

学生提问:如果一个block cache正在被更新,而这个block又正在被写入到磁盘的过程中,会怎样呢?

Robert教授:这的确会是一个问题,这里有个潜在的困难点,因为transaction写入到log中的内容只能包含由该transaction中的系统调用所做的更新,而不能包含在该transaction之后的系统调用的更新。因为如果这么做了的话,那么可能log中会只包含系统调用的部分更新,而我们需要确保transaction包含系统调用的所有更新。所以我们不能承担transaction包含任何在该transaction之后的更新的风险。

ext3是这样解决这个问题的,当它决定结束当前的open transaction时,它会在内存中拷贝所有相关的block,之后transaction的commit是基于这些block的拷贝进行的。所以transaction会有属于自己的block的拷贝。为了保证这里的效率,操作系统会使用copy-on-write(注,详见8.4)来避免不必要的拷贝,这样只有当对应的block在后面的transaction中被更新了,它在内存中才会实际被拷贝。

concurrency之所以能帮助提升性能,是因为它可以帮助我们并行的运行系统调用,我们可以得到多核的并行能力。如果我们可以在运行应用程序和系统调用的同时,来写磁盘,我们可以得到I/O concurrency,也就是同时运行CPU和磁盘I/O。这些都能帮助我们更有效,更精细的使用硬件资源。

Last updated