https://lwn.net/Articles/730531/

Speculative page-fault handling(投机性缺页异常处理)的又一次尝试

大家都知道避免缺页异常带来的性能损耗最好的办法是避免产生缺页(我说了一句废话。。。)。但实际上用户态程序根本做不到这一点,而对于一个多线程程序而言这个问题尤其严重,所以内核就需要想尽办法将缺页异常处理带来的额外开销降到最低。于是乎最近一个八年老坑Speculative page-fault handling终于可能要到被考虑合并进主线的阶段了。

Linux内核使用mmap_sem来串行化对描述进程地址空间的数据结构的访问,而缺页异常的处理也需要访问mmap_sem。所以多线程程序在访问内存方面的能力是严重受到获取mmap_sem读/写信号量的能力制约的。而由于线程数很多所产生的缺页异常就更加容易发生资源争用。为了解决这个问题,内核研发人员提出了Speculative page-fault handling,它的基本思è·¯是当我们对进程的virtual memory areas (VMA) 进行访问的时候不持有mmap_sem,从而实现无锁读访问。

Speculative page-fault handling的patch,第一次出现是在2009年(八年了,内核研发的效率啊!),之后断断ç»­续地,多个内核开发者又对它进行了讨论和改进,但相关工作一直没有被merge进主线。Laurent Dufour最近重启了这些工作,fix了这些patch的bug,添加上了自己的改进并重新提交到了邮件列表。之后大家就开始在linux-kernel的邮件列表上对这份工作展开了活跃的讨论。一个令人激动的事实是,在Dufour的性能测试报告中,当我们启用了Speculative page-fault handling后,读取一个2TB的数据库会得到20%的速度提升。

如上所述,mmap_sem是多线程程序一个显著的资源争用点。而我们关注的缺页处理也需要访问进程的VMA结构。VMA结构描述了进程的内存布局,因此要求缺页处理时需要持有mmap_sem。即使只要求读锁(我们讨论的缺页处理即为这种情况),我们也会频繁访问mmap_sem,从而导致缓存颠簸(cache-line bouncing)以及性能下降。Speculative page-fault handling背后的思想是,通过避免在缺页异常处理中使用mmap_sem,无锁遍历VMA,从而提高内存访问的性能。但是我们不得不说,设计和使用mmap_sem就是为了解决一些不好解决的同步问题,现在不持有锁了,这些问题就得想其他的办法了。

第一个问题是:假如不持有mmap_sem,当我们处理缺页异常的时候,如果对应的VMA描述中的区域发生了变化怎么办?应对这个问题的策略是,尽可能把与VMA状态无关的工作都先做掉,然后在直接改变进程地址空间之前,再检查一下VMA是否发生了改变。举例来说,当我们从磁盘读数据到内存的时候,我们可以先分配一个内存页,将数据读取出来,这些阶段都是不需要mmap_sem的,而当我们把这个页加入到进程地址空间的时候我们需要一个一致的VMA,所以这个时候是需要拿mmap_sem的。

对于这个问题的解决,内核有一直都有一个机制叫seqlock。所以这套patch把seqlock添加到了VMA结构中,在所有改变VMA的地方都递增sequence count。Speculative fault-handling代码便可以在工作之前记录sequence number,并且在工作结束后检查sequence number有没有改变。如果sequence number改变了,我们可以知道VMA也改变了,投机进行的工作就当做白做了。这种情况就是失败的投机尝试,此时只能绕过Speculative fault-handling,并按照老办法重试处理缺页异常。

第二个问题要更棘手一些,没有持有mmap_sem,在处理缺页异常的时候,一个VMA可能会完全消失。这种情况可以使用Read-Copy-Update(RCU)来避免,使用RCU,可以保证在处理缺页异常的时候,VMA结构是存在的。当然因为缺页异常处理过程中的很多操作都可能会睡眠,所以我们要使用SRCU(RCU的一种可睡眠的变体)来串行化VMA的更新。

进行Speculative fault-handling时,内核会先无锁地遍历页表,同时持有一个细粒度的页表锁。然后调用srcu_read_lock()以便进行VMA查找。最后检查VMA的write-sequence count。未来遵守内核的锁使用顺序,我们需要先放弃页表锁,然后使用VMA来找到发生故障的地址所在的页。一旦页找到了,VMA需要重新验证一次,进行页表遍历、获取页表锁,并检查VMA的sequence number是否没有改变。如果没有改变,那么页被安装到页表里,页表锁也被释放。

speculative page-fault handling的另一个陷阱与Translation lookaside buffer(TLB)的失效有关。很多行为,例如unmapping一个内存区域,都会导致TLB的失效。失效TLB的过程是发送处理期间中断(IPI)来告诉每个CPU失效它自己的TLB。unmap的调用路径可能会在锁住特定的页表项期间进行TLB失效操作。此时,Speculative-fault-handling可能在关中断的情况下尝试获取页表锁,如果这种尝试的页表项被锁在unmap的路径上,处理器会在关中断的情况下自旋,因此永远收不到TLB失效的IPI,这将导致死锁。这是比mmap_sem竞争更坏的情况。了解清楚这个问题后,解决办法也显而易见:在speculative路径使用trylock操作获取锁,如果获取锁失败,则立即fall back到传统page fault处理流程上。

第一套speculative page fault相关的patch由Hiroyuki Kamezawa在2009年发布,接下来Peter Zijlstra组织了内核社区的讨论并开发了他自己的实现,他使用了RCU来完成无锁读VMA。不过Peter的实现也有一些问题,所以没能被合并进主干。到了2014年,由于很多之前导致他的patch不能工作的问题都已经解决了,所以Peter Zijlstra重启了这个想法。然而,讨论再次无疾而终。今年6月,Dufourt在最新内核移植了PeterZ的patch,同时也添加了自己的一些patch,然后重新发送到了邮件列表。不过Dufour提到,他的patch集里仍然存在TLB失效的问题。

抛开前面两个废弃掉的Speculative page fault的实现不说,这套patch目前的进展已经非常不错了,所以也许社区可以认真考虑一下合并进内核主线的事情。Dufour的测试中关于数据库读取性能的提高也引起内核其他开发者,如Michal Hocko的注意,Hocko追问Dufour是否在别的benchmark,例如kernbench或其他的高度多线程的测试负载上测试过这套patch的收益。作为回应,今年8月8日Dufour给出了很多不同benchmark的测试结果,显示了针对不同的测试,结果也有差异(有的提升显著,有的基本没有提升)。

到此为止,这套patch的主要问题已经都解决了。鉴于speculative page-fault handling对于部分测试负荷有着显著的性能提升,在这份工作最初的想法浮出水面八年后,我们有理由相信这份工作有望在不远的未来被合并。希望更多的应用能够最终收益。