Linux內存管理之頁面回收 .
Linux內存管理之頁面回收 .
請求調頁機制,只要用戶態進程繼續執行,他們就能獲得頁框,然而,請求調頁沒有辦法強制進程釋放不再使用的頁框。因此,遲早所有空閑內存將被分配給進程和高速緩存,Linux內核的頁面回收演算法(PFRA)採取從用戶進程和內核高速緩存「竊取」頁框的辦法不從夥伴系統的空閑塊列表。
實際上,在用完所有空閑內存之前,就必須執行頁框回收演算法。否則,內核很可能陷入一種內存請求的僵局中,並導致系統崩潰。也就是說,要釋放一個頁框,內核就必須把頁框的數據寫入磁碟;但是,為了完成這一操作,內核卻要請求另一個頁框(例如,為I/O數據傳送分配緩衝區首部)。因為不存在空閑頁框,因此,不可能釋放頁框。
頁框演算法的目標之一就是保存最少的空閑頁框並使內核可以安全地從「內存緊缺」的情形中恢復過來。
選擇目標頁
PFRA的目標就是獲得頁框並使之空閑。PFRA按照頁框所含內容,以不同的方式處理頁框。我們將他們區分成:不可回收頁、可交換頁、可同步頁和可丟棄頁:
頁類型
說明
回收操作
不可回收頁
空閑頁(包含子夥伴系統列表中)
保留頁(PG_reserved標誌置位)
內核動態分配頁
進程內核態堆棧頁
臨時鎖定頁(PG_locked標誌置位)
內存鎖定頁(在先行區中且VM_LOCKED標誌置位)
不允許也無需回收
可回收頁
用戶太地址空間的匿名頁
Tmpfs文件系統的映射頁(如IPC共享內存的頁)
將頁的內容保存在交換區
可同步頁
用戶態地址空間的映射頁
存有磁碟文件數據且在頁高速緩存中的頁
塊設備緩衝區頁
某些磁碟高速緩存的頁(如索引節點高速緩存)
必要時,與磁碟鏡像同步這些頁
可丟棄頁
內存高速緩存中的未使用頁(如slab分配器高速緩存)
目錄想高速緩存的未使用頁
無需操作
進行頁面回收的時機
Linux 操作系統使用如下這兩種機制檢查系統內存的使用情況,從而確定可用的內存是否太少從而需要進行頁面回收。
◦周期性的檢查:這是由後台運行的守護進程 kswapd 完成的。該進程定期檢查當前系統的內存使用情況,當發現系統內空閑的物理頁面數目少於特定的閾值時,該進程就會發起頁面回收的操作。
◦「內存嚴重不足」事件的觸發:在某些情況下,比如,操作系統忽然需要通過夥伴系統為用戶進程分配一大塊內存,或者需要創建一個很大的緩衝區,而當時系統中的內存沒有辦法提供足夠多的物理內存以滿足這種內存請求,這時候,操作系統就必須儘快進行頁面回收操作,以便釋放出一些內存空間從而滿足上述的內存請求。這種頁面回收方式也被稱作「直接頁面回收」。
◦睡眠回收,在進入suspend-to-disk狀態時,內核必須釋放內存。
如果操作系統在進行了內存回收操作之後仍然無法回收到足夠多的頁面以滿足上述內存要求,那麼操作系統只有最後一個選擇,那就是使用 OOM( out of memory )killer,它從系統中挑選一個最合適的進程殺死它,並釋放該進程所佔用的所有頁面。
上面介紹的內存回收機制主要依賴於三個欄位:pages_min,pages_low 以及 pages_high。每個內存區域( zone )都在其區域描述符中定義了這樣三個欄位,這三個欄位的具體含義如下表 所示。
欄位含義
名稱
欄位描述
pages_min
區域的預留頁面數目,如果空閑物理頁面的數目低於 pages_min,那麼系統的壓力會比較大,此時,內存區域中急需空閑的物理頁面,頁面回收的需求非常緊迫。
pages_low
控制進行頁面回收的最小閾值,如果空閑物理頁面的數目低於 pages_low,那麼操作系統內核會開始進行頁面回收。
pages_high
控制進行頁面回收的最大閾值,如果空閑物理頁面的數目多於 pages_high,則內存區域的狀態是理想的。
PFRA設計
設計總則
1. 首先釋放「無害」頁,即必須線回收沒有被任何進程使用的磁碟與內存高速緩存中的頁;
2. 將用戶態進程和所有頁定為可回首頁,FPRA必須能夠竊得人任何用戶態進程頁,包括匿名頁。這樣,睡眠較長時間的進程將逐漸失去所有頁;
3. 同時取消引用一個共享頁的所有頁表項的映射,就可以回收該共享頁;
4. 只回收「未用」頁,使用LRU演算法。Linux使用每個頁表項中的訪問標誌位,在頁被訪問時,該標誌位由銀獎自動置位;而且,頁年齡由頁描述符在鏈表(兩個不同的鏈表之一)中的位置來表示。
因此,頁框回收演算法是集中啟髮式方法的混合:
1. 謹慎選擇檢查高速緩存的順序;
2. 基於頁年齡的變化排序;
3. 區別對待不同狀態的頁;
反向映射
PFRA的目標之一是能釋放共享頁框。為達到這個目地。Linux內核能夠快速定為指向同一頁框的所有頁表項。這個過程就叫做反向映射。Linux 操作系統為物理頁面建立一個鏈表,用於指向引用了該物理頁面的所有頁表項。
基本思想如下圖:
Linux採用「面向對象的反向映射」技術。實際上,對任何可回收的用戶態頁,內核保留系統中該頁所在所有現行區(「對象」)的反向鏈接,每個線性區描述符( vm_area_struct 結構)存放一個指針指向一個內存描述符( mm_struct 結構),而該內存描述符又包含一個指針指向一個頁全局目錄(PGD)。因此,這些反向鏈接使得PFRA能夠檢索引用某頁的所有頁表項。因為線性區描述符比頁描述符少,所以更新共享頁的反向鏈接就比較省時間。下面是具體的實現:
基於對象的反向映射的實現
數據結構
首先,PFRA必須要確定待回收頁是共享的還是非共享的,以及是映射頁或是匿名頁。為做到這一點,內核要查看頁描述符的兩個欄位:_mapcount和mapping。_mapcount欄位存放引用頁框的頁表項數目,確定其是否共享;mapping欄位用於確定頁是映射的或是匿名的:為空表示該頁屬於交換高速緩存;非空,且最低位是1,表示該頁為匿名頁,同時mapping欄位中存放的是指向anon_vma描述符的指針;如果mapping欄位非空,且最低位是0,表示該頁為映射頁;同時mapping欄位指向對應文件的address_space對象。
view plaincopyprint?01.struct page
02.{
03. atomic_t _mapcount;
04.
05. union {
06. ……
07. struct {
08. ……
09. struct address_space *mapping;
10. };
11. ……
12. };
struct page
{
atomic_t _mapcount;
union {
……
struct {
……
struct address_space *mapping;
};
……
}; Linux的address_space對象在RAMA中是對其的,所以其起始地址是4的倍數。因此其mapping欄位的最低位可以用作一個標誌位來表示該欄位的指針是指向address_space對象還是anon_vma描述符。PageAnon檢查mapping最低位。
view plaincopyprint?01./*檢查頁是否為匿名頁,低位為1時為匿名頁*/
02.static inline int PageAnon(struct page *page)
03.{
04. return ((unsigned long)page->mapping & PAGE_MAPPING_ANON) != 0;
05.}
/*檢查頁是否為匿名頁,低位為1時為匿名頁*/
static inline int PageAnon(struct page *page)
{
return ((unsigned long)page->mapping & PAGE_MAPPING_ANON) != 0;
} 匿名頁面和文件映射頁面分別採用了不同的底層數據結構去存放與頁面相關的虛擬內存區域。對於匿名頁面來說,與該頁面相關的虛擬內存區域存放在結構 anon_vma 中定義的雙向鏈表中。結構 anon_vma 定義很簡單,如下所示:
view plaincopyprint?01.struct anon_vma
02.{
03. spinlock_t lock;
04. struct list_head head;
05.};
struct anon_vma
{
spinlock_t lock;
struct list_head head;
};匿名頁的面向對象反向映射如下圖:
可以通過頁面的mapping找到anon_vma然後找到映射該頁面的所有線性區域(vm_area_struct結構)。
而對於基於文件映射的頁面來說,與匿名頁面不同的是,與該頁面相關的虛擬內存區域的存放是利用了優先順序搜索樹這種數據結構的。這是因為對於匿名頁面來說,頁面雖然可以是共享的,但是一般情況下,共享匿名頁面的使用者的數目不會很多;而對於基於文件映射的頁面來說,共享頁面的使用者的數目可能會非常多,使用優先順序搜索樹這種結構可以更加快速地定位那些引用了該頁面的虛擬內存區域。操作系統會為每一個文件都建立一個優先順序搜索樹,其根節點可以通過結構 address_space 中的 i_mmap 欄位獲取。
view plaincopyprint?01.struct address_space {
02. ……
03. struct prio_tree_root i_mmap;
04.……
05. }
struct address_space {
……
struct prio_tree_root i_mmap;
……
} Linux中使用 (radix,size,heap) 來表示優先順序搜索樹中的節點。其中,radix 表示內存區域的起始位置,heap 表示內存區域的結束位置,size 與內存區域的大小成正比。在優先順序搜索樹中,父節點的 heap 值一定不會小於子節點的 heap 值。在樹中進行查找時,根據節點的 radix 值進行。程序可以根據 size 值區分那些具有相同 radix 值的節點。
在用於表示虛擬內存區域的結構 vm_area_struct 中,與上邊介紹的雙向鏈表和優先順序搜索樹相關的欄位如下所示:
view plaincopyprint?01.struct vm_area_struct {
02. struct mm_struct * vm_mm;
03.……
04. union {
05. struct {
06. struct list_head list;
07. void *parent;
08. struct vm_area_struct *head;
09. } vm_set;
10.
11. struct raw_prio_tree_node prio_tree_node;
12. } shared;
13.
14.
15. struct list_head anon_vma_node;
16. struct anon_vma *anon_vma;
17.};
struct vm_area_struct {
struct mm_struct * vm_mm;
……
union {
struct {
struct list_head list;
void *parent;
struct vm_area_struct *head;
} vm_set;
struct raw_prio_tree_node prio_tree_node;
} shared;
struct list_head anon_vma_node;
struct anon_vma *anon_vma;
};
與匿名頁面的雙向鏈表相關的欄位是 anon_vma_node 和 anon_vma。union shared 則與文件映射頁面使用的優先順序搜索樹相關。欄位 anon_vma 指向 anon_vma 表;欄位 anon_vma_node 將映射該頁面的所有虛擬內存區域鏈接起來;union shared 中的 prio_tree_node 結構用於表示優先順序搜索樹的一個節點;在某些情況下,比如不同的進程的內存區域可能映射到了同一個文件的相同部分,也就是說這些內存區域具有相同的(radix,size,heap)值,這個時候 Linux 就會在樹上相應的節點(樹上原來那個具有相同(radix,size,heap) 值的內存區域)上接一個雙向鏈表用來存放這些內存區域,這個鏈表用 vm_set.list 來表示;樹上那個節點指向的鏈表中的第一個節點是表頭,用 vm_set.head 表示;vm_set.parent 用於表示是否是樹結點。下邊給出一個小圖示簡單說明一下 vm_set.list 和 vm_set.head。
vm_set.list 和 vm_set.head
通過結構 vm_area_struct 中的 vm_mm 欄位可以找到對應的 mm_struct 結構,在該結構中找到頁全局目錄,從而定位所有相關的頁表項。
反向映射實現
在進行頁面回收的時候,Linux的 shrink_page_list() 函數中調用 try_to_unmap() 函數去更新所有引用了回收頁面的頁表項。其代碼流程如下所示:
實現函數 try_to_unmap() 的關鍵代碼流程圖
函數 try_to_unmap() 分別調用了兩個函數 try_to_unmap_anon() 和 try_to_unmap_file(),其目的都是檢查並確定都有哪些頁表項引用了同一個物理頁面,但是,由於匿名頁面和文件映射頁面分別採用了不同的數據結構,所以二者採用了不同的方法。
函數 try_to_unmap_anon() 用於匿名頁面,該函數掃描相應的 anon_vma 表中包含的所有內存區域,並對這些內存區域分別調用 try_to_unmap_one() 函數。
函數 try_to_unmap_file() 用於文件映射頁面,該函數會在優先順序搜索樹中進行搜索,並為每一個搜索到的內存區域調用 try_to_unmap_one() 函數。
兩條代碼路徑最終匯合到 try_to_unmap_one() 函數中,更新引用特定物理頁面的所有頁表項的操作都是在這個函數中實現的。
《解決方案》
謝謝分享