基于上面的系统调用的结构,接下来我将介绍commit transaction完整的步骤。每隔5秒,文件系统都会commit当前的open transaction,下面是commit transaction涉及到的步骤:
- 首先需要阻止新的系统调用。当我们正在commit一个transaction时,我们不会想要有新增的系统调用,我们只会想要包含已经开始了的系统调用,所以我们需要阻止新的系统调用。这实际上会损害性能,因为在这段时间内系统调用需要等待并且不能执行。
- 第二,需要等待包含在transaction中的已经开始了的系统调用们结束。所以我们需要等待transaction中未完成的系统调用完成,这样transaction能够反映所有的写操作。
- 一旦transaction中的所有系统调用都完成了,也就是完成了更新cache中的数据,那么就可以开始一个新的transaction,并且让在第一步中等待的系统调用继续执行。所以现在需要为后续的系统调用开始一个新的transaction。
- 还记得ext3中的log包含了descriptor,data和commit block吗?现在我们知道了transaction中包含的所有的系统调用所修改的block,因为系统调用在调用get函数时都将handle作为参数传入,表明了block对应哪个transaction。接下来我们可以更新descriptor block,其中包含了所有在transaction中被修改了的block编号。
- 我们还需要将被修改了的block,从缓存中写入到磁盘的log中。之前有同学问过,新的transaction可能会修改相同的block,所以在这个阶段,我们写入到磁盘log中的是transaction结束时,对于相关block cache的拷贝。所以这一阶段是将实际的block写入到log中。
- 接下来,我们需要等待前两步中的写log结束。
- 之后我们可以写入commit block。
- 接下来我们需要等待写commit block结束。结束之后,从技术上来说,当前transaction已经到达了commit point,也就是说transaction中的写操作可以保证在面对crash并重启时还是可见的。如果crash发生在写commit block之前,那么transaction中的写操作在crash并重启时会丢失。
- 接下来我们可以将transaction包含的block写入到文件系统中的实际位置。
- 在第9步中的所有写操作完成之后,我们才能重用transaction对应的那部分log空间。
在一个非常繁忙的系统中,log的头指针一直追着尾指针在跑(注,也就是说一直没有新的log空间)。在当前最早的transaction的所有步骤都完成之前,或许不能开始commit一个新的transaction,因为我们需要重复利用最早的transaction对应的log空间。不过人们通常会将log设置的足够大,让这种情况就不太可能发生。
学生提问:你刚刚说没有进程会等待这些步骤完成,那么这些步骤是在哪里完成的呢?
Robert教授:这些是在后台的内核线程完成的。
学生提问:我有个有关重用log空间的问题,假设我们使用了一段特定的log空间,并且这段log空间占据了是刚刚释放出来的所有log空间,但是还不够,那么文件系统会等待另一部分的log空间释放出来吗,还是会做点别的?
Robert教授:是的,会等待。让我画张图来确保我回答的是正确的问题。我们可以认为log是磁盘中的一段线性空间,假设现存的transaction中最早的是T7,之后是T8,T9,我们想要将T10放在T9之后的空闲区域。
我们或许要等待T7将所有的block写入到文件系统对应的位置,这样我们才能释放T7对应的空间。这意味着T10中的步骤需要暂停以等待T7释放出来。这是你的问题吗?
同一个学生:是的,所以可能是这样,我先写入T10的block到现有的log空闲区域,但是如果最后log足够大并且我们用光了空闲区域,我们就需要等待T7的空间被释放出来,是吗?
Robert教授:是的,如果需要写入的数据足够多,并且log迅速的用光了。我们甚至都不能在释放log空间之前开始新的系统调用。如果你们关注细节的话,这里会有一些潜在的死锁。首先系统调用需要预声明需要多少个block,这样logging系统才知道对于该transaction需要多少log空间,因为我们不会在没有足够空间来commit transaction时,开始一个新的transaction(注,难道不能将不能写入到磁盘log中的transaction先缓存在内存中吗?虽然这样可能会导致堆积)。
学生提问:如果新的transaction需要的空间走到了T8,那么现在就需要等待T7,T8结束,这是怎么工作的呢?
Robert教授:图中的T7,T8,T9其中的系统调用都完成了,并且都已经在commit到log中了。在上面的图中,我们会直接开始T10,新的系统调用会写入到transaction T10,最终当T10需要commit到log中,并且它大到需用用到T8的空间时,它需要等待T7,T8结束。文件系统会记录每个transaction的大小,这样文件系统就知道要等待多少个之前的transaction结束。所以这里还有不少的记录工作,这样文件系统才能理解所有旧的transaction的状态。
有关如何重用log空间,这里有个小细节。在log的最开始有一个super block,所以在任何时候log都是由一个super block和一些transaction组成。假设T4是最新的transaction,之前是T1,T2,T3。
我们是否能重用一段log空间,取决于相应的transaction,例如T2,是否已经commit并且写入到文件系统的实际位置中,这样在crash并重启时就不需要重新执行这段transaction了。同时也取决于T2之前的的所有transaction是否已经被释放了。所有的这些条件都满足时,我们就可以释放并重用T2对应的log空间。