linux內存源碼分析 - 內存回收(整體流程)


本文為原創,轉載請注明: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中查看這三個閥值的數值:

  可以很明顯看出來,相對於整個zone管理的總頁框數量(managed),這三個值是非常非常小的,連managed的1%都不到,這些都是在系統初始化期間進行設置的,具體設置函數是__setup_per_zone_wmarks()。有興趣的可以去看看。這個閥值對內存回收的進行具有很重要的意義,后面會詳細進行說明。
  對於zone的內存回收,它針對三樣東西進程回收:slab、lru鏈表中的頁、buffer_head。這里只討論內存回收針對lru鏈表中的頁是如何進行回收的。lru鏈表主要用於管理進程空間中使用的內存頁,它主要管理三種類型的頁:匿名頁、文件頁以及shmem使用的頁。在內存回收過程中,說簡單些,就是將lru鏈表中的一些頁數據放到磁盤中,然后將這些頁釋放,當然實際上可沒有那么簡單,這個后面會詳細說明。
  在說內存回收前,要先補充一些知識,因為內存回收並不是一個孤立的功能,它內部會涉及到其他很多東西,比如內存分配、lru鏈表、反向映射、swapcache、pagecache等。
 

判斷頁是否能夠回收

   拋開內存回收不談,在內核中,只有一種頁能夠進行回收,就是頁描述符中的_count為0的頁,每個頁都有自己唯一的頁描述符,而每個頁描述符中都有一個_count,這個_count代表的是此頁的引用計數,當_count為-1時,說明此頁是空閑的,存放在伙伴系統中,每當有一個進程映射了此頁時,此頁的_count就會++,也就是當某個頁被10個進程映射了,它的page->_count肯定大於10(不等於10是因為可能還有其他模塊引用了此頁,比如塊層、驅動等),所以也可以反過來說,如果某個頁的page->_count == 0,那就說明此頁可以直接釋放回收了。也就是說,內核實際上回收的是那些page->_count == 0的頁,但是如果真的是這樣,內存回收這就沒有任何意義了,因為當最后一個引用此頁的模塊釋放掉此頁的引用時,如果page->_count為0,肯定會釋放回收此頁的。實際上內存回收做的事情,就是想辦法將一些page->_count不為0的頁,嘗試將它們的page->_count降到0,這樣系統就可以回收這些頁了。下面是我總結出來在內存回收過程中會對頁的page->_count產生影響的操作:
  • 一個進程映射此頁,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鏈表

  lru鏈表主要作用就是將頁排序,將最應該回收的頁放到最后面,最不應該回收的頁放到最前面,,然后進行內存回收時,就會從后面向前面進行掃描,將掃描到的頁嘗試進行回收,具體見 linux內存源碼分析 - 內存回收(lru鏈表)。這里只需要記住一點, 回收的頁都是非活動匿名頁lru鏈表或者非活動文件頁lru鏈表上的頁。這些頁包括:進程堆、棧、匿名mmap共享內存映射、shmem共享內存映射使用的頁、映射磁盤文件的頁。
 

頁的換入換出

  首先先說明一下頁描述符中對內存回收來說非常必要的標志:

  • 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鏈表的頁的換入換出進行描述。

  匿名頁lru鏈表上保存的頁為:進程堆、棧、數據段,匿名mmap共享內存映射,shmem映射。這些類型的頁都有個特點,在磁盤上沒有映射對應的文件(shmem有對應的文件,是/dev/zero,但它不是映射此設備文件)。而在內存回收時,會從非活動匿名頁lru鏈表末尾向前掃描一定數量的頁框,然后嘗試將這些頁框進行回收,而如果這些頁框沒有進程映射它們,那么它們可以直接釋放,而如果有進程映射了它們,那么系統就必須將這些頁框回寫到磁盤上。在linux系統中,你可以給系統掛載一個swap分區,這個分區就是專門用於保存這些類型的頁的。當這些頁需要回收,並且有進程映射了它們時,系統就會將這些頁寫入swap分區,需要注意,它們需要回收只有在內存不足進行內存回收時才會發生,也就是當系統內存充足時,是不會將這些類型的頁寫入到swap分區中的(使用memcg除外),在磁盤上,一個swap分區是一組連續的物理扇區,比如一個1G大小的swap分區,那么它在磁盤上會占有1G大小磁盤塊,然后這塊磁盤塊的第一個4K,專門用於存swap分區描述結構的,而之后的磁盤塊,會被划分為一個一個4K大小的頁槽(正好與普通頁大小一致),然后將它們標以ID,如下:
  每個頁槽可以保存一個頁的數據,這樣,一個被換出的頁就可以寫入到磁盤中,系統也能夠將這些頁組織起來了。雖然是叫swap分區,但是內核似乎並不將swap分區當做一個磁盤分區來看待,更像的是將其當做一個文件來看待,因為這個,每個swap分區都有一個address_space結構,這個結構是每個磁盤文件都會有一個的,這個address_space結構中最重要的是有一個基樹和一個address_space操作集。而這里swap分區有一個, swap分區的address_space叫做swap cache,它的作用是從非文件頁在回寫到swap分區到此非文件頁被回收前的這段時間里,起到一個將swap類型的頁表項與此頁關聯的作用和同步的作用。在這個swap cache的基樹中,將此swap分區的所有頁槽組織在了一起。當非活動匿名頁lru鏈表中的一個頁需要寫入到swap分區時,步驟如下:
  1. swap分配一個空閑的頁槽
  2. 根據這個空閑頁槽的ID,從swap分區的swap cache的基樹中找到此頁槽ID對應的結點,將此頁的頁描述符存入當中
  3. 內核以頁槽ID作為偏移量生成一個swap頁表項,並將這個swap頁表項保存到頁描述符中的private中
  4. 對頁進行反向映射,將所有映射了此頁的進程頁表項改為此swap頁表項
  5. 將此頁的mapping改為指向此swap分區的address_space,並將此頁設置為臟頁
  6. 通過swap cache中的address_space操作集將此頁回寫到swap分區中
  7. 回寫完成
  8. 此頁要被回收,將此頁從swap cache中拿出來

