16.8 为什么新transaction需要等前一个transaction中系统调用执行完成

以上就是ext3中相对来说直观的部分。实际上还有一些棘手的细节我想讨论一下。之前我提到过,ext3中存在一个open transaction,但是当ext3决定要关闭该transaction时,它需要等待该transaction中的所有系统调用都结束,之后才能开始新的transaction。假设我们现在有transaction T1,其中包含了多个系统调用。

如果我们想要关闭T1,我们需要停止接收新的系统调用,因为我们想要等待现有的系统调用结束,这样才能commit transaction。所以直到这些系统调用都结束了,在ext3中不能允许开始任何新的系统调用。所以只有在T1中的系统调用完成之后,才能开始在接下来的transaction T2中接收系统调用。在这之间有一段时间,新的系统调用是被拦截的,这降低了性能,因为我们本来应该执行系统调用的但是又不被允许。

这里的问题是,直到T1中所有的系统调用都结束之前,ext3为什么不让T2中的系统调用开始执行呢?让我们来看一下没有这个限制条件可能会带来的错误的场景。我们假设T1只包含了一个系统调用,这是一个create系统调用用来创建文件x。在create系统调用结束之前,文件系统决定开始一个新的transaction T2用来接收create之后的所有系统调用。我们假设T2在T1结束之前就开始了,T2对另一个文件y调用了unlink系统调用。unlink会释放与y关联的inode。

假设在下面的时间点T2将inode标记为空闲的,create会为x分配inode,或许它在之后的一个时间点分配了inode。

因为create在unlink释放inode之后分配的inode,它可能会重用同一个inode,所以x可能会获得y的inode,假设是inode 17。目前为止没有问题,因为unlink本来就是释放inode。当T1中的create结束之后,我们会关闭T1,在最后我们会将T1的所有更新都写入到磁盘的log中。之后unlink还要花点时间才能结束,但是在它结束之前计算机crash了。

在重启并运行恢复软件时,可以发现T1已经commit了,而T2没有。所以恢复软件会完全忽略T2,这意味着T2中的unlink就跟没有发生过一样,恢复软件不会执行T2中的unlink,也就不会删除文件y。所以crash并重启之后y文件仍然存在,并还在使用inode 17。然而T1又完成了,x文件使用的也是inode 17,所以现在我们错误的有了两个文件都使用了相同的inode,这意味着它们共享了文件内容,向一个文件写数据会神奇的出现在另一个文件中。这完全是错误的,因为我们本来想的是删除y,并为x分配一个空闲的inode,而不是一个已经在使用中的inode。这里可以这么想,T2中的unlink修改了一个block,最终这个修改过的block被前一个transaction所使用。T2中修改的信息,被T1所使用了,这意味着我们丢失了T2的原子性。因为T2的目标是unlink的效果要么是全发生,要么是完全不发生。但是刚刚的例子中,因为T1使用了T2中释放的inode,这意味着T2中部分修改已经生效了,但是其他的修改随着crash又丢失了。

或许你可以想到一些修复这里问题的方法,或许T1可以发现inode是由后一个transaction释放的而不去使用它。而ext3采用了一个非常简单的方法,在前一个transaction中所有系统调用都结束之前,它不允许任何新的系统调用执行。所以transaction T1也就不可能看到之后的transaction包含的更新。因为直到T1 commit了,整个unlink都不被允许执行。

学生提问:当你关闭一个open transaction时,具体会发生什么呢?会对当前的缓存做一个快照吗?

Robert教授:会的,当我们关闭一个transaction,文件系统会拷贝被transaction中的系统调用所修改的所有block,之后transaction才会commit这些block。后面的transaction会在真正的block cache上运行。当将block都commit到log之后,对于block cache的拷贝就可以丢弃了。

以上是众多ext3需要处理的小细节之一,因为为了支持并发,ext3需要处理各种各样的特殊细节,但是我们没有时间讨论所有的细节。

Last updated