天天看點

一文讀懂記憶體管理中TLB:位址轉換後援緩沖器

作者:深度Linux

前一章節,我們學習了分頁機制的硬體原理,從虛拟記憶體位址到實體記憶體位址的轉換,我們通過頁表來處理。為了節約頁表的記憶體存儲空間,我們會使用多級頁表。但是,多級頁表雖然節約了我們的存儲空間,但是卻存在問題:

原本我們對于隻需要進行一次位址轉換,隻需要通路一次記憶體就能找到對應的實體頁号了,算出實體位址

現在我們需要多次通路記憶體,才能找到對應的實體頁号。

最終帶來了時間上的開銷,變成了一個“以時間換空間”的政策,極大地限制了記憶體通路性能問題。是以為了解決這種問題導緻處理器性能下降的問題,計算機工程師們專門在 CPU 裡放了一塊緩存晶片,這塊緩存晶片我們稱之為TLB,全稱是位址變換高速緩沖(Translation-Lookaside Buffer)

一,TLB介紹

TLB是Translation Lookaside Buffer的簡稱,可翻譯為“位址轉換後援緩沖器”,也可簡稱為“快表”。簡單地說,TLB就是頁表的Cache,屬于MMU的一部分,其中存儲了目前最可能被通路到的頁表項,其内容是部分頁表項的一個副本。處理器在取指或者執行通路memory指令的時候都需要進行位址翻譯,即把虛拟位址翻譯成實體位址。而位址翻譯是一個漫長的過程,需要周遊幾個level的Translation table,進而産生嚴重的開銷。為了提高性能,我們會在MMU中增加一個TLB的單元,把位址翻譯關系儲存在這個高速緩存中,進而省略了對記憶體中頁表的通路。

一文讀懂記憶體管理中TLB:位址轉換後援緩沖器

TLB存放了之前已經進行過位址轉換的查詢結果。這樣,當同樣的虛拟位址需要進行位址轉換的時候,我們可以直接在 TLB 裡面查詢結果,而不需要多次通路記憶體來完成一次轉換。

TLB其實本質上也是一種cache,既然是一種cache,其目的就是為了提供更高的performance。而與我們知道的指令cache和資料cache又有什麼不同呢?

1.指令cache:解決cpu擷取main memory中的指令資料的速度比較慢的問題而設立

2.資料cache:解決cpu擷取main memory中的資料的速度比較慢的問題而設立

Cache為了更快的通路main memory中的資料和指令,而TLB是為了更快的進行位址翻譯而将部分的頁表内容緩存到了Translation lookasid buffer中,避免了從main memory通路頁表的過程。

更多Linux核心視訊教程文檔資料免費領取背景私信【核心】自行擷取。

一文讀懂記憶體管理中TLB:位址轉換後援緩沖器

二,TLB的轉換過程

TLB中的項由兩部分組成:

  • 辨別區:存放的是虛位址的一部
  • 資料區:存放實體頁号、存儲保護資訊以及其他一些輔助資訊

對于資料區的輔助資訊包括以下内容:

  1. 有效位(Valid):對于作業系統,所有的資料都不會加載進記憶體,當資料不在記憶體的時候,就需要到硬碟查找并加載到記憶體。當為1時,表示在記憶體上,為0時,該頁不在記憶體,就需要到硬碟查找。
  2. 引用位(reference):由于TLB中的項數是一定的,是以當有新的TLB項需要進來但是又滿了的話,如果根據LRU算法,就将最近最少使用的項替換成新的項。故需要引用位。同時要注意的是,頁表中也有引用位。
  3. 髒位(dirty):當記憶體上的某個塊需要被新的塊替換時,它需要根據髒位判斷這個塊之前有沒有被修改過,如果被修改過,先把這個塊更新到硬碟再替換,否則就直接替換。
一文讀懂記憶體管理中TLB:位址轉換後援緩沖器

下面我們來看一下,當存在TLB的通路流程:

當CPU收到應用程式發來的虛拟位址後,首先去TLB中根據标志Tag尋找頁表資料,假如TLB中正好存放所需的頁表并且有效位是1,說明TLB命中了,那麼直接就可以從TLB中擷取該虛拟頁号對應的實體頁号。