當一個進程需要訪問此頁時,系統則會將此頁從swap分區換入內存中,具體步驟如下:

  1. 一個進行訪問了此頁,會先訪問到之前設置的swap頁表項
  2. 產生缺頁異常,在缺頁異常中判斷此頁在swap分區中,而不在內存中
  3. 分配一個新頁
  4. 根據進程的頁表項中的swap頁表項找到對應的頁槽和swap cache
  5. 如果以頁槽ID在swap cache中沒有找到此頁,說明此頁已被回收,從分區中將此頁讀取進來
  6. 如果以頁槽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,接下來的處理就是差不多的,區別為以下三點:

  1. 對於磁盤文件來說,它的數據並不像swap分區這樣是連續的。
  2. 當文件數據讀入到一個頁時,此文件頁就需要在文件的page cache中做關聯,這樣當其他進程也需要訪問文件的這塊數據時,通過page cache就可以知道此頁在不在內存中了。
  3. 並不會為映射了此文件頁的進程頁表項生成一個新的頁表項,會將所有映射了此頁的頁表項清空,因為在缺頁異常中通過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進行多次的內存回收,這是因為兩個方面

  1. 如果每次僅回收2^order個頁框,滿足於本次內存分配(內存分配失敗時才會導致內存回收),那么下次內存分配時又會導致內存回收,影響效率,所以,每次zone的內存回收,都是盡量回收更多頁框,制定回收的目標是2^(order+1)個頁框,比要求的2^order多了一倍。但是當非活動lru鏈表中的數量不滿足這個標准時,則取消這種狀態的判斷。
  2. 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進行內存回收,總的來說,流程如下:

  1. 從root_memcg開始遍歷memcg
    1. 獲取memcg的lru鏈表描述符lruvec
    2. 獲取memcg的swapiness
    3. 調用shrink_lruvec()對此memcg的lru鏈表進行處理
  2. 遍歷完所有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鏈表。此函數主要工作就是:

  1. 調用get_scan_count()計算每個lru鏈表需要掃描的頁框數量,保存到nr數組中;
  2. 循環判斷nr數組中是否還有lru鏈表沒有掃描完成
    • 以活動匿名頁lru鏈表、非活動匿名頁lru鏈表、活動文件頁lru鏈表、非活動文件頁lru鏈表的順序作為一輪掃描,每次每個lru鏈表掃描32個頁框,並且在nr數組中減去lru鏈表對應掃描的數量;
    • 一輪掃描結束后判斷是否回收到了足夠頁框,沒有回收到足夠頁框則跳到 2 繼續循環判斷nr數組;
    • 已經回收到了足夠頁框,當nr數組有剩余時,判斷是否要對lru鏈表繼續掃描,如果要繼續掃描,則跳到 2
  3. 如果非活動匿名頁lru鏈表中頁數量太少,則對活動匿名頁進行一個32個頁框的掃描;
  4. 如果太多臟頁正在進行回寫,則睡眠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()函數總結要點有五:

  1. 將本cpu的lru緩存全部清空,將lru緩存的頁放到lru鏈表中,而其他CPU的則不處理
  2. 根據sc->may_writepage與sc->may_unmap選擇要隔離的頁
  3. 如果結點buffer_heads數量超過限制值,則會嘗試對掃描到的文件頁進行buffer_heads的釋放,進行釋放后的文件頁的page->_count--
  4. 將所有映射了隔離頁的頁表項Accessed都跟清0
  5. 將最近被訪問過的代碼段的頁移動到活動lru鏈表頭,其余頁都移動到非活動lru鏈表頭
  6. 將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()對每個遍歷到的頁主要做哪幾件事情:

  1. 檢查此頁是否正在回寫(通過頁描述符的PG_writeback標志),然后做相應的處理
  2. 檢查此頁最近是否有被訪問過(非文件頁通過頁表項的Accessed判斷,文件頁通過頁描述符的PG_referenced和頁表項的Accessed判斷),有則進行相應處理(此頁就不一定被回收)
  3. 如果是非文件頁,檢查此頁是否加入到了swap cache(置位PG_swapcache),沒有則將此頁加入到swap cache(通過PG_swapcache判斷),並且標記非文件頁為臟頁(重要,標記PG_dirty)以及page->_count++,會為此非文件頁分配一個swap類型的頁表項,保存到page->private中
  4. 如果有進程映射了此頁,則進行unmap操作(是否執行unmap操作與sc->may_unmap有關),如果是非文件頁,那么映射了此非文件頁的頁表項被設置為之前分配的swap類型的頁表項,如果是文件頁,則清空頁表項
  5. 如果頁為臟頁,則對此頁進行異步回寫(是否執行回寫操作與sc->may_writepage有關),一些特殊的文件系統可能進行同步回寫(比如ramdisk),然后設置此頁的PG_reclaim。這里需要注意,只有kswap能夠對文件頁進行回寫
  6. 如果此頁是文件頁,並且包含有buffer_heads(會以鏈表的形式保存在page->private中),則釋放其buffer_heads鏈表,注意這個buffer_heads是文件頁特有的,因為文件離散地保存在磁盤中,而swap分區是連續的,所以非文件頁並不需要這個buffer_heads。
  7. 將page->_count == 2和page->_count == 0的干凈頁進行回收,並將它們從swap cache 或者 page cache中移除
  8. 除了回收的頁,其余的頁都放回到對應的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鏈表,本次回收跳過的頁有:

  1. 此文件頁最近被多個進程訪問(多個映射此頁的進程頁表項Accessed被置位),則將頁移動到活動文件頁lru鏈表頭部。
  2. 此頁的PG_referenced被置位,則將頁移動到活動文件頁lru鏈表頭部。
  3. 對於最近被訪問過的代碼段文件頁,移動到活動文件頁lru鏈表頭部。

