vma, anon_vma和anon_vma_chain的聯系
本文主要參考了vma, anon_vma和anon_vma_chain的聯系這篇文章,結合相關資料,對該文進行了一些改進。
Linux提供了內存映射這一特性,它實現了把物理內存頁映射(map)到進程的地址空間中, 以實現高效的數據操作或傳輸。內核在處理這一特性時, 使用了struct vm_area_struct
, struct anon_vma
和struct anon_vma_chain
這三個重要數據結構, 所以理解這三個數據結構是重中之重, 本文試圖厘清這三者的來歷與聯系。
vma
struct vm_area_struct在內核代碼中常被簡稱為vma
, 所以下文以vma
指稱這一結構。
vma
是內存映射的單位, 它表示進程地址空間中的一個連續的區間, 其中字段vm_start
和vm_end
標明這塊連續區間的起始虛擬地址。在使用mmap
系統調用創建映射時, 用戶指定起始地址(可選)和長度, 內核將據此尋找進程地址空間中符合條件的合法vma
以供映射。cat /proc/<pid>/maps
可以查看某一進程的所有映射區間。
anon_vma
anon_vma的引入需要一番解釋。
反向映射的引入
當Linux系統內存不足時, swap子系統會釋放一些頁面, 交換到交換設備中, 以空出多余的內存頁。虛擬內存的理念就是通過頁表來維護虛擬地址到物理地址的映射。但是, 頁表是種單向映射, 即通過虛擬地址查找物理地址很容易, 但反之通過物理地址查找虛擬地址則很麻煩。這種問題在共享內存的情況下更加嚴重。而swap子系統在釋放頁面時就遇到這個問題, 對於特定頁面(物理地址), 要找到映射到它的頁表項(PTE)
, 並修改PTE, 以使其指向交換設備中的該頁的位置。在2.4之前的內核中, 這是件費時的工作, 因為內核需要遍歷每一個進程的所有頁表, 以找出所有映射該頁的頁表項。
解決這一問題的做法是引入反向映射(reverse mapping)這一概念。該做法就是為每一個內存頁(struct page
)維護一個數據結構, 其中包含所有映射到該頁的PTE
, 這樣在尋找一個內存頁的反向映射時只要掃描這個結構即可, 大大提高了效率。這正是Rik van Riel的做法, 他在struct page
中增加了一個pte_chain
的字段, 它是一個指向所有映射到該頁的PTE的鏈表指針。
當然, 它是有代價的。
-
每個
struct page
都增加了一個字段, 而系統中每個內存頁都對應一個struct page
結構, 這意味着相當數量的內存被用來維護這個字段。而struct page
是重要的內核數據結構, 存放在有限的低端內存中, 增加一個字段浪費了大量的保貴低端內存, 而且, 當物理內存很大時, 這種情況更突出, 這引起了伸縮性(scalability)問題。 -
其它一些需要操作大量頁面的函數慢下來了。
fork()
系統調用就是一個。由於Linux采取寫時復制(COW, Copy On Write)的語義, 意味着新進程共享父進程的頁表, 這樣, 進程地址空間內的所有頁都新增了一個PTE指向它, 因此, 需要為每個頁新增一個反向映射, 這顯著地拖慢了速度。
基於對象的反向映射
這種代價顯然是不能容忍的, 於是, Dave McCracken提出了一個叫做基於對象的反向映射(object-based reverse mapping)的解決方案。他的觀察是, 前面所述的代價來源於反向映射字段的引入, 而如果存在可以從struct page
中獲取映射到該頁面的所有頁表項, 這個字段就不需要了, 自然不需要付出這些代價。他確實找到了一種方法。
Linux的用戶態內存頁大致分兩種使用情況:
-
其中一大部分叫做文件后備頁(file-backed page), 顧名思義, 這種內存頁的內容關聯着后備存儲系統中的文件, 比如程序的代碼, 比如普通的文本文件, 這種內存頁使用時一般通過上述的
mmap
系統調用映射到地址空間中, 並且, 在內存緊張時, 可以簡單地丟棄, 因為可以從后備文件中輕易的恢復。 -
一種叫匿名頁(anonymous page), 這是一種普通的內存頁, 比如棧或堆內存就屬於這種, 這種內存頁沒有后備文件, 這也是其稱為匿名的緣故。
Dave的方案中的對象指的就是第一種內存頁的后備文件。他通過后備文件對象, 以迂回的方式算出PTE,在本文中就不做過多的介紹。
匿名頁的反向映射
Dave的方案只解決了第一種內存頁的反向映射, 於是, Andrea Arcangeli順着Dave的思路, 給出了匿名頁的反向映射解決方案。
如前所述, 匿名頁沒有所謂的后備文件, 但是, 匿名頁有個特點, 就是它們都是私有的, 而非共享的(比如棧, 椎內存都是獨立每個進程的, 非共享的)。這意味着, 每一個匿名內存頁, 只有一個PTE關聯着它, 也就是只有一個vma關聯着它。Andrea的方案是復用struct page
的mapping
字段, 因為對於匿名頁, mapping
為null
, 不指向后備空間。復用方法是利用C語言的union
, 在匿名頁的情況下,mapping
字段不是指向struct address_space
的指針, 而是指向關聯該內存頁的唯一的vma
。由此, 也可以方便地計算出PTE來。
但是, 事情並不是如此簡單。當進程被fork復制時, 前面已經說過, 由於COW的語義, 新進程只是復制父進程的頁表, 這意味着現在一個匿名頁有兩個頁表指向它了, 這樣, 上面的簡單復用mapping
字段的做法不適用了, 因為一個指針, 如何表示兩個vma呢。
Andrea的做法就是多加一層。新創建一個struct anon_vma
結構, 現在mapping
字段是指向它了, 而anon_vma
中, 不出意料的, 包含一個鏈表, 鏈接起所有的vma
。每當進程fork一個子進程, 子進程由於COW機制會復制父進程的vma
, 這個新vma
就鏈接到父進程中的anon_vma
中。這樣, 每次unmap一個內存頁時, 通過mapping
字段指向的anon_vma
, 就可以找到可能關聯該頁的vma
鏈表, 遍歷該鏈表, 就可以找到所有映射到該匿名頁的PTE。
這也有代價, 那就是
- 每個
struct vm_area_struct
結構多了一個list_head
結構字段用以串起所有的vma
。 - 需要額外為
anon_vma
結構分配內存。
但是, 這種方案所需要的內存遠小於前面所提的在每個struct page
中增加一個反向映射字段來得少, 因此是可以接受的。
以上, 便介紹完了anon_vma
結構的來由和作用。
anon_vma_chain
anon_vma
結構的提出, 完善了反向映射機制, 一路看來, 無論是效率還是內存使用, 都有了提升, 應該說是很完美的一套解決方案。但現實不斷提出難題。一開始提到的Rik van Riel就舉了一種工作負載(workload)的例子來反駁說該方案有缺陷。
前面的匿名頁反向映射機制在解除一頁映射時, 通過訪問anon_vma
訪問vma
鏈表, 遍歷整個vma
鏈表, 以查找可能映射到該頁的PTE。但是, 這種方法忽略了一點: 當進程fork而復制產生的子進程中的vma
如果發生了寫訪問, 將會分配新的匿名頁, 把該vma
指向這個新的匿名頁, 這個vma
就跟原來的那個匿名頁沒有關系了, 但原來的vma
鏈表卻沒反映出這種變化, 從而導致了對該vma
不必要的檢查。 Rik舉的例子正是對這種極端情況的描述。
Rik采取的方案是又增加一層, 新增了一個結構叫anon_vma_chain:
1 |
|
每個anon_vma_chain
(AVC)維護兩個鏈表
- same_vma:與給定
vma
相關聯的所有anon_vma
- same_anon_vma:與給定
anon_vma
相關聯的所有vma
最初,我們有一個進程與一個匿名vma
:
這里,“AV”是anon_vma
,“AVC”是上面看到的anon_vma_chain
。 AVC直接通過指針鏈接到anon_vma
和vma
。 (藍色)鏈表是same_anon_vma鏈表,而(紅色)鏈表是same_vma鏈表。
想象一下,這個進程進行了fork操作,導致子進程復制了vma
; 現在有了一個孤立的新vma
:
內核需要將此vma
鏈接到父進程的anon_vma
中; 這需要添加一個新的anon_vma_chain
:
請注意,新的AVC已被添加到same_anon_vma鏈表中。 新的vma
也需要自己的anon_vma
:
現在還有另一個anon_vma_chain
鏈接在新的anon_vma
中。 新的AVC已被添加到same_vma鏈表中。
此刻,根據上圖,可以驗證anon_vma_chain
(AVC)中兩個鏈表的作用。
The “same_vma” list contains the anon_vma_chains linking all the anon_vmas associated with this VMA.
The “same_anon_vma” list contains the anon_vma_chains which link all the VMAs associated with this anon_vma.
當子進程寫內存頁時,發生COW, 子進程的vma
將指向自己匿名頁, 同時, 這個新的匿名頁指向子進程的anon_vma
(此時same_anon_vma鏈與same_vma鏈解除)。
這樣, 在解除一頁映射時, 對於子進程自己的匿名頁, 只要遍歷子進程自己的anon_vma
下的vma
鏈表即可; 擁有大量子進程的父進程對於共享的頁(未發生COW), 則按原來的方法遍歷, 對於子進程自己的匿名頁,父進程則不需要訪問對應的vma
,這樣大大減少了父進程需要遍歷的vma
。
再看anon_vma_chain
這個名字, 它就像個粘合劑, 也像個鏈條, 把初始時父,子進程關聯的vma
和anon_vma
鏈接起來, 當子進程通過COW擁有自己的匿名頁后, 會發生解鏈, 以分冶策略各自管理, 從而使得在解除一頁映射時, 減少了父進程遍歷的vma
數目, 也減少了相應的鎖沖突, 因而提高了效率。
參考資料: