linux內存管理-頁面回收

頁面回收

背景

linux操作系統設計的時候提供了cache的功能,如果有空閒內存就可以用來作爲cache提升性能,例如page cache緩衝硬盤中的內容,dcache,icache緩存文件系統的metadata數據,這些內容是爲了提升性能而設計的,還可以再次從硬盤中重新讀取來構建對象,所以這部分內容可以在內存緊張的時候可以直接釋放。
另外除了提供緩存機制,還提供了緩衝機制,當寫文件的時候,默認不會直接進行寫入硬盤,而是會在page cache中緩衝,一直到超時或者髒頁過多時進行回寫操作,這樣可以減少隨機的IO數量和重複寫操作,這部分頁在內存緊張的時候需要首先回寫到硬盤,之後可以進行回收.
對於匿名頁,例如進程中的堆,棧區域,它是沒有後備文件的,linux爲匿名頁創建了swap機制,可以是分區,也可以是文件,在開啓了swap功能的系統中可以將匿名頁當做文件一樣寫到磁盤中,在訪問數據的時候再次從swap中恢復.

頁面回收的基本思想就是將磁盤和內存看做一體,只是訪問速度有差異,將經常訪問的數據放到速度比較快的內存中,不經常訪問的數據放到磁盤上.

用戶進程的地址空間相互隔離,而內核是所有應用共用的地址空間,雖然同樣都是經過頁表映射訪問物理內存,但是他們的訪問頻度相差很大,所有的進程陷入到內核中時都要訪問內核空間的數據,所以對內核進行頁面回收是十分不明智的.

用戶進程只能看到虛擬地址空間,訪問時通過頁錶轉換到物理地址,而linux上內核對應用進程提供的內存都是虛假的,只有在實際訪問的時候才按需分配,也叫lazy paging. Linux中通過MMU轉換時的異常提供修復機制,也就是我們常說的缺頁異常,是應用進程lazy paging的基礎,理論上用戶空間的所有虛擬地址都可以不分配實際的物理內存,只有實際訪問時在缺頁異常中進行頁面分配、文件映射等操作。

linux在設計的時候是面向多種應用的,雖然頁面回收包括其中的swap,背後的lazy paging機制非常影響性能,但是通過複用物理內存確實可以提升系統的壽命和運行容量。

下面系統的總結內存回收的內容,解決幾個問題:

  1. 哪些頁面可以被回收
  2. 頁面回收的策略
  3. 頁面回收的細節實現
  4. 對於不用頁面的回收方式

頁面回收的對象

先來看一下哪些頁不能不回收,首先內核中的頁絕大多數都不能別回收,linux除了它的核心代碼之外還有module模塊,當module被加載之後除了可以被卸載,只能使用部分export的api之外,它得到的待遇和core中的代碼是相同的,總之對於加載成功的module是完全信任的。而內核空間是所有進程公用的,內核中使用的頁通常是伴隨整個系統運行週期的,頻繁的頁換入和換出是非常影響性能的,所以內核中的頁基本上不能回收,不是技術上實現不了而是這樣做得不償失。
另外一種是應用程序主動申請鎖定的頁,它的實時性要求比較高,頻繁的換入換出和缺頁異常處理無法滿足它對於時間上的要求,所以這部分程序可能使用mlock api將頁主動鎖定,不允許它進行回收。

其他用途使用的物理頁基本上都是可以進行回收的,可以分成兩大類:

  1. 文件映射的頁,包括page cache,slab中的dcache,icache,用戶進程的可執行程序的代碼段,文件映射頁面。
    其中page cache包括文件系統的page,還包括裸塊設備的buffer cache,萬物皆文件,block也是一種文件,它也有關聯的file,inode等。另外根據頁是否是髒的,在回收的時候處理有所不同,髒頁需要先回寫到磁盤再回收,乾淨的頁可以直接釋放。
  2. 匿名頁,包括進程使用各種api(malloc,mmap,brk/sbrk)申請到的物理內存(這些api通常只是申請虛擬地址,真實的頁分配發生在page fault中),包括堆、棧,進程間通信中的共享內存,pipe,bss段,數據段,tmpfs的頁。這部分沒有辦法直接回寫,爲他們創建swap區域,這些頁也轉化成了文件映射的頁,可以回寫到磁盤。