如果掃描的是非活動匿名頁lru鏈表,本次回收跳過的頁有:

  1. 對於最近訪問過的頁(一個或多個映射了此頁的進程頁表項的Accessed被置位),將頁移動到活動匿名頁lru鏈表尾部中。
  2. 對於正在回寫的頁,將頁移動到非活動匿名頁lru鏈表頭部,並標記這些頁的PG_reclaim。

   除了以上這些頁,其他頁都可以順利通過檢查,之前的工作相當於判斷此頁能否進行回收,現在開始的工作就是為此頁的回收做准備了,總的來說,就是三件事:

  1. 非文件頁加入到swap cache
  2. 對頁進行unmap操作
  3. 調用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置位)。

  其實可以總結,非文件頁相對於文件頁來說,在內存回收處理過程中有以下區別:

  1. 一般回收的非文件頁在非活動匿名頁lru鏈表中,而回收的文件頁在非活動文件頁lru鏈表中。
  2. 非文件頁回寫前必須要加入swapcache,並會生成一個以頁槽號為偏移量的swap類型的頁表項;而文件頁不會加入swapcache,並且沒有swap類型的頁表項
  3. unmap時,映射了非文件頁的進程頁表項會被設置為swao類型的頁表項,而映射了文件頁的進程頁表項則直接清空
  4. 非文件頁在有進程映射了的情況下,一定要進行回寫后才能回收;而文件頁即使沒有進程映射的情況下,只要是臟頁,回收時都要回寫
  5. 非文件頁沒有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內存回收。

  1. 快速內存回收:處於get_page_from_freelist()函數中,在遍歷zonelist過程中,對每個zone都在分配前進行判斷,如果分配后zone的空閑內存數量 < 閥值 + 保留頁框數量,那么此zone就會進行快速內存回收,即使分配前此zone空閑頁框數量都沒有達到閥值,都會進行此zone的快速內存回收。注意閥值可能是min/low/high的任何一種,因為在快速內存分配,慢速內存分配和oom分配過程中如果回收的頁框足夠,都會調用到get_page_from_freelist()函數,所以快速內存回收不僅僅發生在快速內存分配中,在慢速內存分配過程中也會發生。
  2. 直接內存回收:處於慢速分配過程中,直接內存回收只有一種情況下會使用,在慢速分配中無法從zonelist的所有zone中以min閥值分配頁框,並且進行異步內存壓縮后,還是無法分配到頁框的時候,就對zonelist中的所有zone進行一次直接內存回收。注意,直接內存回收是針對zonelist中的所有zone的,它並不像快速內存回收和kswapd內存回收,只會對zonelist中空閑頁框不達標的zone進行內存回收。並且在直接內存回收中,有可能喚醒flush內核線程。
  3. 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

 

 

總結:

  文章因為篇幅的關系虎頭蛇尾了,有很多細節沒有說明,之后會對其進行補充的。

 

 

 

 

   


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM