Linux內存管理第十章 -- 頁框回收(Page Frame Reclamation)

Linux內存管理第十章 – 頁框回收(Page Frame Reclamation)

頁替換策略(Page Replacement Policy)

每當討論頁替換策略,提及最多的就是基於LRU(Least Recently Used)的算法,但嚴格來說這是不對的因爲這些lists並不是嚴格按照LRU的順序來維護的。在Linux中LRU有兩個list組成,分別是active_list和inactive_list。avtive_list的目標是包含所有進程工作使用的page而inactive_list的目標是包含回收候選者的page。因爲所有可回收的page都包含在這兩個list中,因此任何進程的page都有可能被回收,而不是僅僅回收屬於出錯進程的page,因此頁回收策略是全局的。
這兩個隊列就像是一個簡單的LRU 2Q,這兩個被維護的隊列分別叫Am和A1。在使用LRU 2Q時,第一次被分配的page將被放入到一個名叫A1的FIFO隊列。如果某個page在A1中並且被人引用,然後改page就會被放入到一個正規的LRU管理的隊列Am中。這就是像使用lru_cache_add()將page放置到inactive_list(A1)中,然後使用mark_page_accessed()將page移動到active_list(Am)中。LRU 2Q中的算法描述了這兩個list的size如何調節但是Linux使用了一種簡單的方法:通過使用refill_inactive()函數將page從active_list的地步移動到inactive_list中來保證active_list的size是整個page cache的2/3. 下圖說明了這兩個list如何結構化,page如何在這兩個list中移動:
reclaim
2Q算法中描述兩個list中預設Am是一個LRU list但是在Linux中更像是一個時鐘算法,其中週期就是active list的size。當一個page到達active list的底部的時候,就會來檢查reference flag會被檢查,如果被置位,該page將會active list的頂部然後接着檢查下一個page。如果reference bit被清除,則需要將該page移動到inactive_list.
儘管Move-To-Front的啓示意味着這兩個列表像是在一LRU的方式在運行但是Linux替換策略和LRU還是有很多的不同,不能將其認爲是一個stack算法。儘管我們可以忽略分析多程序系統的問題和每個進程使用的memory size是不同的這個事實,但是替換策略還是不能滿足inclusion property是因爲每個page在list中的位置取決於list的大小和上次引用的時間。這些list並沒有按優先級排序因爲這會使得每次對page的引用都要更新list。當被換出進程地址空間時,這兩個列表幾乎被忽略,因爲與被換出決策相關的是page在進程虛擬地址空間中的位置而不是在page list中的位置。(這句話不太理解)
總結下,Linux中替換算法並不是和LRU的行爲一樣並且被一個benchmark在實際測試中表現不錯。當前僅僅有兩種case在該算法下表現得比較糟糕。

  • first:當要被回收的候選page是匿名page時
    在這種case中,Linux在線性掃描進程頁表來搜索要回收的page之前,將持續檢查大量的page,但是這種場景相當少。
  • second:單個進程中有許多文件映射的page在inactive_list中,並且經常被寫入。
    該進程和kswapd進程可能進入一種不斷地轉換這些page的循環中並把它們放入到inactive_list中但不釋放任何東西。在這種case下,只有很少的page能夠從active_list移動到inactive_list因爲這兩個列表大小比例始終沒有很大變化。

Page Cache

page cache是一組數據結構其包含一些pages如有文件映射或者塊設備映射或者swap的page。當前有四種最基本類型的page存在於page cache中:

  • 通過讀取內存映射的文件而產生page fault的page
  • 被稱作buffer page的page:從文件系統或者塊設備中讀取的數據塊到特定地page
  • 存在於swap cache中的匿名page
  • 屬於共享內存空間的page,其和匿名page的處理方式差不對。它們唯一的不同點事共享page被加入到swap cache後當它第一次被寫入,它在其存儲介質的空間上會立刻保留。

存在page cache的理由是減少不必要的磁盤訪問。從磁盤中讀取的page被存儲在page hash table中,屬於struct address_space.在每次訪問磁盤前,都會在page cache中搜索其在磁盤中的偏移。下面的API是用來操作page cache的:

void add_to_page_cache(struct page * page, struct address_space * mapping, unsigned long offset)
通過調用lru_cache_add()將page加入到LRU中,然後再將page計入到inode queue和page hash table中
void add_to_page_cache_unique(struct page * page, struct address_space *mapping, unsigned long offset, struct page **hash)
該函數和上一個函數很相似,除了會檢查該page是否已經在page cache中存在,該函數要求其調用者不能是由pagecache_lock自旋鎖
void remove_inode_page(struct page *page)
該函數調用remove_page_from_inode_queue()從inode中刪除page,並調用remove_page_from_hash_queue()從page cache中刪除page
struct page * page_cache_alloc(struct address_space *x)
alloc_pages()的封裝,使用x->gfp_mask
int page_cache_read(struct file * file, unsigned long offset)
如果offset對應的page不在page cache中時,會增加一個page,必要時會通過address_space_operations->readpage從磁盤讀取數據
void page_cache_release(struct page *page)
_free_page()函數的別名,當一個page的引用計數下降爲0,則釋放該page

