天天看點

【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

背景

  • Read the fucking source code!

    --By 魯迅
  • A picture is worth a thousand words.

    --By 高爾基

說明:

  1. Kernel版本:4.14
  2. ARM64處理器,Contex-A53,雙核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

本文将讨論

memory reclaim

記憶體回收這個話題。

在記憶體配置設定出現不足時,可以通過喚醒

kswapd

核心線程來異步回收,或者通過

direct reclaim

直接回收來處理。在針對不同的實體頁會采取相應的回收政策,而頁回收算法采用

LRU(Least Recently Used)

來選擇實體頁。

直奔主題吧。

2.

LRU和pagevec

2.1 資料結構

簡單來說,每個

Node

節點會維護一個

lrvvec

結構,該結構用于存放5種不同類型的

LRU連結清單

,在記憶體進行回收時,在

LRU連結清單

中檢索最少使用的頁面進行處理。

為了提高性能,每個CPU有5個

struct pagevecs

結構,存儲一定數量的頁面(14),最終一次性把這些頁面加入到

LRU連結清單

中。

上述的描述不太直覺,先看代碼,後看圖,一目了然!

typedef struct pglist_data {
...
/* Fields commonly accessed by the page reclaim scanner */
struct lruvec		lruvec;
...
}

/*  5種不同類型的LRU連結清單 */
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
};

struct lruvec {
	struct list_head		lists[NR_LRU_LISTS];
	struct zone_reclaim_stat	reclaim_stat;  //與回收相關的統計資料
	/* Evictions & activations on the inactive file list */
	atomic_long_t			inactive_age;
	/* Refaults at the time of last reclaim cycle */
	unsigned long			refaults;
#ifdef CONFIG_MEMCG
	struct pglist_data *pgdat;
#endif

/* 14 pointers + two long's align the pagevec structure to a power of two */
#define PAGEVEC_SIZE	14
struct pagevec {
	unsigned long nr;
	unsigned long cold;
	struct page *pages[PAGEVEC_SIZE];  //存放14個page結構
};

/*  每個CPU定義5種類型 */
static DEFINE_PER_CPU(struct pagevec, lru_add_pvec);
static DEFINE_PER_CPU(struct pagevec, lru_rotate_pvecs);
static DEFINE_PER_CPU(struct pagevec, lru_deactivate_file_pvecs);
static DEFINE_PER_CPU(struct pagevec, lru_lazyfree_pvecs);
#ifdef CONFIG_SMP
static DEFINE_PER_CPU(struct pagevec, activate_page_pvecs);
#endif
           

上述的資料結構,可以用下圖來進行說明:

【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

簡單來說,在實體記憶體進行回收的時候可以選擇兩種方式:

  • 直接回收,比如某些隻讀代碼段等;
  • 頁面内容儲存後再回收;

針對頁面内容儲存又分為兩種情況:

  1. swap

    支援的頁,寫入到

    swap分區

    後回收,包括程序堆棧段資料段等使用的匿名頁,共享記憶體頁等,

    swap

    區可以是一個磁盤分區,也可以是儲存設備上的一個檔案;
  2. 儲存設備支援的頁,寫入到

    儲存設備

    後回收,主要是針對檔案操作,如果不是髒頁就直接釋放,否則需要先寫回;

有上述這幾種情況,便産生了5種

LRU連結清單

,其中

ACTIVE

INACTIVE

用于表示最近的通路頻率,最終頁面也是在這些連結清單間流轉。

UNEVITABLE

,表示被鎖定在記憶體中,不允許回收的實體頁,比如像核心中大部分頁框都不允許回收。

2.2 流程分析

看一下LRU連結清單的整體操作:

【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

上圖中,主要實作的功能就是将CPU緩存的頁面,轉移到

lruvec

連結清單中,而在轉移過程中,最終會調用

pagevec_lru_move_fn

函數,實際的轉移函數是傳遞給

pagevec_lru_move_fn

的函數指針。在這些具體的轉移函數中,會對

Page

結構狀态位進行判斷,清零,設定等處理,并最終調用

del_page_from_lru_list/add_page_to_lru_list

接口來從一個連結清單中删除,并加入到另一個連結清單中。

首先看看圖中最右側部分中,關于

Page

狀态,在核心中

include/linux/page-flags.h

中有描述,羅列關鍵字段如下:

enum pageflags {
    PG_locked,		/* Page is locked. Don't touch. */
    PG_referenced, //最近是否被通路
    PG_dirty,  //髒頁
    PG_lru,   //處于LRU連結清單中
    PG_active, //活動頁
    PG_swapbacked,		/* Page is backed by RAM/swap */
    PG_unevictable,		/* Page is "unevictable"  */
}    
           

針對這些狀态在該頭檔案中還有一系列的宏來判斷和設定等處理,羅列幾個如下:

ClearPageActive(page);
ClearPageReferenced(page);
SetPageReclaim(page);
PageWriteback(page);
PageLRU(page);
PageUnevictable(page);
...
           

上述的每個CPU5種緩存

struct pagevec

,基本描述了

LRU

連結清單的幾種操作:

  • lru_add_pvec

    :緩存不屬于

    LRU連結清單

    的頁,新加入的頁;
  • lru_rotate_pvecs

    :緩存已經在

    INACTIVE LRU連結清單

    中的非活動頁,将這些頁添加到

    INACTIVE LRU連結清單

    的尾部;
  • lru_deactivate_pvecs

    ACTIVE LRU連結清單

    中的頁,清除掉

    PG_activate, PG_referenced

    标志後,将這些頁加入到

    INACTIVE LRU連結清單

    中;
  • lru_lazyfree_pvecs

    :緩存匿名頁,清除掉

    PG_activate, PG_referenced, PG_swapbacked

    LRU_INACTIVE_FILE

    連結清單中;
  • activate_page_pvecs

    :将LRU中的頁加入到

    ACTIVE LRU連結清單

分析一個典型的流程吧,看看緩存中的頁是如何加入到

lruvec

LRU連結清單

中,對應到圖中的執行流為:

pagevec_lru_add --> pagevec_lru_move_fn --> __pagevec_lru_add_fn

,分别看看這三個函數,代碼簡單直接附上:

/*
 * Add the passed pages to the LRU, then drop the caller's refcount
 * on them.  Reinitialises the caller's pagevec.
 */
void __pagevec_lru_add(struct pagevec *pvec)
{
    //直接調用pagevec_lru_move_fn函數,并傳入轉移函數指針
	pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
}
EXPORT_SYMBOL(__pagevec_lru_add);

static void pagevec_lru_move_fn(struct pagevec *pvec,
	void (*move_fn)(struct page *page, struct lruvec *lruvec, void *arg),
	void *arg)
{
	int i;
	struct pglist_data *pgdat = NULL;
	struct lruvec *lruvec;
	unsigned long flags = 0;

    //周遊緩存中的所有頁
	for (i = 0; i < pagevec_count(pvec); i++) {
		struct page *page = pvec->pages[i];
		struct pglist_data *pagepgdat = page_pgdat(page);

       //判斷是否為同一個node,同一個node不需要加鎖,否則需要加鎖處理
		if (pagepgdat != pgdat) {
			if (pgdat)
				spin_unlock_irqrestore(&pgdat->lru_lock, flags);
			pgdat = pagepgdat;
			spin_lock_irqsave(&pgdat->lru_lock, flags);
		}

       //找到目标lruvec,最終頁轉移到該結構中的LRU連結清單中
		lruvec = mem_cgroup_page_lruvec(page, pgdat);
		(*move_fn)(page, lruvec, arg);  //根據傳入的函數進行回調
	}
	if (pgdat)
		spin_unlock_irqrestore(&pgdat->lru_lock, flags);
    //減少page的引用值,當引用值為0時,從LRU連結清單中移除頁表并釋放掉
	release_pages(pvec->pages, pvec->nr, pvec->cold);
    //重置pvec結構
	pagevec_reinit(pvec);
}

static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
				 void *arg)
{
	int file = page_is_file_cache(page);
	int active = PageActive(page);
	enum lru_list lru = page_lru(page);

	VM_BUG_ON_PAGE(PageLRU(page), page);
    //設定page的狀态位,表示處于Active狀态
	SetPageLRU(page);
    //加入到連結清單中
	add_page_to_lru_list(page, lruvec, lru);
    //更新lruvec中的reclaim_state統計資訊
	update_page_reclaim_stat(lruvec, file, active);
	trace_mm_lru_insertion(page, lru);
}
           

具體的分析在注釋中标明了,其餘4種緩存類型的遷移都大體類似,至于何時進行遷移以及政策,這個在下文中關于記憶體回收的進一步分析中再闡述。

正常情況下,LRU連結清單之間的轉移是不需要的,隻有在需要進行記憶體回收的時候,才需要去在

ACTIVE

INACTIVE

之間去操作。

進入具體的回收分析吧。

3. 頁面回收

3.1 資料結構

memory compact

類似,頁面回收也有一個與之相關的資料結構:

struct scan_control

struct scan_control {
	/* How many pages shrink_list() should reclaim */
	unsigned long nr_to_reclaim;

	/* This context's GFP mask */
	gfp_t gfp_mask;

	/* Allocation order */
	int order;

