本文為原創,轉載請注明:http://www.cnblogs.com/tolimit/
概述
當linux系統內存壓力就大時,就會對系統的每個壓力大的zone進程內存回收,內存回收主要是針對匿名頁和文件頁進行的。對於匿名頁,內存回收過程中會篩選出一些不經常使用的匿名頁,將它們寫入到swap分區中,然后作為空閑頁框釋放到伙伴系統。而對於文件頁,內存回收過程中也會篩選出一些不經常使用的文件頁,如果此文件頁中保存的內容與磁盤中文件對應內容一致,說明此文件頁是一個干凈的文件頁,就不需要進行回寫,直接將此頁作為空閑頁框釋放到伙伴系統中,相反,如果文件頁保存的數據與磁盤中文件對應的數據不一致,則認定此文件頁為臟頁,需要先將此文件頁回寫到磁盤中對應數據所在位置上,然后再將此頁作為空閑頁框釋放到伙伴系統中。這樣當內存回收完成后,系統空閑的頁框數量就會增加,能夠緩解內存壓力,聽起來很厲害,它也有一個弊端,就是在回收過程中會對系統的IO造成很大的壓力,所以,在系統內,一般每個zone會設置一條線,當空閑頁框數量不滿足這條線時,就會執行內存回收操作,而系統空閑頁框數量滿足這條線時,系統是不會進行內存回收操作的。
zone的閥值
內存回收是以zone為單位進行的(也會以memcg為單位,這里不討論這種情況),而系統判斷一個zone需不需要進行內存回收,如上面所說,為zone設置一條線,當此zone的空閑頁框不足以到達這條線時,就會對此zone進行內存回收,實際上一個zone有三條線,這三條線分別是最小閥值(WMARK_MIN),低閥值(WMARK_LOW),高閥值(WMARK_HIGH),它們都保存在zone的watermark[NR_WMARK]數組中,這個數組中保存的是各個閥值要求的頁框數量,而每個閥值都會對內存回收造成影響。而它們的描述如下:
- watermark[WMARK_MIN](min閥值):在快速分配失敗后的慢速分配中會使用此閥值進行分配,如果慢速分配過程中使用此值還是無法進行分配,那就會執行直接內存回收和快速內存回收
- watermark[WMARK_LOW](low閥值):也叫低閥值,是快速分配的默認閥值,在分配內存過程中,如果zone的空閑頁框數量低於此閥值,系統會對zone執行快速內存回收
- watermark[WMARK_HIGH](high閥值):也叫高閥值,是zone對於空閑頁框數量比較滿意的一個值,當zone的空閑頁框數量高於這個值時,表示zone的空閑頁框較多。所以對zone進行內存回收時,目標也是希望將zone的空閑頁框數量提高到此值以上,系統會使用此閥值用於oomkill進行內存回收。
這三個閥值的關系是:min閥值 < low閥值 < high閥值。在系統初始化期間,根據系統中整個內存的數量與每個zone管理的頁框數量,計算出每個zone的min閥值,然后low閥值 = min閥值 + (min閥值 / 4),high閥值 = min閥值 + (min閥值 / 2)。這樣就得出了這三個閥值的數值,我們可以通過/proc/zoneinfo中查看這三個閥值的數值:


判斷頁是否能夠回收
- 一個進程映射此頁,page->_count++
- 一個進程取消映射此頁,page->_count--
- 此頁加入到lru緩存中,page->_count++
- 此頁從lru緩存加入到lru鏈表中,page->_count--
- 此頁被加入到一個address_space中,page->_count++
- 此頁從address_space中移除時,page->_count--
- 文件頁添加了buffer_heads,page->_count++
- 文件頁刪除了buffer_heads,page->_count--
lru鏈表
頁的換入換出
首先先說明一下頁描述符中對內存回收來說非常必要的標志:
- PG_lru:表示頁在lru鏈表中
- PG_referenced: 表示頁最近被訪問(只有文件頁使用)
- PG_dirty:頁為臟頁,文件頁被修改,以及非文件頁加入到swap cache后,就會被標記為臟頁。在此頁回寫前會被清除,但是回寫失敗時又會被置位
- PG_active:頁為活動頁,配合PG_lru就可以得出頁是處於非活動頁lru鏈表還是活動頁lru鏈表
- PG_private:頁描述符中的page->private保存有數據
- PG_writeback:頁正在進行回寫
- PG_swapbacked:此頁可寫入swap分區,一般用於表示此頁是非文件頁
- PG_swapcache:頁已經加入到了swap cache中(只有非文件頁使用)
- PG_reclaim:頁正在進行回收,只有在內存回收時才會對需要回收的頁進行此標記
- PG_mlocked:頁被鎖在內存中
內存回收做的事情就是想辦法將目標頁的page->_count降到0,對於那些沒有進程映射了頁,釋放起來就很簡單,如果頁映射了磁盤文件,並且頁為臟頁(被寫過),那就就把頁中的數據回寫到磁盤中映射的文件中,而如果頁沒有映射磁盤文件,那么直接釋放即可。但是對於有進程映射的頁,如果此頁映射了磁盤文件,並且頁為臟頁,那么和之前一樣,將此頁進行回寫,然后釋放回收即可,但是此頁沒有映射磁盤文件,情況就會稍微復雜,會將頁數據寫入到swap分區中,然后將此頁釋放回收。總結如下:
- 干凈頁,並且映射了磁盤文件的頁,直接回收
- 臟頁(PG_dirty置位),回寫到對應磁盤文件中,然后回收
- 沒有進程映射,並且沒有映射磁盤文件的頁,直接回收
- 有進程映射,並且沒有映射磁盤文件的頁,回寫到swap分區中,然后回收
接下來會分為非活動匿名頁lru鏈表的頁的換入換出,非活動文件頁lru鏈表的頁的換入換出進行描述。