Page Cache Hash Table

有一個需求:page cache中的page需要快速被找到。爲了實現此需求,page被插入到一個page_hash_table。page->next_hash和page->pprev_hash被用來決絕衝突。(貌似kernel2.6中已經沒有page_hash_table)
在kernel2.6中使用索引樹來進行page存儲用於快速查找。

Adding Pages to the Page Cache

從文件或者塊設別中讀出來的page通常會被添加到page cache從而避免更多的磁盤IO。大多數文件系統使用generic_file_read()
當作它們的file_operations->read()函數。一般來講文件系統是通過page來執行它們的IO操作。下面就來說明下generic_file_read()是如何操作的以及它是如何將page添加到page cache。
對於普通的IO來說,generic_file_read()在調用do_generic_read()之中會先做一些基本的檢查。通過find_get_page()來查詢該page是否已經在page cache中存在,如果不存在,調用page_cache_alloc_cold()從cpu code list中分配page。然後再調用add_to_page_cache_lru()將page加入到page cache同時將page加入到lru list中。如果page一旦在page cache中存在,就會調用page_cache_readahead()從磁盤中讀取數據。
read
沒用從進程空間映射的匿名page將被添加到swap cache中,這將在後續的章節來討論。匿名page在嘗試將它們換出之前,它們沒有address_space作爲一個映射或者是一個文件偏移將它們加入到page cache中。所以這些page仍然留在LRU list中。一旦進入page cache,匿名page和file backed page的真正不同點事匿名page將swapper_space作爲address_space。
共享內存的pages在下列兩種case下會被加入到page cache中。
第一種case是:當page第一次從swap中獲取或者第一次被分配並且第一次被引用的時候加入到page cache。即shmem_getpage()中完成。
第二種case是當swap code調用shmem_unuse()。當一個swap area正在被deactive,並且發現一個page並且在swapper_address中並且沒有一個進程在使用。

Reclaiming Pages from the LRU Lists

函數shrink_cache()是替換算法的一部分,它從inactive_list獲取page並決定如何將它們換出。該函數是一個大循環,從inactive_list底部最多掃描max_scan個page來釋放nr_pages個page,直到inactive_list爲空。
每種不同類型的page,釋放的時候具體的做法不同。具體的處理順序如下:

  • page被lock了且PG_launder被置位:該page在IO中被lock,因此需要跳過此page。但是如果PG_launder被置位,這就意味着該page是第二次被發現上鎖了,因此最好等待IO完成然後不管它。如果一個page通過page_cache_get()被引用因此該page不會被過早地釋放並調用wait_on_page()進入睡眠知道IO完成。一旦IO結束調用 page_cache_release()來減少它的引用計數。當引用計數爲0,則該page可以被回收了。
  • page是髒頁且沒有被任何進程映射且沒有buffer且屬於穩健映射或者設備:因爲該page屬於一個文件映射或者設備映射,因此它擁有合法的writepage()函數可用:page->mapping->a_ops->writepage.PG_dirty被清空並且PG_launder被置位說明它準備IO。在調用writepage()前需要調用page_cache_get()來增加它的引用計數來。值得注意的是這種case同樣適用於處於swap cache中的匿名page。該page仍然在LRU中,當它再次被發現後,如果IO完成那麼就將它簡單滴釋放,並且page被回收。如果IO沒有完成,kernel會等待IO完成。
  • page擁有buff:將會調用try_to_release_page()來引用它,並嘗試將它釋放。如果成功了,那麼它是一個匿名page,它將從LRU中移除並減少它的引用計數。匿名page存在buff只有一種情況:它指向一個swap file因爲該page需要寫出block-sized chunk。換句話說如果該page直接有一個file映射,那麼就將其簡單滴減少它的引用計數,如果引用計數爲0,則可以直接釋放了。
  • 匿名page且被映射到多個進程:調用swap_out()
  • 沒有進程在引用的page:如果page在swap cache中,那麼將從swap cache刪除。如果是一個file的一部分,那麼它將從page cache中刪除和釋放。

Shrinking all caches

shrink_caches()的作用是釋放多個cache。在Linux2.6中一次遍歷各個zone,然後調用shrink_zone()來回收頁框。
如果有多個釋放任務,這就需要給每個釋放任務設定一個優先級,priority默認是DEF_PRIORITY,如果每次釋放的page數達不到預期,那麼就將priority從最高優先級遞減1。

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