背景
大家都知道避免缺頁異常帶來的性能損耗最好的辦法是避免產生缺頁(我說了一句廢話。。。)。但實際上用戶態程序根本做不到這一點,而對於一個多線程程序而言這個問題尤其嚴重,所以內核就需要想盡辦法將缺頁異常處理帶來的額外開銷降到最低。於是乎最近一個八年老坑Speculative page-fault handling終於可能要到被考慮合併進主線的階段了。
Linux內核使用mmapsem來串行化對描述進程地址空間的數據結構的訪問,而缺頁異常的處理也需要訪問mmapsem。所以多線程程序在訪問內存方面的能力是嚴重受到獲取mmapsem讀/寫信號量的能力制約的。而由於線程數很多所產生的缺頁異常就更加容易發生資源爭用。為了解決這個問題,內核研發人員提出了Speculative page-fault handling,它的基本思路是當我們對進程的virtual memory areas (VMA) 進行訪問的時候不持有mmapsem,從而實現無鎖讀訪問。
Speculative page-fault handling的patch,第一次出現是在2009年(八年了,內核研發的效率啊!),之後斷斷續續地,多個內核開發者又對它進行了討論和改進,但相關工作一直沒有被merge進主線。Laurent Dufour最近重啟了這些工作,fix了這些patch的bug,添加上了自己的改進並重新提交到了郵件列表。之後大家就開始在linux-kernel的郵件列表上對這份工作展開了活躍的討論。一個令人激動的事實是,在Dufour的性能測試報告中,當我們啟用了Speculative page-fault handling後,讀取一個2TB的數據庫會得到20%的速度提升。
如上所述,mmapsem是多線程程序一個顯著的資源爭用點。而我們關注的缺頁處理也需要訪問進程的VMA結構。VMA結構描述了進程的內存佈局,因此要求缺頁處理時需要持有mmapsem。即使只要求讀鎖(我們討論的缺頁處理即為這種情況),我們也會頻繁訪問mmapsem,從而導致緩存顛簸(cache-line bouncing)以及性能下降。Speculative page-fault handling背後的思想是,通過避免在缺頁異常處理中使用mmapsem,無鎖遍歷VMA,從而提高內存訪問的性能。但是我們不得不說,設計和使用mmap_sem就是為了解決一些不好解決的同步問題,現在不持有鎖了,這些問題就得想其他的辦法了。
問題和解法
第一個問題是:假如不持有mmapsem,當我們處理缺頁異常的時候,如果對應的VMA描述中的區域發生了變化怎麼辦?應對這個問題的策略是,儘可能把與VMA狀態無關的工作都先做掉,然後在直接改變進程地址空間之前,再檢查一下VMA是否發生了改變。舉例來說,當我們從磁盤讀數據到內存的時候,我們可以先分配一個內存頁,將數據讀取出來,這些階段都是不需要mmapsem的,而當我們把這個頁加入到進程地址空間的時候我們需要一個一致的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時,內核會先無鎖地遍歷頁表,同時持有一個細粒度的頁表鎖。然後調用srcureadlock()以便進行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對於部分測試負荷有著顯著的性能提升,在這份工作最初的想法浮出水面八年後,我們有理由相信這份工作有望在不遠的未來被合併。希望更多的應用能夠最終收益。