假如有效位是0,說明該頁不在記憶體中,這時候就發生缺頁異常,CPU需要先去外存中将該頁調入記憶體并将頁表和TLB更新

假如在TLB中沒有找到,就通過上一章節的方法,通過分頁機制來實作虛拟位址到實體位址的查找。

如果TLB已經滿了,那麼還要設計替換算法來決定讓哪一個TLB entry失效,進而加載新的頁表項。

引用位、髒位何時更新?

1. 如果是TLB命中,那麼引用位就會被置1,當TLB或頁表滿時,就會根據該引用位選擇适合的替換位置

2. 如果TLB命中且這個訪存操作是個寫操作,那麼髒位就會被置1,表明該頁被修改過,當該頁要從記憶體中移除時會先執行将該頁寫會外存的操作,保證資料被正确修改。

三,如何确定TLB match

我們選擇Cortex-A72 processor來描述ARMv8的TLB的組成結構以及維護TLB的指令

一文讀懂記憶體管理中TLB:位址轉換後援緩沖器

A72實作了2個level的TLB,綠色是L1 TLB,包括L1 instruction TLB(48-entry fully-associative)和L1 data TLB(32-entry fully-associative)。

黃色block是L2 unified TLB,它要大一些,可以容納1024個entry,是4-way set-associative的。當L1 TLB發生TLB miss的時候,L2 TLB是它們堅強的後盾

通過上圖,我們還可以看出:對于多核CPU,每個processor core都有自己的TLB。

一文讀懂記憶體管理中TLB:位址轉換後援緩沖器

假如不做任何的處理,那麼在程序A切換到程序B的時候,TLB和Cache中同時存在了A和B程序的資料。對于kernel space其實無所謂,因為所有的程序都是共享的,對于A和B程序,它們各種有自己的獨立的使用者位址空間,也就是說,同樣的一個虛拟位址X,在A的位址空間中可以被翻譯成Pa,而在B位址空間中會被翻譯成Pb,如果在位址翻譯過程中,TLB中同時存在A和B程序的資料,那麼舊的A位址空間的緩存項會影響B程序位址空間的翻譯

是以,在程序切換的時候,需要有tlb的操作,以便清除舊程序的影響,具體怎樣做呢?

當系統發生程序切換,從程序A切換到程序B,進而導緻位址空間也從A切換到B,這時候,我們可以認為在A程序執行過程中,所有TLB和Cache的資料都是for A程序的,一旦切換到B,整個位址空間都不一樣了,是以需要全部flush掉

這種方案當然沒有問題,當程序B被切入執行的時候,其面對的CPU是一個幹幹淨淨,從頭開始的硬體環境,TLB和Cache中不會有任何的殘留的A程序的資料來影響目前B程序的執行。當然,稍微有一點遺憾的就是在B程序開始執行的時候,TLB和Cache都是冰冷的(空空如也),是以,B程序剛開始執行的時候,TLB miss和Cache miss都非常嚴重,進而導緻了性能的下降。我們管這種空TLB叫做cold TLB,它需要随着程序的運作warm up起來才能慢慢發揮起來效果,而在這個時候有可能又會有新的程序被排程了,而造成TLB的颠簸效應。

我們采用程序位址空間這樣的術語,其實它可以被進一步細分為核心位址空間和使用者位址空間。對于所有的程序(包括核心線程),核心位址空間是一樣的,是以對于這部分位址翻譯,無論程序如何切換,核心位址空間轉換到實體位址的關系是永遠不變的,其實在程序A切換到B的時候,不需要flush掉,因為B程序也可以繼續使用這部分的TLB内容(上圖中,橘色的block)。對于使用者位址空間,各個程序都有自己獨立的位址空間,在程序A切換到B的時候,TLB中的和A程序相關的entry(上圖中,青色的block)對于B是完全沒有任何意義的,需要flush掉。

在這樣的思路指導下,我們其實需要區分global和local(其實就是process-specific的意思)這兩種類型的位址翻譯,是以,在頁表描述符中往往有一個bit來辨別該位址翻譯是global還是local的,同樣的,在TLB中,這個辨別global還是local的flag也會被緩存起來。有了這樣的設計之後,我們可以根據不同的場景而flush all或者隻是flush local tlb entry。