	/*
	 * Nodemask of nodes allowed by the caller. If NULL, all nodes
	 * are scanned.
	 */
	nodemask_t	*nodemask;

	/*
	 * The memory cgroup that hit its limit and as a result is the
	 * primary target of this reclaim invocation.
	 */
	struct mem_cgroup *target_mem_cgroup;

	/* Scan (total_size >> priority) pages at once */
	int priority;

	/* The highest zone to isolate pages for reclaim from */
	enum zone_type reclaim_idx;

	/* Writepage batching in laptop mode; RECLAIM_WRITE */
	unsigned int may_writepage:1;

	/* Can mapped pages be reclaimed? */
	unsigned int may_unmap:1;

	/* Can pages be swapped as part of reclaim? */
	unsigned int may_swap:1;

	/*
	 * Cgroups are not reclaimed below their configured memory.low,
	 * unless we threaten to OOM. If any cgroups are skipped due to
	 * memory.low and nothing was reclaimed, go back for memory.low.
	 */
	unsigned int memcg_low_reclaim:1;
	unsigned int memcg_low_skipped:1;

	unsigned int hibernation_mode:1;

	/* One of the zones is ready for compaction */
	unsigned int compaction_ready:1;

	/* Incremented by the number of inactive pages that were scanned */
	unsigned long nr_scanned;

	/* Number of pages freed so far during a call to shrink_zones() */
	unsigned long nr_reclaimed;
};
           
  • nr_to_reclaim

    :需要回收的頁面數量;
  • gfp_mask

    :申請配置設定的掩碼,使用者申請頁面時可以通過設定标志來限制調用底層檔案系統或不允許讀寫儲存設備,最終傳遞給LRU處理;
  • order

    :申請配置設定的階數值,最終期望記憶體回收後能滿足申請要求;
  • nodemask

    :記憶體節點掩碼,空指針則通路所有的節點;
  • priority

    :掃描LRU連結清單的優先級,用于計算每次掃描頁面的數量

    (total_size >> priority,初始值12)

    ,值越小,掃描的頁面數越大,逐級增加掃描粒度;
  • may_writepage

    :是否允許把修改過檔案頁寫回儲存設備;
  • may_unmap

    :是否取消頁面的映射并進行回收處理;
  • may_swap

    :是否将匿名頁交換到swap分區,并進行回收處理;
  • nr_scanned

    :統計掃描過的非活動頁面總數;
  • nr_reclaimed

    :統計回收了的頁面總數;

3.2 總體流程分析

與頁面壓縮類似,有兩種方式來觸發頁面回收:

  1. 記憶體節點中的記憶體空閑頁面低于

    low watermark

    時,

    kswapd

    核心線程被喚醒,進行異步回收;
  2. 在記憶體配置設定的時候,遇到記憶體不足,空閑頁面低于

    min watermark

    時,直接進行回收;

兩種方式的調用流程如下圖所示:

【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

3.3 直接回收

  • __alloc_pages_slowpath

    該函數調用

    _perform_reclaim

    來對頁面進行回收處理後,再重新申請配置設定頁面,如果第一次申請失敗,将pcp緩存清空後再retry。
  • __perform_reclaim

    該函數中做了以下工作:
  1. 如果設定了

    cpuset_memory_pressure_enabled

    ,則先更新目前任務的

    cpuset頻率表fmeter

    ;
  2. 将目前任務的标志置上

    PF_MEMALLOC

    ,防止遞歸調用頁面回收例程;
  3. 調用

    try_to_free_pages

    來進行回收處理;
  4. 恢複目前任務的标志;
  • try_to_free_pages

    try_to_free_pages

    函數中,主要完成了以下工作:
  1. 初始化

    struct scan_control sc

    結構;
  2. throttle_direct_reclaim

    函數進行判斷,該函數會對使用者任務的直接回收請求進行限制;
  3. do_try_to_free_pages

    進行回收處理;
    【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

再來看看

throttle_direct_reclaim

函數中調用的

alloc_direct_reclaim

【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

隻有

throttle_direct_reclaim

函數傳回值為false,頁面的回收才會進一步往下執行。

  • do_try_to_free_pages

  1. 通過

    delayacct_freepages_start/delayacct_freepages_end

    量化頁面回收的時間開銷;
  2. 随着回收優先級的調整,通過

    vmpressure_prio

    來更新

    memory pressure

    值;
  3. 循環調用

    shrink_zones

    來回收頁面,回收頁面足夠了或者可以進行記憶體壓縮時,就會跳出循環不再進行回收處理;

3.4 異步回收

kswapd

核心線程,當空閑頁面低于watermark時會被喚醒,進行頁面回收處理,

balance_pgdat

是回收的主函數,如下圖:

【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

