記憶體管理九 linux記憶體頁面回收
一、概序:
在記憶體緊張時,核心會將很少使用的記憶體換出到交換分割槽,以便釋放出實體記憶體,此種機制成為“頁交換”,
也統稱為頁面回收,頁面回收涉及到LRU連結串列、記憶體回收演算法、Kswapd核心執行緒等知識,下面會做相關介紹。
二、LRU連結串列:
1、LRU連結串列:
(1)LRU連結串列按照zone來配置,即每一個zone管理自己單獨的LRU連結串列,在struct zone的結構體中有一個
lruvec的成員執行這些連結串列,根據不同的頁面型別和頁面的活躍度有如下5中型別的連結串列:
- 不活躍匿名頁面連結串列LRU_INACTIVE_ANON
- 活躍匿名頁面連結串列LRU_ACTIVE_ANON
- 不活躍檔案對映頁面連結串列LRU_INACTIVE_FILE
- 活躍對映頁面連結串列LRU_ACTIVE_FILE
- 不可回收頁面連結串列LRU_UNEVICTABLE
struct zone { /* Fields commonly accessed by the page reclaim scanner */ spinlock_t lru_lock; struct lruvec lruvec; ...... } struct lruvec { struct list_head lists[NR_LRU_LISTS]; struct zone_reclaim_stat reclaim_stat; }; enum lru_list { LRU_INACTIVE_ANON = LRU_BASE, LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE, LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE, LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE, LRU_UNEVICTABLE, NR_LRU_LISTS };
(2)頁面加入到LRU連結串列lru_cache_add():
通過list_add函式將頁面新增到LRU連結串列lruvec->list結構的頭部:
lru_cache_add -> __pagevec_lru_add -> __pagevec_lru_add_fn -> add_page_to_lru_list-> list_add
static __always_inline void add_page_to_lru_list(struct page *page, struct lruvec *lruvec, enum lru_list lru) { int nr_pages = hpage_nr_pages(page); mem_cgroup_update_lru_size(lruvec, lru, nr_pages); list_add(&page->lru, &lruvec->lists[lru]); __mod_zone_page_state(lruvec_zone(lruvec), NR_LRU_BASE + lru, nr_pages); }
lru_to_page(&lru_list)和list_del(&page->lru )實現從LRU連結串列摘除頁面,lru_to_page從連結串列的尾部摘除頁面,實現了
先進先出的演算法(FIFO),隨著時間的推移,不活躍的LRU都移動到了LRU連結串列的尾部,比較適合被回收,如下圖所示:
#define lru_to_page(head) (list_entry((head)->prev, struct page, lru))
2、第二次機會演算法:
經典LRU連結串列的FIFO演算法,存在一定的弊端,可能會將經常使用的頁面在LRU連結串列的尾部被回收掉,故第二次
機會演算法在此基礎做了修改,設定了訪問狀態位(硬體控制的位元位):
- 頁面被訪問,訪問的狀態位置1;
- 頁面回收時會檢查訪問位,如果為0,淘汰頁面,如果為1,給此頁面第二次機會;
- 如果一個頁面硬體被訪問,其訪問位一直為1,就一直不會被淘汰;
- 核心使用PG_active表示頁面活躍度,PG_referenced表示頁面是否被訪問過;
其中涉及的主要操作函式有:
- mark_page_accessed()控制狀態位;
- page_referenced()判斷page是否訪問呼叫;
- page_check_references()掃描不活躍LRU連結串列,判斷頁面是否活躍;
(1)mark_page_accessed(struct page *page)
//kernel-4.4/mm/swap.c
void mark_page_accessed(struct page *page)
{
if (!PageActive(page) && !PageUnevictable(page) &&
PageReferenced(page)) {
if (PageLRU(page))
activate_page(page);
else
__lru_cache_activate_page(page);
ClearPageReferenced(page);
if (page_is_file_cache(page))
workingset_activation(page);
} else if (!PageReferenced(page)) {
SetPageReferenced(page);
}
}
a、if PG_active == 0 && PG_reference == 1:
把該頁加入到活躍LRU,並設定PG_active = 1;
清除PG_reference = 0;
b、如果PG_reference == 0:
設定PG_reference = 1;
(2)page_check_references(struct page *page, struct scan_control *sc)
//kernel-4.4/mm/vmscan.c
static enum page_references page_check_references(struct page *page,
struct scan_control *sc)
{
int referenced_ptes, referenced_page;
unsigned long vm_flags;
//page_referenced檢查該頁有多少個訪問引用pte
referenced_ptes = page_referenced(page, 1, sc->target_mem_cgroup,
&vm_flags);
//返回該頁面PG_reference標誌位的值
referenced_page = TestClearPageReferenced(page);
if (referenced_ptes) {
//如果該頁面是匿名頁面,則加入到活躍連結串列
if (PageSwapBacked(page))
return PAGEREF_ACTIVATE;
SetPageReferenced(page);
//如果最近第二次訪問的page cache或shared page cache,則介入到活躍連結串列
if (referenced_page || referenced_ptes > 1)
return PAGEREF_ACTIVATE;
//可執行的檔案加入到活躍連結串列
if (vm_flags & VM_EXEC)
return PAGEREF_ACTIVATE;
//如果都不符合尚需三種情況繼續留在不活躍連結串列等待回收
return PAGEREF_KEEP;
}
//如果沒有訪問引用PTE,可以嘗試回收此page
if (referenced_page && !PageSwapBacked(page))
return PAGEREF_RECLAIM_CLEAN;
return PAGEREF_RECLAIM;
}
(3)page_referenced
page_referenced的函式比較長,這裡不再展示出來,此函式主要完成的工作如下:
- 利用RMAP系統遍歷所有對映該頁面的pte;
- 對於每個pte,如果L_PTE_YOUNG位元位置位,說明之前被訪問過,referenced技術加1,然後情況位元位;
- 返回referenced計數,表示該頁有多少個訪問引用pte;
三、kswapd核心執行緒:
kswapd是非常重要的核心執行緒,負責在記憶體不足的情況下回收頁面,下面會分幾個不同的階段來介紹。
1、kswapd初始化及喚醒:
kswapd在初始化時會在node節點建立一個kswapd%d的核心執行緒,在前面有說過每一個node節點都有
一個pg_data_t的結構體來描敘,與kswapd相關的結構體成員如下:
typedef struct pglist_data {
//kswapd_wait是一個等待佇列
wait_queue_head_t kswapd_wait;
struct task_struct *kswapd;
//在記憶體水位低的時候,通過wakeup_kswapd喚醒kswapd,並傳入如下兩個引數
enum zone_type classzone_idx;
int kswapd_max_order;
} pg_data_t;
int kswapd_run(int nid)
{
pg_data_t *pgdat = NODE_DATA(nid);
int ret = 0;
if (pgdat->kswapd)
return 0;
pgdat->kswapd = kthread_run(kswapd, pgdat, "kswapd%d", nid);
return ret;
}
系統啟動時會通過kswapd_try_to_sleep()函式中睡眠讓出CPU,alloc_page在低水位時(ALLOC_WAMRK_LOW)
無法分配出記憶體時,會通過wakeup_kswapd來喚醒,其中喚醒kswapd執行緒的流程如下:
alloc_pages-> __alloc_pages_nodemask -> __alloc_pages_slowpath ->wake_all_kswapds
void wakeup_kswapd(struct zone *zone, int order, enum zone_type classzone_idx)
{
pg_data_t *pgdat;
......
pgdat->kswapd_max_order = order;
pgdat->classzone_idx = min(pgdat->classzone_idx, classzone_idx);
wake_up_interruptible(&pgdat->kswapd_wait);
}
2、kswapd執行函式回收記憶體:
在記憶體低的時候,喚醒了kswapd回去執行期執行函式,當記憶體節點的的水位處於平衡狀態時,停止回收記憶體:
//balance_pgdat是記憶體回收的核心函式
static int kswapd(void *p)
{
......
for ( ; ; ) {
balanced_classzone_idx = classzone_idx;
balanced_order = balance_pgdat(pgdat, order,
&balanced_classzone_idx);
}
}
static unsigned long balance_pgdat(pg_data_t *pgdat, int order,
int *classzone_idx)
{
do {
//從高階zone查詢第一個處於不平衡水位的end_zone
for (i = pgdat->nr_zones - 1; i >= 0; i--) {
if (!zone_balanced(zone, order, 0, 0)) {
end_zone = i;
break;
}
}
//從低端zone開始回收頁面至end_zone
for (i = 0; i <= end_zone; i++) {
struct zone *zone = pgdat->node_zones + i;
if (kswapd_shrink_zone(zone, end_zone,
&sc, &nr_attempted))
raise_priority = false;
}
//加大掃描粒度進行回收,並且檢查最低端zone到classzone_idx的zone是否處於平衡狀態
//classzone_idx是記憶體分配時計算出的最適合記憶體分配的zone的編號
} while (sc.priority >= 1 &&
!pgdat_balanced(pgdat, order, *classzone_idx));
*classzone_idx = end_zone;
return order;
}
其中記憶體回收涉及到的核心函式如下:
(1)kswapd_shrink_zone:是真正掃描頁面和進行頁面回收的函式,返回true表明回收成功;
(2)shrink_zone/shrink_lruvecshrink_list:用於掃描zone中所有可回收的頁面;
(3)shrink_active_list:掃描活躍LRU連結串列,看是否有頁面可以遷移到不活躍LRU連結串列中;
(4)shrink_inactive_list:掃描不活躍LRU連結串列嘗試回收頁面,返回已回收的頁面數量;
3、總結:
頁面分配和回收的流程如下,是兩個相反的方向,這樣可以避免一些資源或鎖等的競爭關係,從而
程式碼一些資源上的浪費和不必要的BUG,這種設計的思想非常好:
作者:frank_zyp
您的支援是對博主最大的鼓勵,感謝您的認真閱讀。
本文無所謂版權,歡迎轉載。