四,多核的TLB操作

完成單核場景下的分析之後,我們一起來看看多核的情況。程序切換相關的TLB邏輯block示意圖如下

一文讀懂記憶體管理中TLB:位址轉換後援緩沖器

在多核系統中,程序切換的時候,TLB的操作要複雜一些,主要原因有兩點:其一是各個cpu core有各自的TLB,是以TLB的操作可以分成兩類,一類是flush all,即将所有cpu core上的tlb flush掉,還有一類操作是flush local tlb,即僅僅flush本cpu core的tlb。另外一個原因是程序可以排程到任何一個cpu core上執行(當然具體和cpu affinity的設定相關),進而導緻task處處留情(在各個cpu上留有殘餘的tlb entry)。

我們了解到位址翻譯有global(各個程序共享)和local(程序特定的)的概念,因而tlb entry也有global和local的區分。如果不區分這兩個概念,那麼程序切換的時候,直接flush該cpu上的所有殘餘。這樣,當程序A切出的時候,留給下一個程序B一個清爽的tlb,而當程序A在其他cpu上再次排程的時候,它面臨的也是一個全空的TLB(其他cpu的tlb不會影響)。當然,如果區分global 和local,那麼tlb操作也基本類似,隻不過程序切換的時候,不是flush該cpu上的所有tlb entry,而是flush所有的tlb local entry就OK了。

五,PCID

按照這種思路走下去,那就要思考,有沒有别的辦法能夠不重新整理TLB呢?有辦法的,那就是PCID。

PCID(程序上下文辨別符)是在Westmere架構引入的新特性。簡單來說,在此之前,TLB是單純的VA到PA的轉換表,程序1和程序2的VA對應的PA不同,不能放在一起。加上PCID後,轉換變成VA + 程序上下文ID到PA的轉換表,放在一起完全沒有問題了。這樣程序1和程序2的頁表可以和諧的在TLB中共處,程序在它們之前切換完全不需要預熱了!

是以新的加載CR3的過程變成了:如果CR4的PCID=1,加載CR3就不需要Flush TLB。

六,TLB shootdown

一切看起來很美好,PCID這個在多年前就有了的技術,現在已經在每個Intel CPU中生根了,那麼是不是已經被廣泛使用了呢?而實際的情況是Linux在2017年底才在4.15版中真正全面使用了PCID(盡管在4.14中開始部分引入PCID,見參考資料1),這是為什麼呢?

PCID這麼好的技術也有副作用。在它之前的日子裡,Linux在多核CPU上排程程序時候,因為每次程序排程都會刷掉程序使用者空間的TLB,并沒有什麼問題。如果支援PCID的話,TLB操作變得很簡單,或者說我們沒有必要去執行TLB的操作,因為在TLB的搜尋的時候已經區分各個程序,這樣TLB不會影響其他任務的執行。

在單核系統中,這樣的操作确實能夠獲得很好的性能,例如場景為A—>B—>A,如果TLB足夠大,TLB再兩個程序中反複切換,極大的提升了性能。

但是在多核系統重,如果CPU支援PCID,并且在程序切換的時候不flush tlb,那麼系統中各個CPU中的TLB entry則保留各個程序的TLB entry,當在某個CPU上,一個程序被銷毀了,或者該程序修改了自己的頁表的時候,就必須将該程序的TLB從系統中請出去。這時候,不僅僅需要flush本CPU上對應的TLB entry,還需要flush其他CPU上和該程序相關的殘餘。而這個動作就需要通過IPI實作,進而引起了系統開銷,此外PCID的配置設定和管理也會帶來額外的開銷。再加上PCID裡面的上下文ID長度有限,隻能夠放得下4096個程序ID,這就需要一定的管理以便申請和放棄。如此種種,導緻Linux系統在應用PCID上并不積極,直到不得不這樣做。

七.,結論

TLB的引入解決了分頁機制的性能問題,但是如何提高TLB的性能問題,但是如何提高TLB的命中确成為一個新的技術難題,對于X86提供了PCID的方式,而ARM采用的ASID技術,但是對于現在日益複雜的應用場景,這些都未能徹底的解決這些問題。

繼續閱讀