1. 程式人生 > >記憶體管理九 linux記憶體頁面回收

記憶體管理九 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
您的支援是對博主最大的鼓勵,感謝您的認真閱讀。
本文無所謂版權,歡迎轉載。