頁面回收的策略

頁面回收主要有兩種方式:後臺異步回收和同步阻塞回收,其中後臺回收主要是指kswapd線程中進行的頁面回收,兩種方式除了是否堵塞當前的頁面分配之外,回收的力度也有所不同,kswapd在zone的空閒頁滿足high watermark之後就停止回收,而直接回收在回收到一定數量的頁(當前32個頁)就會停止.
直接回收是在申請頁面的路徑上,它的上下文比較複雜,可能是IO棧上或者文件系統中申請的頁面,這時候頁面回收的時候不能進行回寫磁盤的動作,因爲這時候的回寫可能又會觸發新一輪的頁面申請,陷入到死循環中。

頁面回收的基準watermark

申請內存的時候,首先會檢查是否內存緊張,也就是到達當前zone的空閒頁幀是否小於watermark,如果低於low watermark會開始內存回收,保證申請結束後水位還能滿足要求,這樣儘量在內存開始緊張的時候就後臺慢慢回收,此時只回收那些沒有映射的頁並且不需要回寫的頁。
申請不到內存之後,首先會wakeup kswapd線程在後臺進行回收,等待短時間之後再次嘗試頁申請,系統希望通過kswapd後臺回收方式能夠得到足夠的內存,這樣不會影響正常的頁面申請.
最後申請失敗,則開始同步內存回收,這時候直接堵塞住了內存申請的路徑,頁面回收結束後再次開始進行頁面申請。

alloc_page申請內存時會表明自身的緊急情況,用來指示當內存緊張時是否應該等待

__GFP_REPEAT:內存緊張時,等待內存回收結束,之後再次嘗試,目前重試3次
__GFP_NOFAIL:內存緊張時,喚醒kswapd之後週期性再次嘗試獲取頁,如果一直沒有獲取到就一直循環
__GFP_NORETRY:嘗試獲取內存,獲取不到就返回失敗
__GFP_NOMEMALLOC:清除該標誌位的時候,申請的時候不管watermark是否到達min,只要有就分配給他

水位的計算可以通過vm.min_free_kbytes來調節,系統默認是1024,根據zone的大小進行調節,和min_free_kbytes成正相關的,但是規定了上下限128k-64M,這樣在小內存的系統中仍然會預留足夠的緊急內存處理核心模塊的內存分配,在大內存系統中不會造成太多的空閒內存浪費,這些是通用場景的經驗調值,可以根據自身業務場景調節的。緊急內存是爲了特殊場景使用的,例如頁面回收,對於髒頁和匿名頁需要寫到磁盤上,這中間需要經過文件系統,IO棧等,他們也需要申請buffer_head之類的內存.

 * min_free_kbytes = 4 * sqrt(lowmem_kbytes)   //DMA+DMA32+NORMAL zone bytes
 * 16MB:    512k                                                                    
 * 32MB:    724k                                                                    
 * 64MB:    1024k 
 * ......
 * 16384MB: 16384k

根據各個zone佔據low mem的比例平分了min_free_kbytes,計算出每個zone的pages_min和low,high水位參數。

enum zone_watermarks {                                                              
    WMARK_MIN,                                                                      
    WMARK_LOW,                                                                      
    WMARK_HIGH,                                                                     
    NR_WMARK                                                                                                                                   
};
名稱 字段描述 計算方式
min zone的預留頁面數目,如果空閒物理頁面的數目低於 pages_min,那麼系統的壓力會比較大,此時,內存區域中急需空閒的物理頁面,頁面回收的需求非常緊迫。 (zone page /lowmem page)*lmin_free_kbytes
low 控制進行頁面回收的最小閾值,如果空閒物理頁面的數目低於 pages_low,那麼內核會開始進行後臺頁面回收。 pages_min + pages_min/4
high 控制進行頁面回收的最大閾值,如果空閒物理頁面的數目多於 pages_high,則內存區域的狀態是理想的。 pages_min + pages_min/2