異步回收線程和同步直接回收存程序在互動的地方:

  1. 在低水位情況下程序在直接回收時會喚醒

    kswapd

    線程;
  2. 異步回收時,

    kswapd

    線程也會通過

    wake_up_all(&pgdat->pfmemalloc_wait)

    來喚醒等待在該隊列上進行同步回收的程序;

kswapd

核心線程會在記憶體節點達到平衡狀态時,退出LRU連結清單的掃描。

3.5

shrink_node

前邊鋪墊了很多,真正的主角要上場了,不管是同步還是異步的回收,最終都落實在

shrink_node

函數上。

【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

shrink_node

的調用關系如上圖所示,下邊将針對關鍵函數進行分析。

  • get_scan_count

    這個函數用于擷取針對檔案頁和匿名頁的掃描頁面數。這個函數決定記憶體回收每次掃描多少頁,匿名頁和檔案頁分别是多少,比例如何配置設定等。

    在函數的執行過程中,根據四種掃描平衡的方法标簽來最終選擇計算方式,四種掃描平衡标簽如下:

enum scan_balance {
	SCAN_EQUAL,  // 計算出的掃描值按原樣使用
	SCAN_FRACT,  // 将分數應用于計算的掃描值
	SCAN_ANON,  // 對于檔案頁LRU,将掃描次數更改為0
	SCAN_FILE,     // 對于匿名頁LRU,将掃描次數更改為0
};
           

來一張圖:

【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5
  • shrink_node_memcg

    shrink_node_memcg

    函數中,調用了

    get_scan_count

    函數之後,擷取到了掃描頁面的資訊後,就開始進入主題對LRU連結清單進行掃描處理了。它會對匿名頁和檔案頁做平衡處理,選擇更合适的頁面來進行回收。當回收的頁面超過了目标頁面數後,将停止對檔案頁和匿名頁兩者間LRU頁面數少的那一方的掃描,并調整對頁面數多的另一方的掃描速度。最後,如果不活躍頁面少于活躍頁面,則需要将活躍頁面遷移到不活躍頁面連結清單中。
    【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5
  • shrink_list

    shrink_list

    函數中主要是從

    lruvec

    的連結清單中進行頁面回收:
  1. 僅當活動頁面數多于非活動頁面數時才調用

    shrink_active_list

    對活動連結清單處理;
  2. shrink_inactive_list

    對非活動連結清單進行處理;
  • shrink_active_list

    從函數的調用關系圖中可以看出,

    shrink_active_list/shrink_inactive_list

    函數都調用了

    isolate_lru_pages

    函數,有必要先了解一下這個函數。

    isolate_lru_pages

    函數,完成的工作就是從指定的

    lruvec

    中連結清單掃描目标數量的頁面進行分離處理,并将分離的頁面以連結清單形式傳回。而在這個過程中,有些特殊頁面不能進行分離處理時,會被rotate到LRU連結清單的頭部。

shrink_active_list

的整體效果圖如下:

【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

先對

LRU ACTIVE

連結清單做

isolate

操作,這部分操作會分離出來一部分頁面,然後再對這些分離頁面做進一步的判斷,根據最近是否被

referenced

以及其它标志位做處理,基本上有四種去向:

1)rotate回原來的

ACTIVE連結清單

2)處理成功移動到對應的

UNACTIVE連結清單

3)不再使用傳回Buddy系統;

4)如果出現了不可回收的情況(機率比較低),則放回

LRU_UNEVICTABLE

連結清單。

  • shrink_inactive_list

    記憶體回收的最後一步就是處理

    LRU_UNACTIVE連結清單

    了,該寫回儲存設備的寫回儲存設備,該寫到

    Swap

    分區的寫到

    Swap

    分區,最終就是釋放處理。

    在提供最終效果圖之前,先來分析一下

    shrink_page_list

    函數,它是

    shrink_inactive_list

    的核心。
    【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

從上圖中可以看出,

shrink_page_list

函數執行完畢後,頁面要不就是rotate回原來的LRU連結清單中了,要不就是進行回收并最終傳回了Buddy System了。

是以,最終的

shrink_inactive_list

的效果如下圖:

【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

頁面回收的子產品還是挺複雜的,還有很多内容沒有深入細扣,比如頁面反向映射,memcg記憶體控制組等。

前前後後看了半個月時間的代碼,就此收工。

下一個專題要開始看看

SLUB記憶體配置設定器

了,待續。

【原創】(十)Linux記憶體管理 - zoned page frame allocator - 5

作者:LoyenWang

出處:https://www.cnblogs.com/LoyenWang/

公衆号:

LoyenWang

版權:本文版權歸作者和部落格園共有

轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接配接;否則必究法律責任

繼續閱讀