Linux內核-內存回收邏輯和算法(LRU)
Linux內核內存回收邏輯和算法(LRU)
LRU 鏈表
在 Linux 中,操作系統對 LRU 的實現主要是基於一對雙向鏈表:active 鏈表和 inactive 鏈表,這兩個鏈表是 Linux 操作系統進行頁面回收所依賴的關鍵數據結構,每個內存區域都存在一對這樣的鏈表。顧名思義,那些經常被訪問的處於活躍狀態的頁面會被放在 active 鏈表上,而那些雖然可能關聯到一個或者多個進程,但是並不經常使用的頁面則會被放到 inactive 鏈表上。頁面會在這兩個雙向鏈表中移動,操作系統會根據頁面的活躍程度來判斷應該把頁面放到哪個鏈表上。頁面可能會從 active 鏈表上被轉移到 inactive 鏈表上,也可能從 inactive 鏈表上被轉移到 active 鏈表上,但是,這種轉移並不是每次頁面訪問都會發生,頁面的這種轉移發生的間隔有可能比較長。那些最近最少使用的頁面會被逐個放到 inactive 鏈表的尾部。進行頁面回收的時候,Linux 操作系統會從 inactive 鏈表的尾部開始進行回收。
用於描述內存區域的 struct zone() 中關於這兩個鏈表以及相關的關鍵字段的定義如下所示:
struct zone {
……
spinlock_t lru_lock;
struct list_head active_list;
struct list_head inactive_list;
unsigned long nr_active;
unsigned long nr_inactive;
……
}
各字段含義如下所示:
lru_lock:active_list 和 inactive_list 使用的自旋鎖。 active_list:管理內存區域中處於活躍狀態的頁面。 inactive_list:管理內存區域中處於不活躍狀態的頁面。 nr_active:active_list 鏈表上的頁面數目。 nr_inactive:inactive_list 鏈表上的頁面數目。
如何在兩個LRU 鏈表之間移動頁面
Linux 引入了兩個頁面標誌符 PG_active
和 PG_referenced
用於標識頁面的活躍程度,從而決定如何在兩個鏈表之間移動頁面。
PG_active
用於表示頁面當前是否是活躍的,如果該位被置位,則表示該頁面是活躍的。PG_referenced
用於表示頁面最近是否被訪問過,每次頁面被訪問,該位都會被置位。
Linux 必須同時使用這兩個標誌符來判斷頁面的活躍程度,假如只是用一個標誌符,在頁面被訪問時,置位該標誌符,之後該頁面一直處於活躍狀態,如果操作系統不清除該標誌位,那麽即使之後很長一段時間內該頁面都沒有或很少被訪問過,該頁面也還是處於活躍狀態。為了能夠有效清除該標誌位,需要有定時器的支持以便於在超時時間之後該標誌位可以自動被清除。然而,很多 Linux 支持的體系結構並不能提供這樣的硬件支持,所以 Linux 中使用兩個標誌符來判斷頁面的活躍程度。
Linux 2.6 中這兩個標誌符密切合作,其核心思想如下所示:
如果頁面被認為是活躍的,則將該頁的 PG_active
置位;否則,不置位。當頁面被訪問時,檢查該頁的 PG_referenced
位,若未被置位,則置位之;若發現該頁的 PG_referenced
已經被置位了,則意味著該頁經常被訪問,這時,若該頁在 inactive 鏈表上,則置位其 PG_active
位,將其移動到 active 鏈表上去,並清除其 PG_referenced
位的設置;如果頁面的 PG_referenced
位被置位了一段時間後,該頁面沒有被再次訪問,那麽 Linux 操作系統會清除該頁面的 PG_referenced
位,因為這意味著這個頁面最近這段時間都沒有被訪問。
PG_referenced
位同樣也可以用於頁面從 active 鏈表移動到 inactive 鏈表。對於某個在 active 鏈表上的頁面來說,其 PG_active
位被置位,如果 PG_referenced
位未被置位,給定一段時間之後,該頁面如果還是沒有被訪問,那麽該頁面會被清除其 PG_active
位,挪到 inactive 鏈表上去。
Linux 中實現在 LRU 鏈表之間移動頁面的關鍵函數如下所示(本文涉及的源代碼均是基於 Linux 2.6.18.1 版本的):
mark_page_accessed()
:當一個頁面被訪問時,則調用該函數相應地修改PG_active
和PG_referenced
。page_referenced()
:當操作系統進行頁面回收時,每掃描到一個頁面,就會調用該函數設置頁面的PG_referenced
位。如果一個頁面的PG_referenced
位被置位,但是在一定時間內該頁面沒有被再次訪問,那麽該頁面的PG_referenced
位會被清除。activate_page()
:該函數將頁面放到 active 鏈表上去。shrink_active_list()
:該函數將頁面移動到 inactive 鏈表上去。
LRU 緩存
前邊提到,頁面根據其活躍程度會在 active 鏈表和 inactive 鏈表之間來回移動,如果要將某個頁面插入到這兩個鏈表中去,必須要通過自旋鎖以保證對鏈表的並發訪問操作不會出錯。為了降低鎖的競爭,Linux 提供了一種特殊的緩存:LRU 緩存,用以批量地向 LRU 鏈表中快速地添加頁面。有了 LRU 緩存之後,新頁不會被馬上添加到相應的鏈表上去,而是先被放到一個緩沖區中去,當該緩沖區緩存了足夠多的頁面之後,緩沖區中的頁面才會被一次性地全部添加到相應的 LRU 鏈表中去。Linux 采用這種方法降低了鎖的競爭,極大地提升了系統的性能。
LRU 緩存用到了 pagevec 結構,如下所示 :
struct pagevec {
unsigned long nr;
unsigned long cold;
struct page *pages[PAGEVEC_SIZE];
};
pagevec 這個結構就是用來管理 LRU 緩存中的這些頁面的。該結構定義了一個數組,這個數組中的項是指向 page 結構的指針。一個 pagevec 結構最多可以存在 14 個這樣的項(PAGEVEC_SIZE 的默認值是 14)。當一個 pagevec 的結構滿了,那麽該 pagevec 中的所有頁面會一次性地被移動到相應的 LRU 鏈表上去。
用來實現 LRU 緩存的兩個關鍵函數是 lru_cache_add()
和 lru_cache_add_active()
。前者用於延遲將頁面添加到 inactive 鏈表上去,後者用於延遲將頁面添加到 active 鏈表上去。這兩個函數都會將要移動的頁面先放到頁向量 pagevec 中,當 pagevec 滿了(已經裝了 14 個頁面的描述符指針),pagevec 結構中的所有頁面才會被一次性地移動到相應的鏈表上去。
下圖概括總結了上文介紹的如何在兩個鏈表之間移動頁面,以及 LRU 緩存在其中起到的作用:
頁面在 LRU 鏈表之間移動示意圖
其中,
1 表示函數 mark_page_accessed()
2 表示函數 page_referenced()
3 表示函數 activate_page()
4 表示函數 shrink_active_list()
。
PFRA具體實現
PFRA必須處理多種屬於用戶態進程、磁盤高速緩存和內存高速緩存的頁,而且必須遵照幾條試探法準則。PFRA的大部分函數如下:
如上圖在分配VFS緩沖區或緩沖區首部時,內核調用free_more_memory()
;而當從夥伴系統分配一個或多個頁框時,調用try_to_free_pages()
。
頁面回收關鍵代碼流程圖
上文提到 Linux 中頁面回收主要是通過兩種方式觸發的,一種是由“內存嚴重不足”事件觸發的;一種是由後臺進程 kswapd 觸發的,該進程周期性地運行,一旦檢測到內存不足,就會觸發頁面回收操作。對於第一種情況,系統會調用函數 try_to_free_pages()
去檢查當前內存區域中的頁面,回收那些最不常用的頁面。對於第二種情況,函數 balance_pgdat()
是入口函數。
當 NUMA 上的某個節點的低內存區域調用函數 try_to_free_pages()
的時候,該函數會反復調用 shrink_zones()
以及 shrink_slab()
釋放一定數目的頁面,默認值是 32 個頁面。如果在特定的循環次數內沒有能夠成功釋放 32 個頁面,那麽頁面回收會調用 OOM killer 選擇並殺死一個進程,然後釋放它占用的所有頁面。函數 shrink_zones()
會對內存區域列表中的所有區域分別調用 shrink_zone()
函數,後者是從內存回收最近最少使用頁面的入口函數。
對於定期頁面檢查並進行回收的入口函數 balance_pgdat()
來說,它主要調用的函數是 shrink_zone()
和 shrink_slab()
。從上圖中我們也可以看出,進行頁面回收的兩條代碼路徑最終匯合到函數 shrink_zone()
和函數 shrink_slab()
上。
函數 shrink_zone()
其中,shrink_zone()
函數是 Linux 操作系統實現頁面回收的最核心的函數之一,它實現了對一個內存區域的頁面進行回收的功能,該函數主要做了兩件事情:
將某些頁面從 active 鏈表移到 inactive 鏈表,這是由函數 shrink_active_list()
實現的。
從 inactive 鏈表中選定一定數目的頁面,將其放到一個臨時鏈表中,這由函數 shrink_inactive_list()
完成。該函數最終會調用 shrink_page_list()
去回收這些頁面。
函數 shrink_page_list()
返回的是回收成功的頁面數目。概括來說,對於可進行回收的頁面,該函數主要做了這樣幾件事情,其代碼流程圖如下所示:
函數 shrink_page_list()
實現的關鍵功能
函數 shrink_slab()
函數 shrink_slab()
是用來回收磁盤緩存所占用的頁面的。Linux 操作系統並不清楚這類頁面是如何使用的,所以如果希望操作系統回收磁盤緩存所占用的頁面,那麽必須要向操作系統內核註冊 shrinker 函數,shrinker 函數會在內存較少的時候主動釋放一些該磁盤緩存占用的空間。函數 shrink_slab()
會遍歷 shrinker 鏈表,從而對所有註冊了 shrinker 函數的磁盤緩存進行處理。
從實現上來看,shrinker 函數和 slab 分配器並沒有固定的聯系,只是當前主要是 slab 緩存使用 shrinker 函數最多。
註冊 shrinker 是通過函數 set_shrinker()
實現的,解除 shrinker 註冊是通過函數 remove_shrinker()
實現的。當前,Linux 操作系統中主要的 shrinker 函數有如下幾種:
shrink_dcache_memory()
:該 shrinker 函數負責 dentry 緩存。
shrink_icache_memory()
:該 shrinker 函數負責 inode 緩存。
mb_cache_shrink_fn()
:該 shrinker 函數負責用於文件系統元數據的緩存。
具體的源代碼實現細節有時間再做分析。後面將談論交換。
Linux內核-內存回收邏輯和算法(LRU)