頁面回收中的LRU

Linux 中的頁面回收是基於 LRU(least recently used,即最近最少使用 ) 算法的,它假設過去一段時間內頻繁使用的頁面,很快可能會被再次訪問到,已經很久沒有訪問過的頁面在未來較短的時間內也不會被頻繁訪問到。因此,在物理內存不夠用的情況下,這樣的頁面成爲被換出的最佳候選者。

LRU 算法的基本原理很簡單,在訪問頁的時候對其進行標記,對於活動的頁不斷的提升它在鏈表中的位置,這樣不經常活動的頁自然就會落到LRU鏈表尾部。單個鏈表如果沒有計數只能根據他們的相對位置表示兩種狀態:最近被訪問和最近沒有被訪問,但是對於被訪問的頻次沒有反應,訪問1次和訪問100次得到的待遇是一樣的,所以內核使用了兩個LRU鏈表來維護頁的老化程度:active_list和inactive_list,這樣它能夠表示4種狀態,對於訪問頻次更高的提升到active_list中,對於訪問頻次較低的逐漸老化到inactive_list中。

對於記錄頁是否被訪問的方式,有的arch上提供了硬件置位,例如x86,在頁表項中增加了一個accessed位,當頁面被訪問到的時候被MMU自動置位,之後需要通過軟件來清除這個標誌位。如果arch不支持硬件置位,則需要軟件在每次訪問頁的時候進行軟件標記,也就是page中的PG_referenced標誌位。
page在LRU中遷移
其中,1 表示函數 mark_page_accessed(),2 表示函數 page_referenced(),3 表示函數 activate_page(),4 表示函數 shrink_active_list()。

active_list和inactive_list

當每次訪問頁的時候,軟件會主動進行mark_page_accessed來標記當前頁已經被訪問過,因爲不是所有的arch MMU都能自動設置accessed標記,所以需要軟件主動設置標記。代碼中的註釋詳細的說明了根據當前頁面的狀態進行狀態遷移。

/*  
 * Mark a page as having seen activity. 
 * 
 * inactive,unreferenced    ->  inactive,referenced                             
 * inactive,referenced      ->  active,unreferenced                             
 * active,unreferenced      ->  active,referenced                               
 */                                                                             
void fastcall mark_page_accessed(struct page *page)                             
{                                                                               
    if (!PageActive(page) && PageReferenced(page) && PageLRU(page)) {           
        activate_page(page);                                                    
        ClearPageReferenced(page);                                              
    } else if (!PageReferenced(page)) {
    	SetPageReferenced(page); 
    }
}

而page_referenced()一個是測試當前page是否最近被訪問過,如果當前頁被訪問過則清除page的訪問標記。當一個page被多次映射,例如一塊匿名共享內存作爲進程間通信使用,一個可執行文件被執行多次,此時會有反向映射的數據結構來記錄有哪些虛擬地址區域映射了該頁,遍歷所有的反向映射區域,檢查vma對應的頁表項並清除accessed位,除此之外還檢查vma的flag是否被鎖定VMLOCKED,通過mlock告訴系統不希望這塊區域被交換出去,只要其中有vma標記被鎖定或者被訪問,就不會swap out這個頁。和訪問頁時隨時標記mark_page_accessed不一樣,只有在後臺頁面回收或者直接頁面回收的時候纔會清除頁的標記。

activate_page完成從inactive_list到active_list頭部的遷移,主要是上面的mark_page_accessed中當前頁PG_active=0和PG_referenced=1時進行頁面遷移。早期的實現直接完成page在鏈表間的遷移,從3.x之後都是將頁從inactive_list中摘除先放到lru緩存中,緩存滿的時候再放入active_list中,lru緩存會解釋。
shrink_active_list是頁面回收過程中的一環,從active_list中摘除部分PG_referenced=0的頁,先批量放到中間鏈表l_hold上,判斷當前回收的壓力和頁面狀態分別放到中間鏈表l_active和l_inactive中,最終批量放到zone的active_list和inactive_list上。
上面4種方式共同完成了lru兩個鏈表的內部循環,而頁面回收的時候從inactive_list中尾部獲取到可回收頁進行回收徹底從LRU刪除,那麼頁又是怎麼加入到LRU中的?
在頁面回收的時候,對不同的頁面是有傾向性的,主要是回收代價的大小,首先回收哪些沒有映射的頁面代價最小,例如page cache中預讀的但是還沒有映射的頁,直接回收不需要任何IO和頁表項修改,主要是內核中的可回收頁。其次是經過映射的頁,這部分主要是用戶空間進程使用的頁面,包括堆、棧、數據段、shmem共享的內存,映射文件的頁。內核對於用戶空間使用頁都是laze paging的模式,爲他們分配或者映射頁主要是缺頁異常觸發之後才實際分配物理頁。

頁類型 缺頁異常中fix過程 對應的lru鏈表
進程堆、棧、數據段中使用的匿名頁,mmap共享內存映射頁 do_anonymous_page
do_cow_fault
do_wp_page
lru_cache_add_active到活動匿名頁lru鏈表
映射文件頁 filemap_fault add_to_page_cache_lru非活動文件頁lru鏈表

SMP中的lru緩存

在多核系統中,操作active_list/inactive_list鏈表需要獲取鎖來防止併發訪問,頻繁的申請和釋放頁就會使得鎖競爭變得激烈,所以這裏使用了per-cpu的lru緩存:pagevec,通過批量操作來減少衝突的概率,當緩存滿的時候批量移到active_list/inactive_list中。

struct pagevec {                                                                                                                               
    unsigned long nr;                                                              
    unsigned long cold;                                                            
    struct page *pages[PAGEVEC_SIZE]; //目前是14個頁                                              
}; 
static DEFINE_PER_CPU(struct pagevec, lru_add_pvecs) = { 0, };              //緩存加入到inactive鏈表中的頁    
static DEFINE_PER_CPU(struct pagevec, lru_add_active_pvecs) = { 0, };    //緩存加入到active鏈表中的頁

頁面回收的傾向性

1、 儘量不要修改page table。例如回收各種沒有使用的內核cache的時候,我們直接回收,根本不需要修改頁表項。而用戶空間進程的頁面回收往往涉及將對應的pte條目修改爲無效狀態。
2、 除非調用mlock將page鎖定,否則所有的用戶空間對應的page frame都應該可以被回收。
3、 如果一個page frame被多個進程共享,那麼我們需要清除所有的pte entry,之後才能回收該頁面。
4、 不要回收那些最近訪問過的page frame,或者說優先回收那些最近沒有訪問的page frame。
5、 儘量先回收那些不需要磁盤IO操作的page frame。

頁面回收的過程

掃描控制

頁面回收需要掃描不同的頁,而當前內存的緊急情況也分不同的等級,包括是否開啓了swap,是否允許writeback操作等,在頁面回收過程中就構造了scan_control對象來指示頁面回收的範圍。另外回收時還有priority來指示當前頁面回收的激烈程度,初始值爲12,逐步遞減,當前zone需要掃描的頁面=LRU鏈表上的頁面除以(2^priority),每次頁面回收結束後,如果回收的頁面還是不能滿足頁面申請的要求,伴隨着priority的遞減逐步增加頁面掃描的數量。

其中初始的掃描控制參數如下,

    struct scan_control sc = {                                                  
        .gfp_mask = gfp_mask,
        .may_writepage = !laptop_mode,	//是否允許回收匿名頁和髒頁
        .swap_cluster_max = SWAP_CLUSTER_MAX,	//直接回收頁數量大於該值時結束回收
        .may_swap = 1, //是否開啓了swap
        .swappiness = vm_swappiness, //文件映射頁和匿名頁回收時的傾向,範圍從0-100,默認60,值越高對匿名頁回收越積極 
    };

鏈表轉移

  1. 掃描active_list,根據當前優先級選擇需要掃描zone,當active_list上頁太少時就不再掃描了,可以通過不斷調高優先級加大掃描力度.爲了減少對於lru鏈表的競爭,在收縮active_list鏈表的時候創建了三個臨時鏈表:l_hold,l_active,l_inactive,批量從active_list中移除和添加頁.
    首先從active_list鏈表尾部摘取需要掃描的頁,放到臨時鏈表l_hold中,這樣後續的處理可以訪問在l_hold中,之後檢查是否回收這些掃描過的頁.如果當前不需要回收頁表映射過的頁,將這這部分頁從l_hold移到l_active中.
    如果當前沒有可用的swap,將匿名頁從l_hold移到l_active中.
    再次檢查是否訪問過該頁面,不回收最近訪問過的頁,將頁從l_hold移到l_active中.
    剩餘的頁都是可以回收的,從l_hold中轉移到l_inactive中;之後將l_active批量加入到active_list的lru緩存中,將l_inactive中的頁批量加入到inactive_list的lru緩存中.
  2. 掃描inactive_list,直接從鏈表尾部摘除頁到page_list上,之後shrink_page_list開始進行頁面回收,回收失敗的頁仍然保留在page_list,按照當前狀態加回到對應的lru鏈表上.

slab的回收

並不是所有的slab都可以回收的,只有那些註冊了shrinker的才能收縮,在申請和釋放的時候他們也會維護類似於LRU這樣的老化鏈表,當收縮的時候進行回收

頁面回收

shrink_page_list

  1. 頁面回收不包括PG_locked,PG_writeback的頁,前者表示頁正在使用中,後者表示頁正在被回寫
  2. 再次檢查頁是否被訪問過,準備回收的頁PG_active=0,PG_referenced=0並且已經不在lru鏈表中了,當頁被訪問過之後PG_referenced=1,但是PG_active一定不會被置位,它又不是新分配的頁,不能憑空從inactive_list直接躍升到active_list.
  3. 對於匿名頁,需要在swap中分配位置,如果swap已經滿的情況就不能swap out了.之後page先加入到swap cache中,它是類似於page cache一樣的結構,因爲一個頁可能在多個vma中被映射使用,只有當所有的區域都解除了映射之後,它纔會從swap cache中移除,實現頁面回收.在加入到swap cache之後它標記當前頁PG_dirty,在下面的pageout中將頁內容先寫入到磁盤.
  4. 對於頁表映射的頁需要逐個解除映射,linux使用反向映射來管理多個虛擬地址和物理頁的對應關係,對於匿名頁和文件映射頁都是遍歷反向映射區間.對於匿名頁,需要更新它在swap中的位置到頁表項中,這樣在缺頁中斷中仍然能夠從swap中恢復數據,並且將反向映射關係進行銷燬.對於文件頁來說就比較簡單了,只需要將反向映射解除就OK了.
    try to unmap
  5. 對於需要進行回寫的頁進行處理,包括文件髒頁,匿名頁。文件髒頁都是基於文件系統的,文件系統定義的address_space->a_ops->writepage進行實際回寫,它再往下就屬於IO棧的東西了;對於匿名頁,它是屬於swap管理的,有自己的address_space和address_space_operations。當頁回寫成功,釋放頁的引用計數,最後一個引用計數釋放時將頁返回給buddy系統.
static const struct address_space_operations swap_aops = {
	.writepage  = swap_writepage,
}
  1. 對於不能回收的頁和回收失敗的頁,將他們再次返回,根據page的狀態加入到LRU鏈表中

頁面回收終止條件

1.對於kswapd中的頁面回收,當zone的page高於high watermark之後就會停止頁面回收
2.對於直接頁面回收,每次回收頁面數量大於SWAP_CLUSTER_MAX就會終止,如果掃描頁面較多,它會主動wakeup pdflush線程將髒頁回寫來釋放更多的頁.
3.如果回收不到任何的頁,代表系統已經到了絕境了,爲了生存開始自殘:OOM,和鬥地主一樣,哪個進程佔用的物理內存最多就它釋放物理頁.

reflink:
https://www.ibm.com/developerworks/cn/linux/l-cn-pagerecycle/
http://www.wowotech.net/memory_management/page_reclaim_basic.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章