- swap分配一個空閑的頁槽
- 根據這個空閑頁槽的ID,從swap分區的swap cache的基樹中找到此頁槽ID對應的結點,將此頁的頁描述符存入當中
- 內核以頁槽ID作為偏移量生成一個swap頁表項,並將這個swap頁表項保存到頁描述符中的private中
- 對頁進行反向映射,將所有映射了此頁的進程頁表項改為此swap頁表項
- 將此頁的mapping改為指向此swap分區的address_space,並將此頁設置為臟頁
- 通過swap cache中的address_space操作集將此頁回寫到swap分區中
- 回寫完成
- 此頁要被回收,將此頁從swap cache中拿出來
當一個進程需要訪問此頁時,系統則會將此頁從swap分區換入內存中,具體步驟如下:
- 一個進行訪問了此頁,會先訪問到之前設置的swap頁表項
- 產生缺頁異常,在缺頁異常中判斷此頁在swap分區中,而不在內存中
- 分配一個新頁
- 根據進程的頁表項中的swap頁表項找到對應的頁槽和swap cache
- 如果以頁槽ID在swap cache中沒有找到此頁,說明此頁已被回收,從分區中將此頁讀取進來
- 如果以頁槽ID在swap cache中找到了此頁,說明此頁還在內存中,還沒有被回收,則直接映射此頁
這樣再此頁沒有被換出或者正在換出的情況下,所有映射了此頁的進程又可以重新訪問此頁了,而當此頁被完全換出到swap分區然后被回收后,此頁就會從swap cache中移除,之后如果進程想要訪問此頁,就需要等此頁被完全換入之后才行了。也就是這個swap cache完全為了提高效率,在頁沒有被回收前,即使此頁已經回寫到swap分區了,只要有進映射此頁,就可以直接映射內存中的頁,而不需要將頁從磁盤讀進來。對於非活動匿名頁lru鏈表上的頁進行換入換出這里就算是說完了。記住對於非活動匿名頁lru鏈表上的頁來說,當此頁加入到swap cache中時,那么就意味着這個頁已經被要求換出,然后進行回收了。
但是相反文件頁則不是這樣,接下來簡單說說映射了磁盤文件的文件頁的換入換出,實際上與非活動匿名頁lru鏈表上的頁進行換入換出是一模一樣的,因為每個磁盤文件都有一個自己的address_space,這個address_space就是swap分區的address_space,磁盤文件的address_space稱為page cache,接下來的處理就是差不多的,區別為以下三點:
- 對於磁盤文件來說,它的數據並不像swap分區這樣是連續的。
- 當文件數據讀入到一個頁時,此文件頁就需要在文件的page cache中做關聯,這樣當其他進程也需要訪問文件的這塊數據時,通過page cache就可以知道此頁在不在內存中了。
- 並不會為映射了此文件頁的進程頁表項生成一個新的頁表項,會將所有映射了此頁的頁表項清空,因為在缺頁異常中通過vma就可以判斷發生缺頁的頁是映射了文件的哪一部分,然后通過文件系統可以查到此頁在不在內存中。而對於匿名頁的vma來說,則無法做到這一點。
內存分配過程
要說清楚內存回收,就必須要先理清楚內存分配過程,在調用alloc_page()或者alloc_pages()等接口進行一次內存分配時,最后都會調用到__alloc_pages_nodemask()函數,這個函數是內存分配的心臟,對內存分配流程做了一個整體的組織。具體可以見我博客的另一篇文章linux內存源碼分析 - 伙伴系統(初始化和申請頁框)。主要需要注意的,就是在__alloc_pages_nodemask()中會進行一次使用low閥值的快速內存分配和一次使用min閥值的慢速內存分配,快速內存分配使用的函數是get_page_from_freelist(),這個函數是分配頁框的基本函數,也就是說,在慢速內存分配過程中,收集到和足夠數量的頁框后,也需要調用這個函數進行分配。先簡單說明快速內存分配和慢速內存分配:
- 快速內存分配:是get_page_from_freelist()函數,通過low閥值從zonelist中獲取合適的zone進行分配,如果zone沒有達到low閥值,則會進行快速內存回收,快速內存回收后再嘗試分配。
- 慢速內存分配:當快速分配失敗后,也就是zonelist中所有zone在快速分配中都沒有獲取到內存,則會使用min閥值進行慢速分配,在慢速分配過程中主要做三件事,異步內存壓縮、直接內存回收以及輕同步內存壓縮,最后視情況進行oom分配。並且在這些操作完成后,都會調用一次快速內存分配嘗試獲取頁框。
通過以下這幅圖,來說明流程:
說到內存分配過程,就必須要說說中的preferred_zone和zonelist,preferred_zone可以理解為內存分配時,最希望從這個zone進行分配,而zonelist理解為,當沒辦法從preferred_zone分配內存時,則根據zonelist中zone的順序嘗試進行分配,為什么會有這兩個參數,是因為numa架構導致的,我們知道,當有多個node結點時,CPU跨結點訪問內存是效率比較低的工作,所以CPU會優先在本node上的zone進行內存分配工作,如果本node上實在分配不出內存,那就嘗試在離本node最近的node上分配,如果還是無法分配到,那就找再下一個node。這樣每個node會將其他node的距離進行一個排序形成了其他node的一個鏈表,這個鏈表越前面的node就表示里本node越近,越后面的node就離本node越遠。而在32位系統中,每個node有3個zone,分別是ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA。每個區管理的內存數量不一樣,導致每個區的優先級不同,優先級為ZONE_HIGHMEM > ZONE_NORMAL > ZONE_DMA,對於進程使用的頁,系統優先分配ZONE_HIGHMEM的頁框,如果ZONE_HIGHMEM無法分配頁框,則從ZONE_NORMAL進行分配,當然,對於內核使用的頁來說,大部分只會從ZONE_NORMAL和ZONE_DMA進行分配,這樣,將這個zone優先級與node鏈表結合,就得到zonelist鏈表了,比如對於node0,它完整的zonelist鏈表就可能如下:
node0的管理區 node1的管理區
ZONE_HIGHMEM(0) -> ZONE_NORMAL(0) -> ZONE_DMA(0) -> ZONE_HIGHMEM(1) -> ZONE_NORMAL(1) -> ZONE_DMA(1)
因為每個node都有自己完整的zonelist鏈表,所以對於node1,它的鏈表時這樣的
node1的管理區 node0的管理區
ZONE_HIGHMEM(1) -> ZONE_NORMAL(1) -> ZONE_DMA(1) -> ZONE_HIGHMEM(0) -> ZONE_NORMAL(0) -> ZONE_DMA(0)
這樣得到了兩個node自己的zonelist,但是在內存分配中,還不一定會使用node自己的zonelist,因為有些內存只希望從ZONE_NORMAL和ZONE_DMA中進行分配,所以,在每次進行內存分配時,都會此次內存分配形成一個滿足的zonelist,比如:某次內存分配在node0的CPU上執行了,希望從ZONE_NORMAL和ZONEDMA區中進行分配,那么就會形成下面這個鏈表
node0的管理區 node1的管理區
ZONE_NORMAL(0) -> ZONE_DMA(0) -> ZONE_NORMAL(1) -> ZONE_DMA(1)
這樣就是preferred_zone和zonelist,preferred_zone一般都是指向zonelist中的第一個zone,當然這個還會跟nodemask有關,這個就不細說了。
掃描控制結構
之前說內存壓縮的文章也有涉及這個結構,現在詳細說明一下,掃描控制結構用於內存回收和內存壓縮,它的主要作用時保存對一次內存回收或者內存壓縮的變量和參數,一些處理結果也會保存在里面,結構如下:
/* 掃描控制結構,用於內存回收和內存壓縮 */ struct scan_control { /* 需要回收的頁框數量 */ unsigned long nr_to_reclaim; /* 申請內存時使用的分配標志 */ gfp_t gfp_mask; /* 申請內存時使用的order值,因為只有申請內存,然后內存不足時才會進行掃描 */ int order; /* 允許執行掃描的node結點掩碼 */ nodemask_t *nodemask; /* 目標memcg,如果是針對整個zone進行的,則此為NULL */ struct mem_cgroup *target_mem_cgroup; /* 掃描優先級,代表一次掃描(total_size >> priority)個頁框 * 優先級越低,一次掃描的頁框數量就越多 * 優先級越高,一次掃描的數量就越少 * 默認優先級為12 */ int priority; /* 是否能夠進行回寫操作(與分配標志的__GFP_IO和__GFP_FS有關) */ unsigned int may_writepage:1; /* 能否進行unmap操作,就是將所有映射了此頁的頁表項清空 */ unsigned int may_unmap:1; /* 是否能夠進行swap交換,如果不能,在內存回收時則不掃描匿名頁lru鏈表 */ unsigned int may_swap:1; unsigned int hibernation_mode:1; /* 掃描結束后會標記,用於內存回收判斷是否需要進行內存壓縮 */ unsigned int compaction_ready:1; /* 已經掃描的頁框數量 */ unsigned long nr_scanned; /* 已經回收的頁框數量 */ unsigned long nr_reclaimed; };
結構很簡單,主要就是保存一些參數,在內存回收和內存壓縮時就會根據這個結構中的這些參數,做不同的處理,后面代碼會詳細說明。
這里我們只說說會幾個特別的參數:
- priority:優先級,這個參數主要會影響內存回收時一次掃描的頁框數量、在shrink_lruvec()中回收到足夠頁框后是否繼續回收、內存回收時的回寫、是否取消對zone進行回收判斷而直接開始回收,一共四個地方。
- may_unmap:是否能夠進行unmap操作,如果不能進行unmap操作,就只能對沒有進程映射的頁進行回收。
- may_writepage:是否能夠進行將頁回寫到磁盤的操作,這個值會影響臟的文件頁與匿名頁lru鏈表中的頁的回收,如果不能進行回寫操作,臟頁和匿名頁lru鏈表中的頁都不能進行回收(已經回寫完成的頁除外,后面解釋)
- may_swap:能否進行swap交換,同樣影響匿名頁lru鏈表中的頁的回收,如果不能進行swap交換,就不會對匿名頁lru鏈表進行掃描,也就是在本次內存回收中,完全不會回收匿名頁lru鏈表中的頁(進程堆、棧、shmem共享內存、匿名mmap共享內存使用的頁)
在快速內存回收、直接內存回收、kswapd內存回收中,這幾個值的設置不一定會一致,也導致了它們對不同類型的頁處理方式也不同。
除了sc->may_writepage會影響頁的回寫外,還有進行內存分配時使用的分配標志gfp_mask中的__GFP_IO和__GFP_FS會影響頁的回寫,具體如下:
- 掃描到的非活動匿名頁lru鏈表中的頁如果還沒有加入到swapcache中,需要有__GFP_IO標記才允許加入swapcache和回寫。
- 掃描到的非活動匿名頁lru鏈表中的頁如果已經加入到了swapcache中,需要有__GFP_FS才允許進行回寫。
- 掃描到的非活動文件頁lru鏈表中的頁需要有__GFP_FS才允許進行回寫。
這里還需要說說三個重要的內核配置:
/proc/sys/vm/zone_reclaim_mode
這個參數只會影響快速內存回收,其值有三種,
- 0x1:開啟zone的內存回收
- 0x2:開啟zone的內存回收,並且允許回寫
- 0x4:開啟zone的內存回收,允許進行unmap操作
當此參數為0時,會導致快速內存回收只會對最優zone附近的幾個需要進行內存回收的zone進行內存回收(說快速內存會解釋),而只要不為0,就會對zonelist中所有應該進行內存回收的zone進行內存回收。
當此參數為0x1(001)時,就如上面一行所說,允許快速內存回收對zonelist中所有應該進行內存回收的zone進行內存回收。
當此參數為0x2(010)時,在0x1的基礎上,允許快速內存回收進行匿名頁lru鏈表中的頁的回寫操作。
當此參數0x4(100)時,在0x1的基礎上,允許快速內存回收進行頁的unmap操作。
/proc/sys/vm/laptop_mode
此參數只會影響直接內存回收,只有兩個值:
- 0:允許直接內存回收對匿名頁lru鏈表中的頁進行回寫操作,並且允許直接內存回收喚醒flush內核線程
- 非0:直接內存回收不會對匿名頁lru鏈表中的頁進行回寫操作
/proc/sys/vm/swapiness
此參數影響進行內存回收時,掃描匿名頁lru鏈表和掃描文件頁lru鏈表的比例,范圍是0~200,系統默認是30:
- 接近0:進行內存回收時,更多地去掃描文件頁lru鏈表,如果為0,那么就不會去掃描匿名頁lru鏈表。
- 接近200:進行內存回收時,更多地去掃描匿名頁lru鏈表。
內存回收
對zone進行一次內存回收流程
內存回收可以針對某個zone進行回收,也可以針對某個memcg進行回收,這里我們就只討論針對某個zone進行回收的情況,無論是針對zone進行內存回收還是針對memcg進行內存回收,整個內核只有一個函數入口,就是是shrink_zone()函數,也就是內核中無論怎么樣進行內存回收,最終調用到的函數都會是這個shrink_zone(),這個函數要求調用者傳入一個設置好的struct scan_control結構以及目標zone的指針。雖然是對zone進行一次內存回收,但是實際上在這個函數里,如果此zone還可以回收頁框時,可能會對zone進行多次的內存回收,這是因為兩個方面
- 如果每次僅回收2^order個頁框,滿足於本次內存分配(內存分配失敗時才會導致內存回收),那么下次內存分配時又會導致內存回收,影響效率,所以,每次zone的內存回收,都是盡量回收更多頁框,制定回收的目標是2^(order+1)個頁框,比要求的2^order多了一倍。但是當非活動lru鏈表中的數量不滿足這個標准時,則取消這種狀態的判斷。
- zone的內存回收后往往伴隨着zone的內存壓縮(見linux內存源碼分析 - 內存壓縮),所以進行zone的內存回收時,會回收到空閑頁框數量滿足進行內存壓縮為止。
我們看一下這個shrink_zone():
/* 對zone進行內存回收 * 返回是否回收到了頁框,而不是十分回收到了sc中指定數量的頁框 * 即使沒回收到sc中指定數量的頁框,只要回收到了頁框,就返回真 */ static bool shrink_zone(struct zone *zone, struct scan_control *sc) { unsigned long nr_reclaimed, nr_scanned; bool reclaimable = false; do { /* 當內存回收是針對整個zone時,sc->target_mem_cgroup為NULL */ struct mem_cgroup *root = sc->target_mem_cgroup; struct mem_cgroup_reclaim_cookie reclaim = { .zone = zone, .priority = sc->priority, }; struct mem_cgroup *memcg; /* 記錄本次回收開始前回收到的頁框數量 * 第一次時是0 */ nr_reclaimed = sc->nr_reclaimed; /* 記錄本次回收開始前掃描過的頁框數量 * 第一次時是0 */ nr_scanned = sc->nr_scanned; /* 獲取最上層的memcg * 如果沒有指定開始的root,則默認是root_mem_cgroup * root_mem_cgroup管理的每個zone的lru鏈表就是每個zone完整的lru鏈表 */ memcg = mem_cgroup_iter(root, NULL, &reclaim); do { struct lruvec *lruvec; int swappiness; /* 獲取此memcg在此zone的lru鏈表 * 如果內核沒有開啟memcg,那么就是zone->lruvec */ lruvec = mem_cgroup_zone_lruvec(zone, memcg); /* 從memcg中獲取swapiness,此值代表了進行swap的頻率,此值較低時,那么就更多的進行文件頁的回收,此值較高時,則更多進行匿名頁的回收 */ swappiness = mem_cgroup_swappiness(memcg); /* 對此memcg的lru鏈表進行回收工作 * 此lru鏈表中的所有頁都是屬於此zone的 * 每個memcg中都會為每個zone維護一個lru鏈表 */ shrink_lruvec(lruvec, swappiness, sc); /* 如果是對於整個zone進行回收,那么會遍歷所有memcg,對所有memcg中此zone的lru鏈表進行回收 * 而如果只是針對某個memcg進行回收,如果回收到了足夠內存則返回,如果沒回收到足夠內存,則對此memcg下面的memcg進行回收 */ if (!global_reclaim(sc) && sc->nr_reclaimed >= sc->nr_to_reclaim) { mem_cgroup_iter_break(root, memcg); break; } /* 下一個memcg,對於整個zone進行回收和對某個memcg進行回收但回收數量不足時會執行到此 */ memcg = mem_cgroup_iter(root, memcg, &reclaim); } while (memcg); /* 計算此memcg的內存壓力,保存到memcg->vmpressure */ vmpressure(sc->gfp_mask, sc->target_mem_cgroup, sc->nr_scanned - nr_scanned, sc->nr_reclaimed - nr_reclaimed); if (sc->nr_reclaimed - nr_reclaimed) reclaimable = true; /* 判斷是否再次此zone進行內存回收 * 繼續對此zone進行內存回收有兩種情況: * 1. 沒有回收到比目標order值多一倍的數量頁框,並且非活動lru鏈表中的頁框數量 > 目標order多一倍的頁 * 2. 此zone不滿足內存壓縮的條件,則繼續對此zone進行內存回收 * 而當本次內存回收完全沒有回收到頁框時則返回,這里大概意思就是想回收比order更多的頁框 */ } while (should_continue_reclaim(zone, sc->nr_reclaimed - nr_reclaimed, sc->nr_scanned - nr_scanned, sc)); return reclaimable; }
在此函數中,首先會遍歷memcg,根據memcg獲取lru鏈表描述符lruvec與swapiness,這個swapiness的值的范圍是0~200,它會影響掃描匿名頁lru鏈表和文件頁lru鏈表的頁框數量,當此值越低時,就需要掃描的匿名頁lru鏈表的頁框越少,當此值為0時,則不掃描匿名頁lru鏈表的頁框,相反,此值越高,則需要掃描的匿名頁lru鏈表的頁框越多,當其為200時,則只掃描匿名頁lru鏈表中的頁框,不掃描文件頁lru鏈表中的頁框。然后調用shrink_lruvec()對此lru鏈表描述符的lru鏈表進行掃描,最后遍歷完所有memcg后,判斷是否繼續對此zone進行內存回收,總的來說,流程如下:
- 從root_memcg開始遍歷memcg
- 獲取memcg的lru鏈表描述符lruvec
- 獲取memcg的swapiness
- 調用shrink_lruvec()對此memcg的lru鏈表進行處理
- 遍歷完所有memcg后,檢查是否還要對此zone再次進行內存回收。
核心函數就是shrink_lruvec(),我們先看代碼:
/* 對lru鏈表描述符lruvec中的lru鏈表進行內存回收,此lruvec有可能屬於一個memcg,也可能是屬於一個zone * lruvec: lru鏈表描述符,里面有5個lru鏈表,活動/非活動匿名頁lru鏈表,活動/非活動文件頁lru鏈表,禁止換出頁鏈表 * swappiness: 掃描匿名頁的親和力,其值越低,就掃描越少的匿名頁,當為0時,基本不會掃描匿名頁lru鏈表,除非針對整個zone進行內存回收時,此zone的所有文件頁都釋放了都不能達到高閥值,那就只對匿名頁進行掃描 * sc: 掃描控制結構 */ static void shrink_lruvec(struct lruvec *lruvec, int swappiness, struct scan_control *sc) { unsigned long nr[NR_LRU_LISTS]; unsigned long targets[NR_LRU_LISTS]; unsigned long nr_to_scan; enum lru_list lru; unsigned long nr_reclaimed = 0; /* 需要回收的頁框數量 */ unsigned long nr_to_reclaim = sc->nr_to_reclaim; struct blk_plug plug; bool scan_adjusted; /* 對這個lru鏈表描述符中的每個lru鏈表,計算它們本次掃描應該掃描的頁框數量 * 計算好的每個lru鏈表需要掃描的頁框數量保存在nr中 * 每個lru鏈表需要掃描多少與sc->priority有關,sc->priority越小,那么掃描得越多 */ get_scan_count(lruvec, swappiness, sc, nr); /* 將nr的數據復制到targets中 */ memcpy(targets, nr, sizeof(nr)); /* 是否將nr[]中的數量頁數都掃描完才停止 * 如果是針對整個zone進行掃描,並且不是在kswapd內核線程中調用的,優先級為默認優先級,就會無視需要回收的頁框數量,只有將nr[]中的數量頁數都掃描完才停止 * 快速回收不會這樣做(快速回收的優先級不是DEF_PRIORITY) */ scan_adjusted = (global_reclaim(sc) && !current_is_kswapd() && sc->priority == DEF_PRIORITY); /* 初始化這個struct blk_plug * 主要初始化list,mq_list,cb_list這三個鏈表頭 * 然后current->plug = plug */ blk_start_plug(&plug); /* 如果LRU_INACTIVE_ANON,LRU_ACTIVE_FILE,LRU_INACTIVE_FILE這三個其中一個需要掃描的頁框數沒有掃描完,那掃描就會繼續 * 注意這里不會判斷LRU_ACTIVE_ANON需要掃描的頁框數是否掃描完,這里原因大概是因為系統不太希望對匿名頁lru鏈表中的頁回收 */ while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] || nr[LRU_INACTIVE_FILE]) { unsigned long nr_anon, nr_file, percentage; unsigned long nr_scanned; /* 以LRU_INACTIVE_ANON,LRU_INACTIVE_ANON,LRU_INACTIVE_FILE,LRU_ACTIVE_FILE這個順序遍歷lru鏈表 * 然后對遍歷到的lru鏈表進行掃描,一次最多32個頁框 */ for_each_evictable_lru(lru) { /* nr[lru類型]如果有頁框需要掃描 */ if (nr[lru]) { /* 獲取本次需要掃描的頁框數量,nr[lru]與SWAP_CLUSTER_MAX的最小值 * 也就是每一輪最多只掃描SWAP_CLUSTER_MAX(32)個頁框 */ nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX); /* nr[lru類型]減掉本次需要掃描的頁框數量 */ nr[lru] -= nr_to_scan; /* 對此lru類型的lru鏈表進行內存回收 * 一次掃描的頁框數是nr[lru]與SWAP_CLUSTER_MAX的最小值,也就是如果全部能回收,一次也就只能回收SWAP_CLUSTER_MAX(32)個頁框 * 都是從lru鏈表末尾向前掃描 * 本次回收的頁框數保存在nr_reclaimed中 */ nr_reclaimed += shrink_list(lru, nr_to_scan, lruvec, sc); } } /* 沒有回收到足夠頁框,或者需要忽略需要回收的頁框數量,盡可能多的回收頁框,則繼續進行回收 * 當scan_adjusted為真時,掃描到nr[三個類型]數組中的數都為0為止,會忽略是否回收到足夠頁框,即使回收到足夠頁框也繼續進行掃描 * 也就是盡可能的回收頁框,越多越好,alloc_pages()會是這種情況 */ if (nr_reclaimed < nr_to_reclaim || scan_adjusted) continue; /* kswapd和針對某個memcg進行回收的情況中會調用到此,已經回收到了足夠數量的頁框,調用到此是用於判斷是否還要繼續掃描,因為已經回收到了足夠頁框了 */ /* 掃描一遍后,剩余需要掃描的文件頁數量和匿名頁數量 */ nr_file = nr[LRU_INACTIVE_FILE] + nr[LRU_ACTIVE_FILE]; nr_anon = nr[LRU_INACTIVE_ANON] + nr[LRU_ACTIVE_ANON]; /* 已經掃描完成了,退出循環 */ if (!nr_file || !nr_anon) break; /* 下面就是計算再掃描多少頁框,會對nr[]中的數進行相應的減少 * 調用到這里肯定是kswapd進程或者針對memcg的頁框回收,並且已經回收到了足夠的頁框了 * 如果nr[]中還剩余很多數量的頁框沒有掃描,這里就通過計算,減少一些nr[]待掃描的數量 * 設置scan_adjusted,之后把nr[]中剩余的數量掃描完成 */ if (nr_file > nr_anon) { /* 剩余需要掃描的文件頁多於剩余需要掃描的匿名頁時 */ /* 原始的需要掃描匿名頁數量 */ unsigned long scan_target = targets[LRU_INACTIVE_ANON] + targets[LRU_ACTIVE_ANON] + 1; lru = LRU_BASE; /* 計算剩余的需要掃描的匿名頁數量占 */ percentage = nr_anon * 100 / scan_target; } else { /* 剩余需要掃描的文件頁少於剩余需要掃描的匿名頁時 */ unsigned long scan_target = targets[LRU_INACTIVE_FILE] + targets[LRU_ACTIVE_FILE] + 1; lru = LRU_FILE; percentage = nr_file * 100 / scan_target; } nr[lru] = 0; nr[lru + LRU_ACTIVE] = 0; lru = (lru == LRU_FILE) ? LRU_BASE : LRU_FILE; nr_scanned = targets[lru] - nr[lru]; nr[lru] = targets[lru] * (100 - percentage) / 100; nr[lru] -= min(nr[lru], nr_scanned); lru += LRU_ACTIVE; nr_scanned = targets[lru] - nr[lru]; nr[lru] = targets[lru] * (100 - percentage) / 100; nr[lru] -= min(nr[lru], nr_scanned); scan_adjusted = true; } blk_finish_plug(&plug); /* 總共回收的頁框數量 */ sc->nr_reclaimed += nr_reclaimed; /* 非活動匿名頁lru鏈表中頁數量太少 */ if (inactive_anon_is_low(lruvec)) /* 從活動匿名頁lru鏈表中移動一些頁去非活動匿名頁lru鏈表,最多32個 */ shrink_active_list(SWAP_CLUSTER_MAX, lruvec, sc, LRU_ACTIVE_ANON); /* 如果太多臟頁進行回寫了,這里就睡眠100ms */ throttle_vm_writeout(sc->gfp_mask); }
此函數主要是對lru鏈表描述符中的lru鏈表進行處理,我們知道,lru鏈表描述符中一共有5個鏈表:LRU_ACTIVE_ANON,LRU_INACTIVE_ANON,LRU_ACTIVE_FILE,LRU_INACTIVE_FILE,LRU_UNEVICTABLE。對於內存回收來說,它只會處理前面4個lru鏈表,也就是活動匿名頁lru鏈表,非活動匿名頁lru鏈表,活動文件頁lru鏈表,非活動文件頁lru鏈表。此函數主要工作就是:
- 調用get_scan_count()計算每個lru鏈表需要掃描的頁框數量,保存到nr數組中;
- 循環判斷nr數組中是否還有lru鏈表沒有掃描完成
- 以活動匿名頁lru鏈表、非活動匿名頁lru鏈表、活動文件頁lru鏈表、非活動文件頁lru鏈表的順序作為一輪掃描,每次每個lru鏈表掃描32個頁框,並且在nr數組中減去lru鏈表對應掃描的數量;
- 一輪掃描結束后判斷是否回收到了足夠頁框,沒有回收到足夠頁框則跳到 2 繼續循環判斷nr數組;
- 已經回收到了足夠頁框,當nr數組有剩余時,判斷是否要對lru鏈表繼續掃描,如果要繼續掃描,則跳到 2
- 如果非活動匿名頁lru鏈表中頁數量太少,則對活動匿名頁進行一個32個頁框的掃描;
- 如果太多臟頁正在進行回寫,則睡眠100ms
這里需要說明的有兩點:計算每個lru鏈表需要掃描的數量和調整nr數組。
在get_scan_count()函數中會計算每個lru鏈表需要掃描的頁框數量,然后將它們保存到nr數組中,在此,有兩個因素會影響這4個lru鏈表需要掃描的數量,一個是sc->priority(掃描優先級),一個是swapiness。
- sc->priority:影響的是這4個lru鏈表掃描頁框數量的基准值,當sc->priority越小,每個lru鏈表需要掃描的頁框數量就越多,當sc->priority為0時,則本次shrink_lruvec()會對每個lru鏈表都完全掃描一遍。在不同內存回收過程中,使用的sc->priority不同,而sc->priority默認值為12。
- swapiness:影響的是在基准值的基礎上,是否做調整,讓系統更多地去掃描文件頁lru鏈表,或者更多地去掃描匿名頁lru鏈表。當swapiness為100時,掃描文件頁lru鏈表與掃描匿名頁lru鏈表是平衡的,並不傾向與誰,也就是它們需要掃描的頁框就是就是sc->priority決定的基准值,當swapiness為0,時,就不會去掃描匿名頁lru鏈表,只掃描文件頁lru鏈表。
有興趣的可以去看看get_scan_count()函數,這個函數這里就不詳細進行說明了,之后可能會出篇文章對此函數進行詳細說明。
計算好每個lru鏈表需要掃描的頁框數量后,就以活動匿名頁lru鏈表、非活動匿名頁lru鏈表、活動文件頁lru鏈表、非活動文件頁lru鏈表的順序對每個鏈表進行一次最多32個頁框的掃描,然后將對應的nr數組的數值進行減少,當對這4個lru鏈表都進行過一次掃描后,判斷是否回收到了足夠頁框,如果沒有回收到足夠頁框,則繼續掃描,而如果已經回收到了足夠頁框的話,並且nr數組中的數還有剩余的情況下,這里會有兩種可能,一種是直接返回,另一種是繼續掃描,這兩種情況發生的條件如下:
- 回收到足夠頁框后直接返回:快速內存回收、kswapd內存回收中會這樣做,在回收到sc->nr_to_reclaim數量的頁框后直接返回上一級
- 回收到足夠頁框后繼續掃描:直接內存回收時第一次調用shrink_zone()時、kswapd針對某個memcg進行內存回收時會這樣做,即使回收到sc->nr_to_reclaim數量的頁框后,還會繼續掃描,直到nr數組為0具體見后面直接內存回收
當回收到sc>nr_to_reclaim數量的頁框后,還打算繼續掃描的情況,則會繼續掃描這4個lru鏈表,而對於kswapd針對某個memcg進行內存回收的情況會稍微有所不同,雖然這種情況也會繼續掃描,但是它會執行一些代碼去減少一些nr數組中的值,這樣重新掃描時,就會掃描得少一些。
接下來說說對每個lru鏈表的處理,在shrink_lruvec()中已經設計出了每個lru鏈表一次掃描32個頁框,然后調用shrink_list()函數,我們先看看shrink_list():
/* * 對lru鏈表進行處理 * lru: lru鏈表的類型 * nr_to_scan: 需要掃描的頁框數量,此值 <= 32,當鏈表長度不足32時,就為鏈表長度 * lruvec: lru鏈表描述符,與lru參數結合就得出待處理的lru鏈表 * sc: 掃描控制結構 */ static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan, struct lruvec *lruvec, struct scan_control *sc) { /* 如果lru類型是活動lru(包括活動匿名頁lru和活動文件頁lru) */ if (is_active_lru(lru)) { /* 如果此活動lru對應的非活動lru鏈表中維護的頁框數量太少,則會從活動lru鏈表中移動一些到對應非活動lru鏈表中 * 這里需要注意,文件頁和匿名頁的非活動lru鏈表中是否少計算方式是不同的 * 匿名頁的話,有一個經驗值表示大概多少匿名頁保存到非活動匿名頁lru鏈表 * 文件頁的話,大概非活動文件頁數量要大於活動文件頁 * 而如果遇到page->_count == 0的頁,則會將它們釋放到每CPU頁框高速緩存中 */ if (inactive_list_is_low(lruvec, lru)) /* 從活動lru中移動一些頁框到非活動lru中,移動nr_to_scan個,nr_to_scan <= 32,從活動lru鏈表末尾拿出頁框移動到非活動lru鏈表頭 * 只有代碼段的頁最近被訪問了,會將其加入到活動lru鏈表頭部,其他頁即使最近被訪問了,也移動到非活動lru鏈表 */ shrink_active_list(nr_to_scan, lruvec, sc, lru); return 0; } /* 如果lru類似是非活動lru,那么會對此lru類型的lru鏈表中的頁框進行回收 */ return shrink_inactive_list(nr_to_scan, lruvec, sc, lru); }
可以很明顯看到,只有非活動lru鏈表中頁框數量不足時,才會調用shrink_active_list()對活動lru鏈表進行處理,否則並不會進行處理,不過需要注意,即使並不對活動lru鏈表進行處理,在shrink_lruvec()中也會相應減少nr數組中的數值。而怎么判斷非活動lru鏈表保存的頁框數量過少的,具體見linux內存源碼分析 - 內存回收(lru鏈表)。需要注意,此函數調用成功后,返回值 >= 0。大於0說明回收到了頁框,因為內存回收只會對非活動lru鏈表中的頁進行回收,所以只有對非活動lru鏈表進行處理時返回值才會大於0。
對活動lru鏈表處理
我們先看怎么對活動lru鏈表進行處理的,活動lru鏈表包括活動匿名頁lru鏈表以及活動文件頁lru鏈表,這兩個lru鏈表都會調用shrink_active_list()進行處理:
/* * 從lruvec中的lru類型的鏈表中獲取一些頁,並移動到非活動lru鏈表頭部,注意此函數會以lru參數為類型,比如lru參數為LRU_ACTIVE_ANON,那只會處理ANON類型的頁,不會處理FILE類型的頁 * 只有代碼段的頁最近被訪問了,會將其加入到活動lru鏈表頭部,其他頁即使最近被訪問了,也移動到非活動lru鏈表 * 從lruvec中的lru類型的鏈表中拿出一些頁之后,會判斷這些頁的去處,然后將page->_count = 1的頁進行釋放,因為說明此頁只有隔離的時候對其page->_count進行了++,已經沒有進程或模塊引用此頁 * 將其釋放到伙伴系統的每CPU高速緩存中 * nr_to_scan: 默認是32,掃描次數,如果掃描的全是普通頁,那最多掃描32個頁,如果全是大頁,最多掃描(大頁/普通頁)*32個頁 * lruvec: 需要掃描的lru鏈表(里面包括一個zone中所有類型的lru鏈表) * sc: 掃描控制結構 * lru: 需要掃描的類型,是active_file或者active_anon的lru鏈表 */ static void shrink_active_list(unsigned long nr_to_scan, struct lruvec *lruvec, struct scan_control *sc, enum lru_list lru) { unsigned long nr_taken; unsigned long nr_scanned; unsigned long vm_flags; /* 從lru中獲取到的頁存放在這,到最后這里面還有剩余的頁的話,就把它們釋放回伙伴系統 */ LIST_HEAD(l_hold); /* The pages which were snipped off */ /* 移動到活動lru鏈表頭部的頁的鏈表 */ LIST_HEAD(l_active); /* 將要移動到非活動lru鏈表的頁放在這 */ LIST_HEAD(l_inactive); struct page *page; /* lruvec的統計結構 */ struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat; unsigned long nr_rotated = 0; isolate_mode_t isolate_mode = 0; /* lru是否屬於LRU_INACTIVE_FILE或者LRU_ACTIVE_FILE */ int file = is_file_lru(lru); /* lruvec所屬的zone */ struct zone *zone = lruvec_zone(lruvec); /* 將當前CPU的多個pagevec中的頁都放入lru鏈表中 */ lru_add_drain(); /* 從kswapd調用過來的情況下,sc->may_unmap為1 * 直接內存回收的情況,sc->may_unmap為1 * 快速內存回收的情況,sc->may_unmap與zone_reclaim_mode有關 */ if (!sc->may_unmap) isolate_mode |= ISOLATE_UNMAPPED; /* 從kswapd調用過來的情況下,sc->may_writepage與latptop_mode有關 * 直接內存回收的情況,sc->may_writepage與latptop_mode有關 * 快速內存回收的情況,sc->may_writepage與zone_reclaim_mode有關 */ if (!sc->may_writepage) isolate_mode |= ISOLATE_CLEAN; /* 對zone的lru_lock上鎖 */ spin_lock_irq(&zone->lru_lock); /* 從lruvec中lru類型鏈表的尾部拿出一些頁隔離出來,放入到l_hold中,lru類型一般是LRU_ACTIVE_ANON或LRU_ACTIVE_FILE * 也就是從活動的lru鏈表中隔離出一些頁,從活動lru鏈表的尾部依次拿出 * 當sc->may_unmap為0時,則不會將有進程映射的頁隔離出來 * 當sc->may_writepage為0時,則不會將臟頁和正在回寫的頁隔離出來 * 隔離出來的頁會page->_count++ * nr_taken保存拿出的頁的數量 */ nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold, &nr_scanned, sc, isolate_mode, lru); if (global_reclaim(sc)) __mod_zone_page_state(zone, NR_PAGES_SCANNED, nr_scanned); reclaim_stat->recent_scanned[file] += nr_taken; /* 做統計 */ __count_zone_vm_events(PGREFILL, zone, nr_scanned); __mod_zone_page_state(zone, NR_LRU_BASE + lru, -nr_taken); __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, nr_taken); /* 釋放lru鏈表鎖 */ spin_unlock_irq(&zone->lru_lock); /* 將l_hold中的頁一個一個處理 */ while (!list_empty(&l_hold)) { /* 是否需要調度,需要則調度 */ cond_resched(); /* 將頁從l_hold中拿出來 */ page = lru_to_page(&l_hold); list_del(&page->lru); /* 如果頁是unevictable(不可回收)的,則放回到LRU_UNEVICTABLE這個lru鏈表中,這個lru鏈表中的頁不能被交換出去 */ if (unlikely(!page_evictable(page))) { /* 放回到page所應該屬於的lru鏈表中 * 而這里實際上是將頁放到zone的LRU_UNEVICTABLE鏈表中 */ putback_lru_page(page); continue; } /* buffer_heads的數量超過了結點允許的最大值的情況 */ if (unlikely(buffer_heads_over_limit)) { /* 文件頁才有的page才有PAGE_FLAGS_PRIVATE標志 */ if (page_has_private(page) && trylock_page(page)) { if (page_has_private(page)) /* 釋放此文件頁所擁有的buffer_head鏈表中的buffer_head,並且page->_count-- */ try_to_release_page(page, 0); unlock_page(page); } } /* 檢查此頁面最近是否有被訪問過,通過映射了此頁的頁表項的Accessed進行檢查,並且會清除頁表項的Accessed標志 * 如果此頁最近被訪問過,返回的是Accessed為1的數量頁表項數量 */ if (page_referenced(page, 0, sc->target_mem_cgroup, &vm_flags)) { /* 如果是大頁,則記錄一共多少個頁,如果是普通頁,則是1 */ nr_rotated += hpage_nr_pages(page); /* 如果此頁映射的是代碼段,則將其放到l_active鏈表中,此鏈表之后會把頁放入頁對應的活動lru鏈表中 * 可以看出對於代碼段的頁,還是比較傾向於將它們放到活動文件頁lru鏈表的 * 當代碼段沒被訪問過時,也是有可能換到非活動文件頁lru鏈表的 */ if ((vm_flags & VM_EXEC) && page_is_file_cache(page)) { list_add(&page->lru, &l_active); continue; } } /* 將頁放到l_inactive鏈表中 * 只有最近訪問過的代碼段的頁不會被放入,其他即使被訪問過了,也會被放入l_inactive */ ClearPageActive(page); /* we are de-activating */ list_add(&page->lru, &l_inactive); } spin_lock_irq(&zone->lru_lock); /* 記錄的是最近被加入到活動lru鏈表的頁數量,之后這些頁被返回到active鏈表 */ reclaim_stat->recent_rotated[file] += nr_rotated; /* 將l_active鏈表中的頁移動到lruvec->lists[lru]中,這里是將active的頁移動到active的lru鏈表頭部 */ move_active_pages_to_lru(lruvec, &l_active, &l_hold, lru); /* 將l_inactive鏈表中的頁移動到lruvec->lists[lru - LRU_ACITVE]中,這里是將active的頁移動到inactive的lru頭部 */ move_active_pages_to_lru(lruvec, &l_inactive, &l_hold, lru - LRU_ACTIVE); __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, -nr_taken); spin_unlock_irq(&zone->lru_lock); mem_cgroup_uncharge_list(&l_hold); /* 剩下的頁的處理,剩下的都是page->_count為0的頁,作為冷頁放回到伙伴系統的每CPU單頁框高速緩存中 */ free_hot_cold_page_list(&l_hold, true); }
這里面,首先,會將當前CPU所有的lru緩存中的頁全部放到lru鏈表中,其次調用isolate_lru_pages()從lru鏈表的末尾隔離出一些頁來放入到l_hold鏈表中,成功隔離出來的頁的page->_count會進行++。這個函數在內存回收中是一個通用函數,也就是它即可以用來隔離活動lru鏈表的頁,也可以用來隔離非活動lru鏈表的頁,需要注意這個函數依賴於sc->may_writepage和sc->may_unmap,這兩個變量在之前有過說明,也如注釋上所說,當sc->may_writepage為0時,則不會將正在回寫的頁和臟頁隔離出來,當sc->may_unmap為0時,則不會將有進程映射的頁隔離出來,這些頁都會被跳過,這樣一來,在這些情況下,實際隔離的頁就會少於需要掃描的頁。隔離出一些頁后,又會調用page_referenced()函數,此函數通過反向映射,檢查映射了此頁的進程頁表項有多少個的Accessed被置1了,然后清除這些頁表項的Accessed標志,此標志被置1說明這些進程最近訪問過此頁。當最近有進程訪問過此頁,如果此頁是映射了代碼段的頁,就把此頁加入到l_active鏈表,其他頁則清除PG_active標志,通過page->lru這個鏈表結點加入到l_inactive鏈表。也就是隔離出來的頁,只有代碼段最近被訪問過了,才會留在活動lru鏈表中,其余的頁,都必須要通過page->lru這個鏈表結點移動到非活動lru鏈表頭中。不過因為代碼段的頁是屬於文件頁lru鏈表,也就是對於活動匿名頁lru鏈表中隔離出來的頁,所有都放到非活動匿名頁lru鏈表頭部,而對於活動文件頁lru鏈表中隔離出來的頁,除了最近被訪問過的代碼段的頁,其余頁都移動到非活動文件頁lru鏈表頭部。之后調用move_active_pages_to_lru()函數,將l_active中的頁加入到活動lru鏈表頭部,將l_inactive中的頁加入到非活動lru鏈表尾部,並且會對成功加入的頁的page->_count進行--,這樣與成功隔離時正好進行了一加一減的操作。在將活動頁移動到非活動lru鏈表時,可能會掃描到一種頁,它們的page->_count為0,也就是已經沒有任何模塊和進程對其進行引用了,這種頁就可以直接釋放了。所以看到shrink_active_list()函數最后將這些類型的頁進行釋放。關於isolate_lru_pages()、page_referenced()和move_active_pages_to_lru()函數,以后的文章再分析,這篇文章放不下了。
shrink_active_list()調用完成后,所有隔離出來的頁都已經被放入到相應的lru鏈表中了。注意,這里在移動頁框時並不會使用lru緩存,之前關於lru鏈表分析的文章也說過,在內存回收過程中,只有將頁加入到LRU_UNEVICTABLE鏈表中時需要用到lru緩存,而對於頁在相同類型的lru鏈表間移動時,是不會使用到lru緩存的。shrink_active_list()函數總結要點有五:
- 將本cpu的lru緩存全部清空,將lru緩存的頁放到lru鏈表中,而其他CPU的則不處理
- 根據sc->may_writepage與sc->may_unmap選擇要隔離的頁
- 如果結點buffer_heads數量超過限制值,則會嘗試對掃描到的文件頁進行buffer_heads的釋放,進行釋放后的文件頁的page->_count--
- 將所有映射了隔離頁的頁表項Accessed都跟清0
- 將最近被訪問過的代碼段的頁移動到活動lru鏈表頭,其余頁都移動到非活動lru鏈表頭
- 將page->_count == 0的頁進行釋放。
對非活動lru鏈表進行處理
接下來我們看看對於非活動lru鏈表的分析,非活動lru鏈表包括非活動匿名頁lru鏈表和非活動文件頁lru鏈表,它們都會調用shrink_inactive_list(),此函數就要比活動lru鏈表的處理函數shrink_active_list()復雜得多,如下:
/* 對lruvec這個lru鏈表描述符中的lru類型的lru鏈表進行內存回收,這個lru類型一定是LRU_INACTIVE_ANON或者LRU_INACTIVE_FILE類型 * nr_to_scan: 最多掃描多少個頁框 * lruvec: lru鏈表描述符,里面有5個lru鏈表 * sc: 掃描控制結構 * lru: 需要掃描的lru鏈表 * 返回本次回收的頁框數量 */ static noinline_for_stack unsigned long shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec, struct scan_control *sc, enum lru_list lru) { LIST_HEAD(page_list); unsigned long nr_scanned; unsigned long nr_reclaimed = 0; unsigned long nr_taken; unsigned long nr_dirty = 0; unsigned long nr_congested = 0; unsigned long nr_unqueued_dirty = 0; unsigned long nr_writeback = 0; unsigned long nr_immediate = 0; isolate_mode_t isolate_mode = 0; /* 此非活動lru是否為非活動文件頁lru */ int file = is_file_lru(lru); /* lru所屬的zone */ struct zone *zone = lruvec_zone(lruvec); struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat; /* 如果隔離的頁數量多於非活動的頁數量,則是隔離太多頁了,個人猜測這里是控制並發 * 當zone的NR_INACTIVE_FILE/ANON < NR_ISOLATED_ANON時,有一種情況是其他CPU也在對此zone進行內存回收,所以NR_ISOLATED_ANON比較高 */ while (unlikely(too_many_isolated(zone, file, sc))) { /* 這里會休眠等待100ms,如果是並發進行內存回收,另一個CPU可能也在執行內存回收 */ congestion_wait(BLK_RW_ASYNC, HZ/10); /* We are about to die and free our memory. Return now. */ /* 當前進程被其他進程kill了,這里接受到了kill信號 */ if (fatal_signal_pending(current)) return SWAP_CLUSTER_MAX; } /* 將當前cpu的pagevec中的頁放入到lru鏈表中 * 而其他CPU的pagevec中的頁則不會放回到lru鏈表中 * 這樣做似乎是因為效率問題 */ lru_add_drain(); if (!sc->may_unmap) isolate_mode |= ISOLATE_UNMAPPED; if (!sc->may_writepage) isolate_mode |= ISOLATE_CLEAN; /* 對lru鏈表上鎖 */ spin_lock_irq(&zone->lru_lock); /* 從lruvec這個lru鏈表描述符的lru類型的lru鏈表中隔離最多nr_to_scan個頁出來,隔離時是從lru鏈表尾部開始拿,然后放到page_list * 返回隔離了多少個此非活動lru鏈表的頁框 */ nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list, &nr_scanned, sc, isolate_mode, lru); /* 更新zone中對應lru中頁的數量 */ __mod_zone_page_state(zone, NR_LRU_BASE + lru, -nr_taken); /* 此zone對應隔離的ANON/FILE頁框數量 */ __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, nr_taken); /* 如果是針對整個zone的內存回收,而不是某個memcg的內存回收的情況 */ if (global_reclaim(sc)) { /* 統計zone中掃描的頁框總數 */ __mod_zone_page_state(zone, NR_PAGES_SCANNED, nr_scanned); /* 如果是在kswapd內核線程中調用到此的,則掃描的頁框數量統計到zone的PGSCAN_KSWAPD */ if (current_is_kswapd()) __count_zone_vm_events(PGSCAN_KSWAPD, zone, nr_scanned); else /* 否則掃描的數量統計到zone的PGSCAN_DIRECT */ __count_zone_vm_events(PGSCAN_DIRECT, zone, nr_scanned); } /* 釋放lru鎖 */ spin_unlock_irq(&zone->lru_lock); /* 隔離出來的頁數量為0 */ if (nr_taken == 0) return 0; /* 上面的代碼已經將非活動lru鏈表中的一些頁拿出來放到page_list中了,這里是對page_list中的頁進行內存回收 * 此函數的步驟: * 1.此頁是否在進行回寫(兩種情況會導致回寫,之前進行內存回收時導致此頁進行了回寫;此頁為臟頁,系統自動將其回寫),這種情況同步回收和異步回收有不同的處理 * 2.此次回收時非強制進行回收,那要先判斷此頁能不能進行回收 * 如果是匿名頁,只要最近此頁被進程訪問過,則將此頁移動到活動lru鏈表頭部,否則回收 * 如果是映射可執行文件的文件頁,只要最近被進程訪問過,就放到活動lru鏈表,否則回收 * 如果是其他的文件頁,如果最近被多個進程訪問過,移動到活動lru鏈表,如果只被1個進程訪問過,但是PG_referenced置位了,也放入活動lru鏈表,其他情況回收 * 3.如果遍歷到的page為匿名頁,但是又不處於swapcache中,這里會嘗試將其加入到swapcache中並把頁標記為臟頁,這個swapcache作為swap緩沖區,是一個address_space * 4.對所有映射了此頁的進程的頁表進行此頁的unmap操作 * 5.如果頁為臟頁,則進行回寫,分同步和異步,同步情況是回寫完成才返回,異步情況是加入塊層的寫入隊列,標記頁的PG_writeback表示正在回寫就返回,此頁將會被放到非活動lru鏈表頭部 * 6.檢查頁的PG_writeback標志,如果此標志位0,則說明此頁的回寫完成(兩種情況: 1.同步回收 2.之前異步回收對此頁進行的回寫已完成),則從此頁對應的address_space中的基樹移除此頁的結點,加入到free_pages鏈表 * 對於PG_writeback標志位1的,將其重新加入到page_list鏈表,這個鏈表之后會將里面的頁放回到非活動lru鏈表末尾,下次進行回收時,如果頁回寫完成了就會被釋放 * 7.對free_pages鏈表的頁釋放 * * page_list中返回時有可能還有頁,這些頁是要放到非活動lru鏈表末尾的頁,而這些頁當中,有些頁是正在進行回收的回寫,當這些回寫完成后,系統再次進行內存回收時,這些頁就會被釋放 * 而有一些頁是不滿足回收情況的頁 * nr_dirty: page_list中臟頁的數量 * nr_unqueued_dirty: page_list中臟頁但並沒有正在回寫的頁的數量 * nr_congested: page_list中正在進行回寫並且設備正忙的頁的數量(這些頁可能回寫很慢) * nr_writeback: page_list中正在進行回寫但不是在回收的頁框數量 * nr_immediate: page_list中正在進行回寫的回收頁框數量 * 返回本次回收的頁框數量 */ nr_reclaimed = shrink_page_list(&page_list, zone, sc, TTU_UNMAP, &nr_dirty, &nr_unqueued_dirty, &nr_congested, &nr_writeback, &nr_immediate, false); /* 對lru上鎖 */ spin_lock_irq(&zone->lru_lock); /* 更新reclaim_stat中的recent_scanned */ reclaim_stat->recent_scanned[file] += nr_taken; /* 如果是針對整個zone,而不是某個memcg的情況 */ if (global_reclaim(sc)) { /* 如果是在kswakpd內核線程中 */ if (current_is_kswapd()) /* 更新到zone的PGSTEAL_KSWAPD */ __count_zone_vm_events(PGSTEAL_KSWAPD, zone, nr_reclaimed); else /* 不是在kswapd內核線程中,更新到PGSTEAL_DIRECT */ __count_zone_vm_events(PGSTEAL_DIRECT, zone, nr_reclaimed); } /* * 將page_list中剩余的頁放回它對應的lru鏈表中,這里的頁有三種情況: * 1.最近被訪問了,放到活動lru鏈表頭部 * 2.此頁需要鎖在內存中,加入到unevictablelru鏈表 * 3.此頁為非活動頁,移動到非活動lru鏈表頭部 * 當頁正在進行回寫回收,當回寫完成后,通過判斷頁的PG_reclaim可知此頁正在回收,會把頁移動到非活動lru鏈表末尾,具體見end_page_writeback()函數 * 加入lru的頁page->_count-- * 因為隔離出來時page->_count++,而在lru中是不需要對page->_count++的 */ putback_inactive_pages(lruvec, &page_list); /* 更新此zone對應隔離的ANON/FILE頁框數量,這里減掉了nr_taken,與此函數之前相對應 */ __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, -nr_taken); spin_unlock_irq(&zone->lru_lock); mem_cgroup_uncharge_list(&page_list); /* 釋放page_list中剩余的頁到伙伴系統中的每CPU頁高速緩存中,以冷頁處理 * 這里剩余的就是page->_count == 0的頁 */ free_hot_cold_page_list(&page_list, true); /* 隔離出來的頁都在進行回寫(但不是回收造成的回寫) */ if (nr_writeback && nr_writeback == nr_taken) /* 標記ZONE的ZONE_WRITEBACK,標記此zone許多頁在回寫 */ set_bit(ZONE_WRITEBACK, &zone->flags); /* 本次內存回收是針對整個zone的,這里面主要對zone的flags做一些標記 */ if (global_reclaim(sc)) { if (nr_dirty && nr_dirty == nr_congested) set_bit(ZONE_CONGESTED, &zone->flags); if (nr_unqueued_dirty == nr_taken) set_bit(ZONE_DIRTY, &zone->flags); /* 有一些頁是因為回收導致它們在回寫,則等待一下設備 */ if (nr_immediate && current_may_throttle()) congestion_wait(BLK_RW_ASYNC, HZ/10); } /* 非kswapd的情況下,如果現在設備回寫壓力較大 */ if (!sc->hibernation_mode && !current_is_kswapd() && current_may_throttle()) /* 等待一下設備 */ wait_iff_congested(zone, BLK_RW_ASYNC, HZ/10); trace_mm_vmscan_lru_shrink_inactive(zone->zone_pgdat->node_id, zone_idx(zone), nr_scanned, nr_reclaimed, sc->priority, trace_shrink_flags(file)); return nr_reclaimed; }
此函數與shrink_inactive_list()函數流程差不多,首先要求當前CPU的所有lru緩存將頁放入到lru鏈表中,然后通過isolate_lru_pages()函數從活動lru鏈表末尾掃描出符合要求的頁,這些頁會通過page->lru加入到page_list鏈表中,然后調用shrink_page_list()對這個page_list鏈表中的頁進行回收處理,之后將page_list鏈表中剩余的頁放回到它們應該放入到鏈表中。
當把頁放回對應的地方后,則將page->_count == 0的頁進行釋放,這里實際上回收的是在shrink_page_list()中沒有進行回收,但是執行完shrink_page_list()后,正巧所有映射了此頁的進程都取消了映射,並且此頁也不為臟,這就以直接回收了。之后會根據狀態標記一些zone的狀態。主要進行具體的回收工作函數還是shrink_page_list():
/* 在page_list中的頁都是非活動lru鏈表的,並且都是同一類型的頁(ANON/FILE) * 注意page_list中的頁還沒有被標注進行回收的標志(PG_reclaim),並且如果為臟頁的頁(PG_dirty被設置),那么只有在kswapd調用到此會進行writeback(回寫到磁盤)操作 * 到達這里之前,所有pagevec中的頁都放回了lru鏈表中 * force_reclaim: 表示是否強制進行回收,強制進行回收則不會判斷此頁是否應該回收,強制回收的意思是即使頁最近被訪問過了,也進行回收,除非頁被mlock在內存中,或者unmap失敗 * ret_nr_dirty: 臟頁數量(包括正在回寫和沒有回寫的臟頁) * ret_nr_unqueued_dirty: 是臟頁但沒有進行回寫的頁 * ret_nr_congested: 正在進行回寫,但是設備正忙 * ret_nr_writeback: 正在進行回寫但不是在回收的頁框數量 * ret_nr_immediate: 正在進行回寫的回收頁框數量 */ static unsigned long shrink_page_list(struct list_head *page_list, struct zone *zone, struct scan_control *sc, enum ttu_flags ttu_flags, unsigned long *ret_nr_dirty, unsigned long *ret_nr_unqueued_dirty, unsigned long *ret_nr_congested, unsigned long *ret_nr_writeback, unsigned long *ret_nr_immediate, bool force_reclaim) { /* 初始化兩個鏈表頭 */ LIST_HEAD(ret_pages); /* 這個鏈表保存本次回收就可以立即進行釋放的頁 */ LIST_HEAD(free_pages); int pgactivate = 0; unsigned long nr_unqueued_dirty = 0; unsigned long nr_dirty = 0; unsigned long nr_congested = 0; unsigned long nr_reclaimed = 0; unsigned long nr_writeback = 0; unsigned long nr_immediate = 0; /* 檢查是否需要調度,需要則調度 */ cond_resched(); /* 將page_list中的頁一個一個釋放 */ while (!list_empty(page_list)) { struct address_space *mapping; struct page *page; int may_enter_fs; enum page_references references = PAGEREF_RECLAIM_CLEAN; bool dirty, writeback; /* 檢查是否需要調度,需要則調度 */ cond_resched(); /* 從page_list末尾拿出一個頁 */ page = lru_to_page(page_list); /* 將此頁從page_list中刪除 */ list_del(&page->lru); /* 嘗試對此頁上鎖,如果無法上鎖,說明此頁正在被其他路徑控制,跳轉到keep * 對頁上鎖后,所有訪問此頁的進程都會加入到zone->wait_table[hash_ptr(page, zone->wait_table_bits)] */ if (!trylock_page(page)) goto keep; /* 在page_list的頁一定都是非活動的 */ VM_BUG_ON_PAGE(PageActive(page), page); /* 頁所屬的zone也要與傳入的zone一致 */ VM_BUG_ON_PAGE(page_zone(page) != zone, page); /* 掃描的頁數量++ */ sc->nr_scanned++; /* 如果此頁被鎖在內存中,則跳轉到cull_mlocked */ if (unlikely(!page_evictable(page))) goto cull_mlocked; /* 如果掃描控制結構中標識不允許進行unmap操作,並且此頁有被映射到頁表中,跳轉到keep_locked */ if (!sc->may_unmap && page_mapped(page)) goto keep_locked; /* Double the slab pressure for mapped and swapcache pages */ /* 對於處於swapcache中或者有進程映射了的頁,對sc->nr_scanned再進行一次++ * swapcache用於在頁換出到swap時,頁會先跑到swapcache中,當此頁完全寫入swap分區后,在沒有進程對此頁進行訪問時,swapcache才會釋放掉此頁 * 這樣做是為了讓sc->nr_scanned增加得更快? */ if (page_mapped(page) || PageSwapCache(page)) sc->nr_scanned++; /* 本次回收是否允許執行IO操作 */ may_enter_fs = (sc->gfp_mask & __GFP_FS) || (PageSwapCache(page) && (sc->gfp_mask & __GFP_IO)); /* 檢查是否是臟頁還有此頁是否正在回寫到磁盤 * 這里面主要判斷頁描述符的PG_dirty和PG_writeback兩個標志 * 匿名頁當加入swapcache后,就會被標記PG_dirty * 如果文件所屬文件系統有特定is_dirty_writeback操作,則執行文件系統特定的is_dirty_writeback操作 */ page_check_dirty_writeback(page, &dirty, &writeback); /* 如果是臟頁或者正在回寫的頁,臟頁數量++ */ if (dirty || writeback) nr_dirty++; /* 是臟頁但並沒有正在回寫,則增加沒有進行回寫的臟頁計數 */ if (dirty && !writeback) nr_unqueued_dirty++; /* 獲取此頁對應的address_space,如果此頁是匿名頁,則為NULL */ mapping = page_mapping(page); /* 如果此頁映射的文件所在的磁盤設備等待隊列中有數據(正在進行IO處理)或者此頁已經在進行回寫回收 */ if ((mapping && bdi_write_congested(mapping->backing_dev_info)) || (writeback && PageReclaim(page))) /* 可能比較晚才能進行阻塞回寫的頁的數量 * 因為磁盤設備現在繁忙,隊列中有太多需要寫入的數據 */ nr_congested++; /* 此頁正在進行回寫到磁盤,對於正在回寫到磁盤的頁,是無法進行回收的,除非等待此頁回寫完成 * 此頁正在進行回寫有兩種情況: * 1.此頁是正常的進行回寫(臟太久了) * 2.此頁是剛不久前進行內存回收時,導致此頁進行回寫的 */ if (PageWriteback(page)) { /* Case 1 above */ /* 下面的判斷都是基於此頁正在進行回寫到磁盤為前提 */ /* 當前處於kswapd內核進程,並且此頁正在進行回收(可能在等待IO),然后zone也表明了很多頁正在進行回寫 * 說明此頁是已經在回寫到磁盤,並且也正在進行回收的,本次回收不需要對此頁進行回收 */ if (current_is_kswapd() && PageReclaim(page) && test_bit(ZONE_WRITEBACK, &zone->flags)) { /* 增加nr_immediate計數,此計數說明此頁准備就可以回收了 */ nr_immediate++; /* 跳轉到keep_locked */ goto keep_locked; /* Case 2 above */ /* 此頁正在進行正常的回寫(不是因為要回收此頁才進行的回寫) * 兩種情況會進入這里: * 1.本次是針對整個zone進行內存回收的 * 2.本次回收不允許進行IO操作 * 那么就標記這個頁要回收,本次回收不對此頁進行回收,當此頁回寫完成后,會判斷這個PG_reclaim標記,如果置位了,把此頁放到非活動lru鏈表末尾 * 快速回收會進入這種情況 */ } else if (global_reclaim(sc) || !PageReclaim(page) || !(sc->gfp_mask & __GFP_IO)) { /* 設置此頁正在進行回收,因為此頁正在進行回寫,那設置稱為進行回收后,回寫完成后此頁會被放到非活動lru鏈表末尾 */ SetPageReclaim(page); /* 增加需要回寫計數器 */ nr_writeback++; goto keep_locked; /* Case 3 above */ } else { /* 等待此頁回寫完成,回寫完成后,嘗試對此頁進行回收,應該只有針對某個memcg進行回收時才會進入這 */ wait_on_page_writeback(page); } } /* * 此次回收時非強制進行回收,那要先判斷此頁需不需要移動到活動lru鏈表 * 如果是匿名頁,只要最近此頁被進程訪問過,則將此頁移動到活動lru鏈表頭部,否則回收 * 如果是映射可執行文件的文件頁,只要最近被進程訪問過,就放到活動lru鏈表,否則回收 * 如果是其他的文件頁,如果最近被多個進程訪問過,移動到活動lru鏈表,如果只被1個進程訪問過,但是PG_referenced置位了,也放入活動lru鏈表,其他情況回收 */ if (!force_reclaim) references = page_check_references(page, sc); /* 當此次回收時非強制進行回收時 */ switch (references) { /* 將頁放到活動lru鏈表中 */ case PAGEREF_ACTIVATE: goto activate_locked; /* 頁繼續保存在非活動lru鏈表中 */ case PAGEREF_KEEP: goto keep_locked; /* 這兩個在下面的代碼都會嘗試回收此頁 * 注意頁所屬的vma標記了VM_LOCKED時也會是PAGEREF_RECLAIM,因為后面會要把此頁放到lru_unevictable_page鏈表 */ case PAGEREF_RECLAIM: case PAGEREF_RECLAIM_CLEAN: ; /* try to reclaim the page below */ } /* page為匿名頁,但是又不處於swapcache中,這里會嘗試將其加入到swapcache中,這個swapcache作為swap緩沖區 * 當頁被換出或換入時,會先加入到swapcache,當完全換出或者完全換入時,才會從swapcache中移除 * 有了此swapcache,當一個頁進行換出時,一個進程訪問此頁,可以直接從swapcache中獲取此頁的映射,然后swapcache終止此頁的換出操作,這樣就不用等頁要完全換出后,再重新換回來 */ if (PageAnon(page) && !PageSwapCache(page)) { /* 如果本次回收禁止io操作,則跳轉到keep_locked,讓此匿名頁繼續在非活動lru鏈表中 */ if (!(sc->gfp_mask & __GFP_IO)) goto keep_locked; /* 將頁page加入到swap_cache,然后這個頁被視為文件頁,起始就是將頁描述符信息保存到以swap頁槽偏移量為索引的結點 * 設置頁描述符的private = swap頁槽偏移量生成的頁表項swp_entry_t,因為后面會設置所有映射了此頁的頁表項為此swp_entry_t * 設置頁的PG_swapcache標志,表明此頁在swapcache中,正在被換出 * 標記頁page為臟頁(PG_dirty),后面就會被換出 */ /* 執行成功后,頁屬於swapcache,並且此頁的page->_count會++,但是由於引用此頁的進程頁表沒有設置,進程還是可以正常訪問這個頁 */ if (!add_to_swap(page, page_list)) /* 失敗,將此頁加入到活動lru鏈表中 */ goto activate_locked; /* 設置可能會用到文件系統相關的操作 */ may_enter_fs = 1; /* Adding to swap updated mapping */ /* 獲取此匿名頁所在的swapcache的address_space,這個是根據page->private中保存的swp_entry_t獲得 */ mapping = page_mapping(page); } /* 這里是要對所有映射了此page的頁表進行設置 * 匿名頁會把對應的頁表項設置為之前獲取的swp_entry_t */ if (page_mapped(page) && mapping) { /* 對所有映射了此頁的進程的頁表進行此頁的unmap操作 * ttu_flags基本都有TTU_UNMAP標志 * 如果是匿名頁,那么page->private中是一個帶有swap頁槽偏移量的swp_entry_t,此后這個swp_entry_t可以轉為頁表項 * 執行完此后,匿名頁在swapcache中,而對於引用了此頁的進程而言,此頁已經在swap中 * 但是當此匿名頁還沒有完全寫到swap中時,如果此時有進程訪問此頁,會將此頁映射到此進程頁表中,並取消此頁放入swap中的操作,放回匿名頁的lru鏈表(在缺頁中斷中完成) * 而對於文件頁,只需要清空映射了此頁的進程頁表的頁表項,不需要設置新的頁表項 * 每一個進程unmap此頁,此頁的page->_count-- * 如果反向映射過程中page->_count == 0,則釋放此頁 */ switch (try_to_unmap(page, ttu_flags)) { case SWAP_FAIL: goto activate_locked; case SWAP_AGAIN: goto keep_locked; case SWAP_MLOCK: goto cull_mlocked; case SWAP_SUCCESS: ; /* try to free the page below */ } } /* 如果頁為臟頁,有兩種頁 * 一種是當匿名頁加入到swapcache中時,就被標記為了臟頁 * 一種是臟的文件頁 */ if (PageDirty(page)) { /* 只有kswapd內核線程能夠進行文件頁的回寫操作(kswapd中不會造成棧溢出?),但是只有當zone中有很多臟頁時,kswapd也才能進行臟文件頁的回寫 * 此標記說明zone的臟頁很多,在回收時隔離出來的頁都是沒有進行回寫的臟頁時設置 * 也就是此zone臟頁不夠多,kswapd不用進行回寫操作 * 當短時間內多次對此zone執行內存回收后,這個ZONE_DIRTY就會被設置,這樣做的理由是: 優先回收匿名頁和干凈的文件頁,說不定回收完這些zone中空閑內存就足夠了,不需要再進行內存回收了 * 而對於匿名頁,無論是否是kswapd都可以進行回寫 */ if (page_is_file_cache(page) && (!current_is_kswapd() || !test_bit(ZONE_DIRTY, &zone->flags))) { /* 增加優先回收頁的數量 */ inc_zone_page_state(page, NR_VMSCAN_IMMEDIATE); /* 設置此頁需要回收,這樣當此頁回寫完成后,就會被放入到非活動lru鏈表尾部 * 不過可惜這里只能等kswapd內核線程對此頁進行回寫,要么就等系統到期后自動將此頁進行回寫,非kswapd線程都不能對文件頁進行回寫 */ SetPageReclaim(page); /* 讓頁移動到非活動lru鏈表頭部,如上所說,當回寫完成后,頁會被移動到非活動lru鏈表尾部,而內存回收是從非活動lru鏈表尾部拿頁出來回收的 */ goto keep_locked; } /* 當zone沒有標記ZONE_DIRTY時,kswapd內核線程則會執行到這里 */ /* 當page_check_references()獲取頁的狀態是PAGEREF_RECLAIM_CLEAN,則跳到keep_locked * 頁最近沒被進程訪問過,但此頁的PG_referenced被置位 */ if (references == PAGEREF_RECLAIM_CLEAN) goto keep_locked; /* 回收不允許執行文件系統相關操作,則讓頁移動到非活動lru鏈表頭部 */ if (!may_enter_fs) goto keep_locked; /* 回收不允許進行回寫,則讓頁移動到非活動lru鏈表頭部 */ if (!sc->may_writepage) goto keep_locked; /* Page is dirty, try to write it out here */ /* 將頁進行回寫到磁盤,這里只是將頁加入到塊層,調用結束並不是代表此頁已經回寫完成 * 主要調用page->mapping->a_ops->writepage進行回寫,對於匿名頁,也是swapcache的address_space->a_ops->writepage * 頁被加入到塊層回寫隊列后,會置位頁的PG_writeback,回寫完成后清除PG_writeback位,所以在同步模式回寫下,結束后PG_writeback位是0的,而異步模式下,PG_writeback很可能為1 * 此函數中會清除頁的PG_dirty標志 * 會標記頁的PG_reclaim * 成功將頁加入到塊層后,頁的PG_lock位會清空 * 也就是在一個頁成功進入到回收導致的回寫過程中,它的PG_writeback和PG_reclaim標志會置位,而它的PG_dirty和PG_lock標志會清除 * 而此頁成功回寫后,它的PG_writeback和PG_reclaim位都會被清除 */ switch (pageout(page, mapping, sc)) { case PAGE_KEEP: /* 頁會被移動到非活動lru鏈表頭部 */ goto keep_locked; case PAGE_ACTIVATE: /* 頁會被移動到活動lru鏈表 */ goto activate_locked; case PAGE_SUCCESS: /* 到這里,頁的鎖已經被釋放,也就是PG_lock被清空 * 對於同步回寫(一些特殊文件系統只支持同步回寫),這里的PG_writeback、PG_reclaim、PG_dirty、PG_lock標志都是清0的 * 對於異步回寫,PG_dirty、PG_lock標志都是為0,PG_writeback、PG_reclaim可能為1可能為0(回寫完成為0,否則為1) */ /* 如果PG_writeback被置位,說明此頁正在進行回寫,這種情況是異步才會發生 */ if (PageWriteback(page)) goto keep; /* 此頁為臟頁,這種情況發生在此頁最近又被寫入了,讓其保持在非活動lru鏈表中 * 還有一種情況,就是匿名頁加入到swapcache前,已經沒有進程映射此匿名頁了,而加入swapcache時不會判斷 * 但是當對此匿名頁進行回寫時,會判斷此頁加入swapcache前是否有進程映射了,如果沒有,此頁可以直接釋放,不需要寫入磁盤 * 所以在此匿名頁回寫過程中,就會將此頁從swap分區的address_space中的基樹拿出來,然后標記為臟頁,到這里就會進行判斷臟頁,之后會釋放掉此頁 */ if (PageDirty(page)) goto keep; /* 嘗試上鎖,因為在pageout中會釋放page的鎖,主要是PG_lock標志 */ if (!trylock_page(page)) goto keep; if (PageDirty(page) || PageWriteback(page)) goto keep_locked; /* 獲取page->mapping */ mapping = page_mapping(page); /* 這個頁不是臟頁,不需要回寫,這種情況只發生在文件頁,匿名頁當加入到swapcache中時就被設置為臟頁 */ case PAGE_CLEAN: ; /* try to free the page below */ } } /* 這里的情況只有頁已經完成回寫后才會到達這里,比如同步回寫時(pageout在頁回寫完成后才返回),異步回寫時,在運行到此之前已經把頁回寫到磁盤 * 沒有完成回寫的頁不會到這里,在pageout()后就跳到keep了 */ /* 通過頁描述符的PAGE_FLAGS_PRIVATE標記判斷是否有buffer_head,這個只有文件頁有 * 這里不會通過page->private判斷,原因是,當匿名頁加入到swapcache時,也會使用page->private,而不會標記PAGE_FLAGS_PRIVATE * 只有文件頁會使用這個PAGE_FLAGS_PRIVATE,這個標記說明此文件頁的page->private指向struct buffer_head鏈表頭 */ if (page_has_private(page)) { /* 因為頁已經回寫完成或者是干凈不需要回寫的頁,釋放page->private指向struct buffer_head鏈表,釋放后page->private = NULL * 釋放時必須要保證此頁的PG_writeback位為0,也就是此頁已經回寫到磁盤中了 */ if (!try_to_release_page(page, sc->gfp_mask)) /* 釋放失敗,把此頁移動到活動lru鏈表 */ goto activate_locked; /* 一些特殊的頁的mapping為空,比如一些日志的緩沖區,對於這些頁如果引用計數為1則進行處理 */ if (!mapping && page_count(page) == 1) { /* 對此頁解鎖,清除PG_lock */ unlock_page(page); /* 對page->_count--,並判斷是否為0,如果為0則釋放掉此頁 */ if (put_page_testzero(page)) goto free_it; else { /* 這里不太明白,大概意思是這些頁馬上就會在其他地方被釋放了,所以算作回收頁 */ nr_reclaimed++; continue; } } } /* * 經過上面的步驟,在沒有進程再對此頁進行訪問的前提下,page->_count應該為2 * 表示只有將此頁隔離出lru的鏈表和加入address_space的基樹中對此頁進行了引用,已經沒有其他地方對此頁進行引用, * 然后將此頁從address_space的基數中移除,然后page->_count - 2,這個頁現在就只剩等待着被釋放掉了 * 如果是匿名頁,則是對應的swapcache的address_space的基樹 * 如果是文件頁,則是對應文件的address_space的基樹 * 當page->_count為2時,才會將此頁從address_space的基數中移除,然后再page->_count - 2 * 相反,如果此page->_count不為2,說明unmap后又有進程訪問了此頁,就不對此頁進行釋放了 * 同時,這里對於臟頁也不能夠進行釋放,想象一下,如果一個進程訪問了此頁,寫了數據,又unmap此頁,那么此頁的page->_count為2,同樣也可以釋放掉,但是寫入的數據就丟失了 * 成功返回1,失敗返回0 */ if (!mapping || !__remove_mapping(mapping, page, true)) goto keep_locked; /* 釋放page鎖 */ __clear_page_locked(page); free_it: /* page->_count為0才會到這 */ /* 此頁可以馬上回收,會把它加入到free_pages鏈表 * 到這里的頁有三種情況,本次進行同步回寫的頁,干凈的不需要回寫的頁,之前異步回收時完成異步回寫的頁 * 之前回收進行異步回寫的頁,不會立即釋放,因為上次回收時,對這些頁進行的工作有: * 匿名頁: 加入swapcache,反向映射修改了映射了此頁的進程頁表項,將此匿名頁回寫到磁盤,將此頁保存到非活動匿名頁lru鏈表尾部 * 文件頁: 反向映射修改了映射了此頁的進程頁表項,將此文件頁回寫到磁盤,將此頁保存到非活動文件頁lru鏈表尾部 * 也就是異步情況這兩種頁都沒有進行實際的回收,而在這些頁回寫完成后,再進行回收時,這兩種頁的流程都會到這里進行回收 * 也就是本次能夠真正回收到的頁,可能是之前進行回收時已經處理得差不多並回寫完成的頁 */ /* 回收頁數量++ */ nr_reclaimed++; /* 加入到free_pages鏈表 */ list_add(&page->lru, &free_pages); /* 繼續遍歷頁 */ continue; cull_mlocked: /* 當前頁被mlock所在內存中的情況 */ /* 此頁為匿名頁並且已經放入了swapcache中了 */ if (PageSwapCache(page)) /* 從swapcache中釋放本頁在基樹的結點,會page->_count-- */ try_to_free_swap(page); unlock_page(page); /* 把此頁放回到lru鏈表中,因為此頁已經被隔離出來了 * 加入可回收lru鏈表后page->_count++,但同時也會釋放隔離的page->_count-- * 加入unevictablelru不會進行page->_count++ */ putback_lru_page(page); continue; activate_locked: /* Not a candidate for swapping, so reclaim swap space. */ /* 這種是持有頁鎖(PG_lock),並且需要把頁移動到活動lru鏈表中的情況 */ /* 如果此頁為匿名頁並且放入了swapcache中,並且swap使用率已經超過了50% */ if (PageSwapCache(page) && vm_swap_full()) /* 將此頁從swapcache的基樹中拿出來 */ try_to_free_swap(page); VM_BUG_ON_PAGE(PageActive(page), page) /* 設置此頁為活動頁 */; SetPageActive(page); /* 需要放回到活動lru鏈表的頁數量 */ pgactivate++; keep_locked: /* 希望頁保持在原來的lru鏈表中,並且持有頁鎖的情況 */ /* 釋放頁鎖(PG_lock) */ unlock_page(page); keep: /* 希望頁保持在原來的lru鏈表中的情況 */ /* 把頁加入到ret_pages鏈表中 */ list_add(&page->lru, &ret_pages); VM_BUG_ON_PAGE(PageLRU(page) || PageUnevictable(page), page); } mem_cgroup_uncharge_list(&free_pages); /* 將free_pages中的頁釋放 */ free_hot_cold_page_list(&free_pages, true); /* 將ret_pages鏈表加入到page_list中 */ list_splice(&ret_pages, page_list); count_vm_events(PGACTIVATE, pgactivate); *ret_nr_dirty += nr_dirty; *ret_nr_congested += nr_congested; *ret_nr_unqueued_dirty += nr_unqueued_dirty; *ret_nr_writeback += nr_writeback; *ret_nr_immediate += nr_immediate; return nr_reclaimed; }
shrink_page_list(),它的工作就是對page_list鏈表中的每個頁嘗試進行回收操作了,但是進行回收操作,並不等於此頁就可以立即進行回收,因為如果為臟頁的話,回寫到磁盤的操作是異步的,而這些頁將在回寫完成后進行回收,具體怎么做的,我們慢慢道來。首先,在shrink_page_list()中會遍歷page_list鏈表中的每一個頁,然后對每個遍歷到的頁都進行處理,先總結一下這個shrink_page_list()對每個遍歷到的頁主要做哪幾件事情:
- 檢查此頁是否正在回寫(通過頁描述符的PG_writeback標志),然后做相應的處理
- 檢查此頁最近是否有被訪問過(非文件頁通過頁表項的Accessed判斷,文件頁通過頁描述符的PG_referenced和頁表項的Accessed判斷),有則進行相應處理(此頁就不一定被回收)
- 如果是非文件頁,檢查此頁是否加入到了swap cache(置位PG_swapcache),沒有則將此頁加入到swap cache(通過PG_swapcache判斷),並且標記非文件頁為臟頁(重要,標記PG_dirty)以及page->_count++,會為此非文件頁分配一個swap類型的頁表項,保存到page->private中
- 如果有進程映射了此頁,則進行unmap操作(是否執行unmap操作與sc->may_unmap有關),如果是非文件頁,那么映射了此非文件頁的頁表項被設置為之前分配的swap類型的頁表項,如果是文件頁,則清空頁表項
- 如果頁為臟頁,則對此頁進行異步回寫(是否執行回寫操作與sc->may_writepage有關),一些特殊的文件系統可能進行同步回寫(比如ramdisk),然后設置此頁的PG_reclaim。這里需要注意,只有kswap能夠對文件頁進行回寫
- 如果此頁是文件頁,並且包含有buffer_heads(會以鏈表的形式保存在page->private中),則釋放其buffer_heads鏈表,注意這個buffer_heads是文件頁特有的,因為文件離散地保存在磁盤中,而swap分區是連續的,所以非文件頁並不需要這個buffer_heads。
- 將page->_count == 2和page->_count == 0的干凈頁進行回收,並將它們從swap cache 或者 page cache中移除
- 除了回收的頁,其余的頁都放回到對應的lru鏈表中。
先討論第一件事情,就是檢查這個頁是否在進行回寫操作,這里要先說說塊層的異步回寫的結束后的處理函數end_buffer_async_write(),這個函數將一個頁回寫完成后會檢查頁的PG_reclaim標志,如果置位了則將此頁移動到非活動lru鏈表末尾,因為內存回收掃描是從lru鏈表的末尾進行的,在下次進行內存回收掃描時,會優先掃描到此頁,也可以對此頁進行優先釋放回收。那么,現在在shrink_page_list()中,如果遍歷到的頁在進行回寫操作(通過頁的PG_writeback位判斷),那么導致此頁進行回寫的情況有兩種:頁臟太久了,系統自動將其回寫(PG_writeback置位,而PG_reclaim沒有置位);頁最近被內存回收處理過,是內存回收要求它進行回寫(PG_writeback和PG_reclaim都置位了)。對於第一種情況,則將此頁的PG_reclaim置位,這樣此頁在回寫完成后,就會被放到非活動lru鏈表末尾,這樣在下次內存回收時,此頁就很大可能被作為一個干凈頁去釋放回收。對於第二種可能,這個頁本來就是進行內存釋放時主動要求其回寫的,那么此頁的PG_writeback和PG_reclaim都在之前處理此頁的內存回收時置位了,這里就不做什么了。
判斷完遍歷到的頁是否正在進行回寫后,還需要判斷此頁最近是否被訪問過,處理如下:
如果掃描的是非活動文件頁lru鏈表,本次回收跳過的頁有:
- 此文件頁最近被多個進程訪問(多個映射此頁的進程頁表項Accessed被置位),則將頁移動到活動文件頁lru鏈表頭部。
- 此頁的PG_referenced被置位,則將頁移動到活動文件頁lru鏈表頭部。
- 對於最近被訪問過的代碼段文件頁,移動到活動文件頁lru鏈表頭部。
如果掃描的是非活動匿名頁lru鏈表,本次回收跳過的頁有:
- 對於最近訪問過的頁(一個或多個映射了此頁的進程頁表項的Accessed被置位),將頁移動到活動匿名頁lru鏈表尾部中。
- 對於正在回寫的頁,將頁移動到非活動匿名頁lru鏈表頭部,並標記這些頁的PG_reclaim。
除了以上這些頁,其他頁都可以順利通過檢查,之前的工作相當於判斷此頁能否進行回收,現在開始的工作就是為此頁的回收做准備了,總的來說,就是三件事:
- 非文件頁加入到swap cache
- 對頁進行unmap操作
- 調用page->mapping->a_ops->writepage進行異步回寫
當一個非文件頁加入swap cache時,主要對此文件做幾件事,首先,分配一個swap類型的頁表項,將所有映射了此頁的進程頁表項設置為這個swap類型的頁表項;其次,置位此頁的PG_dirty,標記此頁是一個臟頁,這樣后面就會通過判斷這個進行異步回寫了;最后,將此頁的mapping指向swap分區的address_space,這樣在進行異步回寫時,就能夠通過swap分區的address_space->a_ops->writepage函數將此頁異步回寫到swap分區中。對於文件頁來說,則沒有這一步加入到swap cache中,因為每個文件都有自己的address_space,一個新的文件頁就已經有對應文件的address_space了。
之后進行unmap操作,對於非文件頁,這個的工作就是將映射了此非文件頁的頁表項設置為之前分配的swap類型的頁表項,而對於文件頁來說,則清空映射了此文件頁的進程頁表項。
然后,就調用頁描述符中的page->mapping->a_ops->writepage將頁進行異步回寫,這里需要注意,只對臟頁進行異步回寫,這就是為什么當非文件頁加入到swap cache后,要設置為臟頁,這里就會將它回寫到磁盤,而對於文件頁,只有數據與磁盤中不一致時,才需要回寫。並且這里會對臟頁設置PG_reclaim標志,而干凈頁則不用。
好了 ,這幾步做完了,可以將此頁進行回收了吧,可惜這時候只能對不用進行回寫的干凈頁進行回收,因為回寫是異步進行的,這些正在進行回寫的頁,會被放到非活動lru鏈表頭部,這里就與前面說的相呼應了,當回寫完成后,通過判斷PG_reclaim標志,會將頁移動到非活動lru鏈表末尾,這樣在下次進行內存回收時,這些頁就更優先進行回收了。
假設現在內存回收掃描到了這個回寫完成的頁,如果此頁是文件頁,那么它還必定會有一個buffer_heads鏈表需要進行釋放,這個buffer_heads用於描述此頁需要回寫到磁盤的位置。當文件頁回寫完后,如果此文件頁又被內存回收掃描到了,准備對它回收,那么就會將此文件頁的buffer_heads進行釋放。buffer_heads都保存頁描述符page->private中。
之后,如果回收的是文件頁,那么還必須將此頁從所屬文件的page cache中移除,如果回收的是非文件頁頁,也必須將此頁從所屬swap分區的swap cache中移除。
到這里,此頁已經可以進行回收了。
下面我們默認此頁能夠回收,忽略回收檢查,並且默認沒有進程在此期間訪問頁,將頁分為干凈文件頁,臟文件頁,非文件頁描述一下回收過程(非文件頁只要加入到swap cache中就會被設置為臟頁):
干凈文件頁回收過程:
可以看到,對於干凈文件頁,由於文件頁不加入swapcache,只需要進行一個unmap操作,就可以直接進行回收了,這種頁回收效率是最高的。
對於臟文件頁:
可以看到對於臟文件頁,待其回寫完成后,內核進行一次內存回收時,如果掃描到此頁,只需要直接將其釋放就可以了。注意:只有kswapd內核線程能夠對臟文件頁進行回寫操作,並且回寫完成后並不會主動要求內核進行一次內存回收,也有可能回寫完成后,zone的內存足夠了,就不進行內存回收了。
再看看非文件頁的回收流程:
其實很簡單,對於臟頁,在回寫之后的下次內存回收時,就可以將其回收,而對於干凈的頁,在本次內存回收時,就可以將其回收。而當非文件頁加入swapcache后,就會被設置為臟頁(PG_dirty置位)。
其實可以總結,非文件頁相對於文件頁來說,在內存回收處理過程中有以下區別:
- 一般回收的非文件頁在非活動匿名頁lru鏈表中,而回收的文件頁在非活動文件頁lru鏈表中。
- 非文件頁回寫前必須要加入swapcache,並會生成一個以頁槽號為偏移量的swap類型的頁表項;而文件頁不會加入swapcache,並且沒有swap類型的頁表項
- unmap時,映射了非文件頁的進程頁表項會被設置為swao類型的頁表項,而映射了文件頁的進程頁表項則直接清空
- 非文件頁在有進程映射了的情況下,一定要進行回寫后才能回收;而文件頁即使沒有進程映射的情況下,只要是臟頁,回收時都要回寫
- 非文件頁沒有buffer_heads,不需要對buffer_heads進行回收,而文件頁回寫完后進行需要進行buffer_heads的回收
現在再說說在回寫過程中,又有進程映射了此頁怎么辦,這里我們結合page->_count來說,之前有說過,當有模塊引用或者進程映射了此頁的時候,此頁的page->_count就會++,這里我們假設一個場景,有10個進程映射了一個非文件頁,沒有其他模塊引用此非文件頁,那么此頁的page->_count就為10。然后此頁在非活動匿名頁lru鏈表中被內存回收掃描到,內核打算對此頁進行回收,第一件做的事情,將此頁從lru鏈表隔離出來,這里page->_count++(就等於11了)。第二件事,將此頁加入到swap cache中,page->_count++(現在等於12了)。第三件事,對此頁進行unmap,由於有10個進程映射了此頁,unmap后,此頁的page->_count -= 10,現在page->_count就剩2了,如果此頁是干凈頁,那么如之前說的,回收時判斷page->_count == 2的可以進行回收。如果此頁是臟頁,那么就回寫,然后將此頁放回到非活動匿名頁lru鏈表,這時page->_count會減1(這時候就為1了,這里為1是因為swapcache在引用此頁)。之后回寫完成再被掃描到時,一樣會進行隔離,那么page->_count++(現在為2了),最后一樣可以通過page->_count == 2判斷此頁能夠釋放。這樣說明,如果在回寫過程中,有進程又映射了此頁,因為映射此頁那么page->_count就會增加,在回寫完成后的回收時,此page->_count就不可能變為0了,更何況由於有進程映射了此頁,說明此進程最近訪問了此頁,此頁還會被移動到活動匿名頁lru鏈表中。而對於文件頁,即使沒有進程映射它,它的page->_count就為1,因為它自出身一來,就被對應文件的page cache引用了。並且因為文件頁不需要加入到swap cache,實際上在內存回收過程中,當沒有進程映射此文件頁時,它的page->_count一樣為2。
內存回收種類
因為在不同的內存分配路徑中,會觸發不同的內存回收方式,內存回收針對的目標有兩種,一種是針對zone的,另一種是針對一個memcg的,而這里我們只討論針對zone的內存回收,個人把針對zone的內存回收方式分為三種,分別是快速內存回收、直接內存回收、kswapd內存回收。
- 快速內存回收:處於get_page_from_freelist()函數中,在遍歷zonelist過程中,對每個zone都在分配前進行判斷,如果分配后zone的空閑內存數量 < 閥值 + 保留頁框數量,那么此zone就會進行快速內存回收,即使分配前此zone空閑頁框數量都沒有達到閥值,都會進行此zone的快速內存回收。注意閥值可能是min/low/high的任何一種,因為在快速內存分配,慢速內存分配和oom分配過程中如果回收的頁框足夠,都會調用到get_page_from_freelist()函數,所以快速內存回收不僅僅發生在快速內存分配中,在慢速內存分配過程中也會發生。
- 直接內存回收:處於慢速分配過程中,直接內存回收只有一種情況下會使用,在慢速分配中無法從zonelist的所有zone中以min閥值分配頁框,並且進行異步內存壓縮后,還是無法分配到頁框的時候,就對zonelist中的所有zone進行一次直接內存回收。注意,直接內存回收是針對zonelist中的所有zone的,它並不像快速內存回收和kswapd內存回收,只會對zonelist中空閑頁框不達標的zone進行內存回收。並且在直接內存回收中,有可能喚醒flush內核線程。
- kswapd內存回收:發生在kswapd內核線程中,每個node有一個swapd內核線程,也就是kswapd內核線程中的內存回收,是只針對所在node的,並且只會對 分配了order頁框數量后空閑頁框數量 < 此zone的high閥值 + 保留頁框數量 的zone進行內存回收,並不會對此node的所有zone進行內存回收。
這三種內存回收雖然是在不同狀態下會被觸發,但是如果當內存不足時,kswapd內存回收和直接內存回收很大可能是在並發的進行內存回收的。而實際上,這三種回收再怎么不同,進行內存回收的執行代碼是一樣的,只是在內存回收前做的一些處理和判斷不同。
快速內存回收
無論是在快速分配還是慢速分配過程中,只要內核希望從一個zonelist中獲取連續頁框,就必須調用get_page_from_freelist()函數,在此函數中會對zonelist中的所有zone進行判斷,判斷能否從此zone分配連續頁框,而判斷一個zone能否進行分配的唯一標准是:分配后剩余的頁框數量 > 閥值 + 此zone的保留頁框數量。當zone不滿足這個標准,內核會對zone進行快速內存回收,這個快速內存回收的執行路徑是:
get_page_from_freelist() -> zone_reclaim() -> __zone_reclaim() ->shrink_zone()
由於篇幅關系,就不列代碼了,之前也說了,/proc/sys/vm/zone_reclaim_mode會影響快速內存回收,在get_page_from_freelist()函數中就有這么一段:
/* * 判斷是否對此zone進行內存回收,如果開啟了內存回收,則會對此zone進行內存回收,否則,通過距離判斷是否進行內存回收 * zone_allows_reclaim()函數實際上就是判斷zone所在node是否與preferred_zone所在node的距離 < RECLAIM_DISTANCE(30或10) * 當內存回收未開啟的情況下,只會對距離比較近的zone進行回收 */ if (zone_reclaim_mode == 0 || !zone_allows_reclaim(preferred_zone, zone)) goto this_zone_full;
zone_allows_reclaim()用於計算zone與preferred_zone之間的距離,這個跟node距離有關,當距離不滿足時,則不會對此zone進行快速內存回收,也就是當zone_reclaim_mode開啟后,才會對zonelist中的所有zone進行內存回收。
需要注意閥值,之前也說了,在一次分配過程中,可能很多地方會調用get_page_from_freelist()函數,而每次傳入的閥值很可能是不同的,在第一次進行快速分配時,使用的是zone的low閥值進行get_page_from_freelist()調用,在慢速分配過程中,會使用zone的min閥值進行get_page_from_freelist()調用,而在oomkill進行分配過程中,會使用high閥值調用get_page_from_freelist(),當zone的分配后剩余的頁框數量 < 閥值 + 此zone的保留頁框數量 時,則會調用zone_reclaim()對此zone進行內存回收而zone_reclaim()又會調用到__zone_relcaim()。在__zone_reclaim()中,主要做三件事:初始化一個struct scan_control結構、循環調用shrink_zone()進行對zone的內存回收、從調用shrink_slab()對slab進行回收,struct scan_ control結構初始化如下:
struct scan_control sc = { /* 最少一次回收SWAP_CLUSTER_MAX,最多一次回收1 << order個,應該是1024個 */ .nr_to_reclaim = max(nr_pages, SWAP_CLUSTER_MAX), /* 當前進程明確禁止分配內存的IO操作(禁止__GFP_IO,__GFP_FS標志),那么則清除__GFP_IO,__GFP_FS標志,表示不進行IO操作 */ .gfp_mask = (gfp_mask = memalloc_noio_flags(gfp_mask)), .order = order, /* 優先級為4,默認是12,會比12一次掃描更多lru鏈表中的頁框,而且掃描次數會比優先級為12的少,並且如果回收過程中回收到了足夠頁框,就會返回 */ .priority = ZONE_RECLAIM_PRIORITY, /* 通過/proc/sys/vm/zone_reclaim_mode文件設置是否允許將臟頁回寫到磁盤,即使設為允許,快速內存回收也不能對臟文件頁進行回寫操作。 * 當zone_reclaim_mode為0時,在這里是不允許頁框回寫的, */ .may_writepage = !!(zone_reclaim_mode & RECLAIM_WRITE), /* 通過/proc/sys/vm/zone_reclaim_mode文件設置是否允許將匿名頁回寫到swap分區 * 當zone_reclaim_mode為0時,在這里是不允許匿名頁回寫的,我們這里假設允許 */ .may_unmap = !!(zone_reclaim_mode & RECLAIM_SWAP), /* 允許對匿名頁lru鏈表操作 */ .may_swap = 1, /* 本結構還有一個 * .target_mem_cgroup 表示是針對某個memcg,還是針對整個zone進行內存回收的,這里為空,也就是說這里是針對整個zone進行內存回收的 */ };
nr_pages是1<<order。可以看到優先級為4,sc->may_writepage和sc->may_unmap與zone_reclaim_mode有關。
這個sc是針對一個zone的,上面也說了,只有當zone不滿足 分配后剩余的頁框數量 > 閥值 + 此zone保留的頁框數量 時,才會對zone進行內存回收,也就是它不是針對整個zonelist進行內存回收的,而是針對不滿足情況的zone進行。
再看看循環調用shrink_zone():
do { /* 對此zone進行內存回收,內存回收的主要函數 */ shrink_zone(zone, &sc); /* 沒有回收到足夠頁框,並且循環次數沒達到優先級次數,繼續 */ } while (sc.nr_reclaimed < nr_pages && --sc.priority >= 0);
可以看到,每次調用shrink_zone后都會sc.priority--,也就是最多進行4次調用shrink_zone(),並且每次調用shrink_zone()掃描的頁框會越來越多,直到回收到了1<<order個頁框為止。
注意:在快速內存回收中,即使zone_reclaim_mode允許回寫,也不會對臟文件頁進行回寫操作的,但是如果zone_reclaim_mode允許,會對非文件頁進行回寫操作。
可以對快速內存回收總結出:
開始標志是:此zone分配后剩余的頁框數量 > 此zone的閥值 + 此zone的保留頁框數量(閥值可能是:min,low,high其中一個)。
結束標志是:對此zone回收到了本次分配時需要的頁框數量 或者 sc->priority降為0(可能會進行多次shrink_zone()的調用)。
回收對象:zone的干凈文件頁、slab、可能會回寫匿名頁
直接內存回收
調用流程:
__alloc_pages_slowpath() -> __alloc_pages_direct_reclaim() -> __perform_reclaim() -> try_to_free_pages() -> do_try_to_free_pages() -> shrink_zones() -> shrink_zone()
直接內存回收發生在慢速分配中,在慢速分配中,首先喚醒所有node結點的kswap內核線程,然后會調用get_page_from_freelist()嘗試用min閥值從zonelist的zone中獲取連續頁框,如果失敗,則對zonelist的zone進行異步壓縮,異步壓縮之后再次調用get_page_from_freelist()嘗試使用min閥值從zonelist的zone中獲取連續頁框,如果還是失敗,就會進入到直接內存回收。在進行直接內存回收時,進程是有可能加入到node的pgdat->pfmemalloc_wait這個等待隊列中,當kswapd進行內存回收后如果node空閑內存達到平衡,那么就會喚醒pgdat->pfmemalloc_wait中的進程,其實也就是,加入到pgdat->pfmemalloc_wait這個等待隊列的進程,自身就不會進行直接內存回收,而是讓kswapd進行,之后kswapd會喚醒它們。之后的文章會詳細說明這種情況。
先看初始化的struct scan_control,是在try_to_free_pages()中進行初始化的:
struct scan_control sc = { /* 打算回收32個頁框 */ .nr_to_reclaim = SWAP_CLUSTER_MAX, .gfp_mask = (gfp_mask = memalloc_noio_flags(gfp_mask)), /* 本次內存分配的order值 */ .order = order, /* 允許進行回收的node掩碼 */ .nodemask = nodemask, /* 優先級為默認的12 */ .priority = DEF_PRIORITY, /* 與/proc/sys/vm/laptop_mode文件有關 * laptop_mode為0,則允許進行回寫操作,即使允許回寫,直接內存回收也不能對臟文件頁進行回寫 * 不過允許回寫時,可以對非文件頁進行回寫 */ .may_writepage = !laptop_mode, /* 允許進行unmap操作 */ .may_unmap = 1, /* 允許進行非文件頁的操作 */ .may_swap = 1, };
在直接內存回收過程中,這個sc結構是對zonelist中所有zone使用的,而不是像快速內存回收,是針對zonelist中不滿足條件的一個一個zone進行使用,對於直接內存回收,以下需要注意:
- sc的c初始使用的是默認的優先級12
- 那么就會對遍歷12遍zonelist中的所有zone,每次遍歷后sc->priority--,相當於讓每個zone執行12次shrink_zone()
- 只有sc->priority == 12時會對zonelist中的所有zone強制執行shrink_zone(),而當sc->priority == 12這輪循環過后,會通過判斷來確定zone是否要執行shrink_zone(),這個判斷標志就是:此zone已經掃描的頁數 < (此zone所有沒有鎖在內存中的文件頁和非文件頁之和 * 6) 。如果掃描頁數超過此值,就說明已經對此zone掃描過太多頁框了,就不對此zone進行shrink_zone()了。
- 並且當優先級降到10以下時,即使原來sc->may_writepage不允許回寫,這時候會開始允許回寫。這樣做是因為不回寫很難回收到頁框。
- 只打算回收的頁框為32個,並且在此期間,如果掃描頁數超過(sc->nr_to_reclaim + sc->nr_to_reclaim / 2),則是會根據laptop_mode的情況喚醒flush內核線程的。
- 直接內存回收無論如何都不會對臟文件頁進行回寫操作,如果sc->may_writepage為1,那么會對非文件頁進行回寫操作
- 會對文件頁和非文件頁進行unmap操作
- 會對非文件頁處理(加入swap cache,unmap,回寫)
- 會先回收在memcg中並且超過所在memcg的soft_limit_in_bytes的進程的內存
- 也會調用shrink_slab()對slab進行回收
個人認為直接內存回收是為了讓更多的頁得到掃描,然后進行回寫操作,也可能是為了后面的內存壓縮回收一些頁框,其實這里不太理解,為什么只回收32個頁框,它並不像直接內存回收,打算回收的頁框數量是1<<order。
可以對直接內存回收總結出:
開始標志是:zonelist的所有zone都不能通過min閥值獲取到頁框時。
結束標志:回收到32個頁框,或者sc->priority降到0,或者空閑頁框足夠進行內存壓縮了(可能會進行多次shrink_zone()的調用)。
回收對象:超過所在memcg的soft_limit_in_bytes的進程的內存、zone的干凈文件頁、slab、匿名頁swap
kswapd內存回收
調用過程:
kswapd -> balance_pgdat() -> kswapd_shrink_zone() -> shrink_zone()
在分配過程中,只要get_page_from_freelist()函數無法以low閥值從zonelist的zone中獲取到連續頁框,並且分配內存標志gfp_mask沒有標記__GFP_NO_KSWAPD,則會喚醒kswapd內核線程,在當中執行kswapd內存回收。
先看初始化的sc結構:
/* 掃描控制結構 */ struct scan_control sc = { /* (__GFP_WAIT | __GFP_IO | __GFP_FS) * 此次內存回收允許進行IO和文件系統操作,有可能阻塞 */ .gfp_mask = GFP_KERNEL, /* 分配內存失敗時使用的order值,因為只有分配內存失敗才會喚醒kswapd */ .order = order, /* 這個優先級決定了一次掃描多少隊列 */ .priority = DEF_PRIORITY, .may_writepage = !laptop_mode, .may_unmap = 1, .may_swap = 1, };
由於此sc是針對整個node的所有zone的,這里沒有設置sc->nr_to_reclaim,在確定對某個zone進行內存回收時,這個sc->nr_to_reclaim被設置為:
sc->nr_to_reclaim = max(SWAP_CLUSTER_MAX, high_wmark_pages(zone));
可以看到,如果回收的頁框數量達到了zone的high閥值,其實意思就是盡可能的回收頁框了。
kswapd內核線程是每個node有一個的,那也意味着,此node的kswapd只會對此node的zone進行內存回收工作,也就不需要zonelist了。
要點:
- 優先級使用默認為的12,會執行多次遍歷node(並不是node中的所有zone),但並不會每次遍歷都進行sc->priority--,當能夠回收的內存時,才進行sc->priority--
- 以ZONE_HIGHMEM -> ZONE_NORMAL ->ZONE_DMA的順序找出第一個不平衡的zone,平衡條件是: 此zone分配頁框后剩余的頁框數量 > 此zone的high閥值 + 此zone保留的頁框數量。不滿足則表示此zone不平衡。
- 對第一個不平衡的zone及其后面的zone進行回收在memcg中並且超過所在memcg的soft_limit_in_bytes的進程的內存,比如第一個不平衡的zone是ZONE_NORMAL,那么執行內存回收的zone就是ZONE_NORMAL和ZONE_DMA。
- 如果zone是平衡的,則不對zone進行內存回收(但是上面那部不會因為zone平衡而不執行),而如果zone是不平衡的,那么會調用shrink_zone()進行內存回收,以及調用shrink_slab()進行slab的回收。
- 對於node中所有 zone分配后剩余內存 < zone的low閥值 + zone保留的頁框數量 的zone,會進行內存壓縮
- 檢查node中所有zone是否都平衡,沒有平衡則繼續循環
- 如果laptop == 0,那么會對文件頁和非文件頁進行回寫操作,如果laptop == 1,那么只有當sc->priority < 10時才會對文件頁和非文件頁進行回寫操作
- 會對文件頁和非文件頁進行回寫unmap操作
- 會對非文件頁進行處理(加入swapcache,unmap,回寫)
可以看出來,kswapd內存回收會將node結點中的所有zone的空閑頁框都至少拉高high閥值。
可以對kswapd內存回收總結出:
開始標志:zonelist的所有zone都不能通過min閥值獲取到頁框時,會喚醒所有node的kswapd內核線程,然后在kswapd中會對不滿足 zone分配頁框后剩余的頁框數量 > 此zone的high閥值 + 此zone保留的頁框數量 的zone進行內存回收。
結束標志:node中所有zone都滿足 zone分配頁框后剩余的頁框數量 > 此zone的high閥值 + 此zone保留的頁框數量(可能會進行多次shrink_zone()的調用)。
回收對象:超過所在memcg的soft_limit_in_bytes的進程的內存、zone的干凈的文件頁、zone的臟的文件頁、slab、匿名頁swap
總結:
文章因為篇幅的關系虎頭蛇尾了,有很多細節沒有說明,之后會對其進行補充的。