本文為原創,轉載請注明:http://www.cnblogs.com/tolimit/
概述
本文章最好結合linux內存管理源碼分析 - 頁框分配器與linux內存源碼分析 -伙伴系統(初始化和申請頁框)一起看,會涉及里面的一些知識。
我們知道內存是以頁框為單位,每個頁框大小默認是4K(大頁除外),而在系統運行時間長后就會出現內存碎片,內存碎片的意思就是一段空閑頁框中,會有零散的一些正在使用的頁框,導致此段頁框被這些正在使用的零散頁框分為一小段一小段連續頁框,這樣當需要大段連續頁框時就沒辦法分配了,這些空閑頁框就成了一些碎片,不能合並起來作為一段大的空閑頁框使用,如下圖:


白色的為空閑頁框,而有斜線的為已經在使用的頁框,在這個圖中,空閑頁框都是零散的,它們沒辦法組成一塊連續的空閑頁框,它們只能單個單個進行分配,當內核需要分配連續頁框時則沒辦法從這里分配。為了解決這個問題,內核實現了內存壓縮功能,其原理很簡單,就是從這塊內存區段的前面掃描可移動的頁框,從內存區段后面向前掃描空閑的頁框,兩邊掃描結束后,將可移動的頁框放入到空閑頁框中,最后最理想的結果就如下圖:
這樣移動之后就把前面的頁框整理為了一大段連續的物理頁框了,當然這只是理想情況,因為並不是所有頁框都可以進行移動,像內核使用的頁框大部分都不能夠移動,而用戶進程的頁框大部分是可以移動了。
內存壓縮
對於內存壓縮來說,只會正對三種類型的頁進行壓縮,分別是:MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE、MIGRATE_CMA。並且內存壓縮是耗費一定的內存、CPU和IO的。
在內存壓縮中,可以移動的頁框有MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE與MIGRATE_CMA這三種類型的頁框,而因為內存壓縮分為同步和異步,在異步過程中,只會移動MIGRATE_MOVABLE和MIGRATE_CMA這兩種類型的頁框。因為這兩種類型的頁框處理,是不會涉及到IO操作的。而在同步過程中,這三種類型的頁框都會進行移動,因為MIGRATE_RECLAIMABLE基本上都是文件頁,在移動過程中,有可能要將臟頁回寫,會涉及到IO操作,也就是在同步過程中,是會涉及到IO操作的。
內存壓縮模式
內存壓縮分為三種模式,三種模式耗費的資源和對整個系統的壓力不一樣,如下:
enum migrate_mode { /* * 異步模式的意思是禁止阻塞,遇到阻塞和需要調度的時候直接返回,返回前會把隔離出來的頁框放回去 * 在內存不足以分配連續頁框時進行內存壓縮,默認初始是異步模式,如果異步模式后還不能分配連續內存,則會轉為輕同步模式(當明確表示不處理透明大頁,或者當前進程是內核線程時,就會轉為請同步模式) * 而kswapd內核線程中只使用異步模式,不會使用同步模式 * 所以異步不處理MIRGATE_RECLAIMABLE類型的頁框,因為這部分頁框很大可能導致回寫然后阻塞,只處理MIGRATE_MOVABLE和MIGRATE_CMA類型中的頁 * 即使匿名頁加入到了swapcache,被標記為了臟頁,這里也不會進行回寫,只有匿名頁被內存回收換出時,才會進行回寫 * 異步模式不會增加推遲計數器閥值 */ MIGRATE_ASYNC, /* 在內存不足以分配連續頁框並進行了異步內存壓縮之后,有可能會進行輕同步模式,輕同步模式下處理MIRGATE_RECLAIMABLE、MIGRATE_MOVABLE和MIGRATE_CMA類型的頁 * 此模式下允許進行大多數操作的阻塞,比如在磁盤設備繁忙時,鎖繁忙時,但不會阻塞等待正在回寫的頁結束,對於正在回寫的頁直接跳過,也不會對臟頁進行回寫 * 輕同步模式會增加推遲計數器閥值 */ MIGRATE_SYNC_LIGHT, /* 同步模式意味着在輕同步基礎上,可能會對隔離出來需要移動的臟文件頁進行回寫到磁盤的操作(只會對臟文件頁進行回寫,臟匿名頁只做移動,不會被回寫),並且當待處理的頁正在回寫時,會等待到回寫結束 * 這種模式發生有三種情況: * 1.cma分配 * 2.通過alloc_contig_range()嘗試分配一段指定了開始頁框號和結束頁框號的連續頁框時 * 3.將1寫入sysfs中的vm/compact_memory * 同步模式會增加推遲計數器閥值,並且在同步模式下,會設置好compact_control,讓同步模式時忽略pageblock的PB_migrate_skip標記 */ MIGRATE_SYNC, };
- 異步模式:內存壓縮最常用的模式,在此模式中不會進行阻塞(但是時間片到了可以進行主動調度),也就是此種模式不會對文件頁進行處理,文件頁用於映射文件數據使用,這種模式也是對整體系統壓力較小的模式。
- 輕同步模式:當異步模式整理不了更多內存時,有兩種情況下會使用輕同步模式再次壓縮內存:1.明確表示分配的不是透明大頁的情況下;2.當前進程是內核線程的情況下。這個模式中允許大多數操作進行阻塞(比如隔離了太多頁,需要阻塞等待一段時間)。這種模式會處理匿名頁和文件頁,但是不會對臟文件頁執行回寫操作,而當處理的頁正在回寫時,也不會等待其回寫結束。
- 同步模式:所有操作都可以進行阻塞,並且會等待處理的頁回寫結束,並會對文件頁、匿名頁進行回寫到磁盤,所以導致最耗費系統資源,對系統造成的壓力最大。它會在三種情況下發生:1.從cma中分配內存時;2.調用alloc_contig_range()嘗試分配一段指定了開始頁框號和結束頁框號的連續頁框時;3.通過寫入1到sysfs中的/vm/compact_memory文件手動實現同步內存壓縮。
在內存不足以分配連續頁框后導致內存壓縮時,首先會進行異步的內存壓縮,如果異步的內存壓縮后還是不能夠獲取連續的頁框(這種情況發生在很多離散的頁的類型是MIGRATE_RECLAIMABLE),並且gfp_mask明確表示不處理透明大頁的情況或者該進程是個內核線程時,則進行輕同步的內存壓縮。
在kswapd中,永遠只進行異步的內存壓縮,不會進行同步的內存壓縮,並且在kswapd中會跳過標記了PB_migrate_skip的pageblock。相反非kswapd中的內存壓縮,當推遲次數超過了推遲閥值時,會將pageblock的PB_migrate_skip標記清除,也就是會掃描之前有PB_migrate_skip標記的pageblock。
在同步內存壓縮時,會忽略所有標記了PB_migrate_skip的pageblock,強制對這段內存中所有pageblock進行掃描(當然除了MIGRATE_UNMOVEABLE的pageblock)。
異步是用得最多的,它壓縮的速度最快,因為它只處理MIGRATE_MOVABLE和MIGRATE_CMA兩種類型,並且不處理臟頁和阻塞的情況,遇到需要阻塞的情況就返回。而輕同步的情況是在異步無法有效的壓縮足夠內存時使用,它會處理MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE、MIGRATE_CMA三種類型的頁框,在一些阻塞情況也會等待阻塞完成(比如磁盤設備回寫繁忙,待移動的頁正在回寫),但是它不會對臟文件頁進行回寫操作。同步壓縮的情況就是在輕同步的基礎上會對臟文件頁進行回寫操作。
這里需要說明一下,非文件映射頁也是有可能被當成臟頁的,當它加入swapcache后會被標記為臟頁,不過在內存壓縮時,即使匿名頁被標記為臟頁也不會被回寫,它只有在內存回收時才會對臟匿名頁進行回寫到swap分區。在臟匿名頁進行回寫到swap分區后,基本上此匿名頁占用的頁框也快被釋放到伙伴系統中作為空閑頁框了。
內存壓縮算法
先說一下內存壓縮的算法,首先,內存壓縮是以zone為單位的,而zone中又以pageblock為單位。在內存壓縮開始前,會在zone的頭和尾各設置一個指針,頭指針從頭向尾掃描可移動的頁,而尾指針從尾向頭掃描空閑的頁,當他們相遇時終止壓縮。下圖就是簡要的說明圖:
初始時內存狀態(默認所有正在使用的頁框都為可移動):
從頭掃描可移動頁框:
從尾掃描空閑頁框:
結果:
但是實際情況並不是與上面圖示的情況完全一致。頭指針每次掃描一個符合要求的pageblock里的所有頁框,當pageblock不為MIGRATE_MOVABLE、MIGRATE_CMA、MIGRATE_RECLAIMABLE時會跳過這些pageblock,當掃描完這個pageblock后有可移動的頁框時,會變為尾指針以pageblock為單位向前掃描可移動頁框數量的空閑頁框,但是在pageblock中也是從開始頁框向結束頁框進行掃描,最后會將前面的頁框內容復制到這些空閑頁框中。
需要注意,掃描可移動頁框是要先判斷pageblock的類型是否符合,符合的pageblock才在里面找可移動的頁框,當掃描了一個符合的pageblock后本次掃描可移動頁框會停止,轉到掃描空閑頁框。而掃描空閑頁框時也會根據pageblock進行掃描,只是從最后一個pageblock向前掃描,而在每個pageblock里面,也是從此pageblock開始頁框向pageblock結束頁框進行掃描。當需要的空閑頁框數量=掃描到的一個pageblock中可移動的頁框數量時,則會停止。
內存壓縮發生時機
現在再來說說什么時候會進行內存壓縮。它會在四個地方調用到:
- 內核從伙伴系統以min閥值獲取連續頁框,但是連續頁框又不足時。
- 當需要從指定地方獲取連續頁框,但是中間有頁框正在使用時。
- 因為內存短缺導致kswapd被喚醒時,在進行內存回收之后會進行內存壓縮。
- 將1寫入sysfs中的/vm/compact_memory時,系統會對所有zone進行內存壓縮。
而內存壓縮是一個相當耗費資源的事情,它並不會經常會執行,即使因為內存短缺導致代碼中經常調用到內存壓縮函數,它也會根據調用次數選擇性地忽略一些執行請求,見內存壓縮推遲。
系統判定是否執行內存壓縮的標准是
- 在分配頁框過程中,zone顯示是有足夠的空閑頁框供於本次分配的,但是伙伴系統鏈表中又沒有連續頁框段用於本次分配。原因就是過多分散的空閑頁框,它們沒辦法組成一塊連續頁框存放在伙伴系統的鏈表中。
- 在kswapd喚醒后會對zone的頁框閥值進行檢查,如果可用頁框少於高閥值則會進行內存回收,每次進行內存回收之后會進行內存壓縮。
即使滿足標准,也不一定會執行內存壓縮,具體見后面的內存壓縮推遲和compact_zone()函數。
內存壓縮結束時機
在內存壓縮中,一次zone的內存壓縮結束條件有三條:
- 可移動頁框掃描的位置是否已經超過了空閑頁框掃描的位置,超過則結束壓縮,並且會重置zone->compact_cached_free_pfn和zone->compact_cached_migrate_pfn,並且不是kswap時,會設置zone->compact_blockskip_flush為真
- zone的空閑頁框數量滿足了 (zone的low閥值 + 1<<order + zone的保留頁框) 條件。
- 判斷伙伴系統中是否有比order值大的空閑連續頁框塊,有則結束壓縮,如果order為-1,則忽略此條件
不過有例外,通過寫入到/proc/sys/vm/compact_memory進行強制內存壓縮的情況,則判斷條件只有第1條。對於zone來說,可移動頁掃描和空閑頁掃描交匯,也就是第一種情況時,才算是對zone進行了一次完整的內存壓縮,這個完整的內存壓縮並不代表一次內存壓縮就能實現,也有可能是對zone進行多次內存壓縮才達到的,因為每次內存壓縮結束時機還有另外兩種。當zone達到一次完整的內存壓縮時,會重置兩個掃描的起始為zone的第一個頁和最后一個頁,並且不是處於kswap中時,會設置zone->compact_blockskip_flush為真,這個zone->compact_blockskip_flush在kswapd准備睡眠時,會將zone的所有pageblock的PB_migrate_skip標志清除。
內存壓縮推遲
內存壓縮雖然是針對每個zone的,但是執行的時候傳入的是一個zonelist,這樣就會有一種情況,就是可能某個zone剛進行過內存壓縮,而系統因為內存不足又進行了內存壓縮,導致這個剛進行內存壓縮的zone又要執行內存壓縮,為了避免這種情況,內核會為每個zone做一個壓縮推遲計數,這個計數是每個zone都會有的,在struct zone里:
struct zone { ...... /* 這兩個用於推遲內存壓縮處理,只有當內存壓縮時使用的order大於compact_order_failed才會推遲 * 只有一種情況會重置這兩個值:在zone執行內存壓縮后,從此zone中分配到了內存,會重置 */ /* 用於判斷是否需要推遲,每次推遲會++,然后判斷是否超過 1UL << compact_defer_shift,超過了則要進行內存壓縮 */ unsigned int compact_considered; /* 用於定量推遲計數,主要用於內存壓縮分為compact_considered < compact_defer_shift和compact_considered >= compact_defer_shift兩種情況,當次管理區的內存壓縮成功后被置0,不會大於COMPACT_MAX_DEFER_SHIFT * 只有在同步和輕同步模式下進行內存壓縮后,zone的空閑頁框數量沒達到 (low閥值 + 1<<order + 保留內存) 時,才會增加此值 */ unsigned int compact_defer_shift; /* * 表示zone內存壓縮失敗時使用的最大order值,此值會影響是否推遲內存壓縮 * 當進行內存壓縮時,使用的order小於此值,則允許進行內存壓縮,否則記一次推遲 * 當內存壓縮完成時,此值為使用的order值+1,意思是假設大一級的order在壓縮中會失敗 * 當內存壓縮失敗時,此值則是等於order值,表示使用此大小的order值,有可能會導致失敗 */ int compact_order_failed; ...... }
- compact_considered:稱為內存壓縮推遲計數器,每次zone的內存壓縮推遲了,此值會+1
- compact_defer_shift:稱為內存壓縮推遲閥值,內存壓縮推遲計數器達到 1 << compact_defer_shift 后,就不能對zone進行內存壓縮推遲了。
- compact_order_failed:稱為內存壓縮失敗最大order值,記錄着此zone進行內存壓縮失敗時使用的最大的order值
當一個zone要進行內存壓縮時,首先會判斷本次壓縮需不需要推遲,如果本次內存壓縮使用的order值小於zone內存壓縮失敗最大order值時,不用進行推遲,可以直接進行內存壓縮;但是當order值大於zone內存壓縮失敗最大order值時,會增加內存壓縮推遲計數器,當內存壓縮推遲計數器未達到內存壓縮推遲閥值,則會跳過本次內存壓縮,如果達到了,那就需要進行內存壓縮。也就是當order小於zone內存壓縮失敗最大order值時,不用進行推遲,而order大於zone內存壓縮失敗最大order值時,才考慮是否進行推遲。
在對一個zone進行內存壓縮時,結果一般分為三種:
- 壓縮結束后,zone的空閑頁框數量達到了 (low閥值 + 1 << order + 保留的頁框數量),這種情況就稱為內存壓縮半成功
- 壓縮結束后,順利從zone中獲取到鏈入1 << order個連續頁框,這種情況稱為內存壓縮成功
- 壓縮結束后,zone的空閑頁框數量沒達到 (low閥值 + 1 << order + 保留的頁框數量),這種情況稱為內存壓縮失敗
當內存壓縮實現半成功時,如果使用的order大於等於zone的內存壓縮失敗最大order值,則將內存壓縮失敗最大order值設置為本次內存壓縮使用的order值+1。
當內存壓縮實現成功時,重置內存壓縮推遲計數器和內存壓縮推遲閥值計數為0並且如果使用的order大於等於zone的內存壓縮失敗最大order值,則將內存壓縮失敗最大order值設置為本次內存壓縮使用的order值+1。
當內存壓縮失敗時,在輕同步和同步模式下,會對內存壓縮推遲閥值計數+1,因為計算內存壓縮推遲量時,是使用1 << zone->compact_defer_shift計算的,所以這個+1,實際上是讓原來的推遲量*2。
如上,代碼中只有一個地方會讓zone重置推遲計數器,就是在內存壓縮完成后,從此zone中分配到2^order個連續頁框,那么就會重置zone->compact_considered和zone->compact_defer_shift為0,但zone->compact_order_failed並不會被重置也永遠不會被重置。
內存壓縮掃描起始位置與pageblock的跳過
在系統初始化過程中,就會將zone的可移動頁掃描起始位置設置為zone的第一個頁框,而空閑頁掃描起始位置設置為zone的最后一個頁框,這兩個數值保存在struct zone中的:
struct zone { ...... /* 以下兩個參數保存的是內存壓縮的兩個掃描的起始位置 */ /* 空閑頁框掃描起始位置,開始設置時是管理區的最后一個頁框 * 在內存壓縮掃描可以移動的頁時,從本次內存壓縮開始到此pageblock結束都沒有隔離出可移動頁時,會將此值設置為pageblock的最后一頁 * 此值默認是zone的結束頁框 */ unsigned long compact_cached_free_pfn; /* pfn where async and sync compaction migration scanner should start */ /* 0用於異步,1用於同步,用於保存管理區可移動頁框掃描起始位置 * 在內存壓縮掃描空閑頁時,從本次內存壓縮開始到此pageblock結束都沒有隔離出空閑頁時,會將此值設置為pageblock的最后一頁 * 此值默認是zone的開始頁框 */ unsigned long compact_cached_migrate_pfn[2]; ...... }
每次對zone進行內存壓縮,都是使用這兩個值初始化本次內存壓縮的可移動頁掃描起始位置和空閑頁掃描起始位置。
對於保存可移動頁掃描起始位置,同步和異步是分開保存到。這兩個值在初始化時會被設置為zone的結束頁框和開始頁框,之后從內存壓縮開始到pageblock結束時,都沒有隔離出頁的情況下,會被更新為pageblock結束頁框。
之前說了,內存是以一個一個連續的pageblock組織起來的,當進行內存壓縮時,一次掃描是以一個pageblock為單位,比如系統正在對zone進行內存壓縮,首先,會從可移動頁框開始位置向后掃描一個pageblock,得到一些可移動頁框,然后空閑頁框從開始位置向前掃描一個pageblock,得到一些空閑頁框,然后將可移動頁框移動到空閑頁框中,之后再繼續循環掃描。對一個pageblock進行掃描后,如果無法從此pageblock隔離出一個要求的頁框,這時候就會將此pageblock標記為跳過,主要通過設置pageblock在zone的pageblock位圖中的PB_migrate_skip標志實現的。而標記之后會有兩種情況:
- 本次內存壓縮在之前的pageblock已經隔離出了此種頁框(可移動頁/空閑頁),這種情況就是設置pageblock的PB_migrate_skip標記。
- 本次內存壓縮在之前的pageblock中沒有隔離出過此種頁框(可移動頁/空閑頁),說明之前的pageblock都被標記了跳過,這種情況不止設置pageblock的PB_migrate_skip標記,還會設置對於的內存壓縮掃描起始位置。
對於第二種情況,以掃描可移動頁為例子,本次內存壓縮可移動頁掃描是從zone的第一個頁框開始,掃描完一個pageblock后,沒有隔離出可移動頁框,則標記此pageblock的跳過標記PB_migrate_skip,然后將zone->compact_cached_migrate_pfn設置為此pageblock的結束頁框,這樣,在下次對此zone進行內存壓縮時,就會直接從此pageblock的下一個pageblock開始,把此pageblock跳過了。同理,對於空閑頁掃描也是一樣。如下圖:
在貼着掃描起始位置的pageblock被連續標記為跳過時,就會將掃描起始位置設置到這段連續被標記為跳過的pageblock的最后一個一頁,而當從pageblock隔離出需要頁框時,pageblock就不會被標記為跳過,之后又有pageblock被標記跳過時,就不會修正掃描起始位置了,因為中間有pageblock隔離出了頁框。本次壓縮結束后,如上圖,修正了可移動頁掃描起始位置和空閑頁掃描起始位置,當下一次對此zone進行內存壓縮時,則從這兩個位置開始:
可以看到,再次對此zone進行內存壓縮時,就會從修正后的掃描起始位置開始,並且掃描過程中會跳過被標記了跳過的pageblock。
如果一直這樣,那不是那些被標記為跳過的pageblock在進行內存壓縮時都會被跳過,然后一直不能夠對它們進行掃描?實際上並不是,當進行同步內存壓縮時,都會設置忽略pageblock的PB_migrate_skip標記,也就是會對跳過的pageblock進行掃描。但是僅僅只有在同步內存壓縮時才對跳過的pageblock進行掃描也不行,畢竟同步內存壓縮只是一些特殊情況下才會使用。所以,在一些情況下,內核會將zone的所有pageblock的PB_migrate_skip清除,也就是之后的內存壓縮掃描,又會從最開始的狀態開始進行,有以下兩種情況會發生,如下:
- 在可移動頁掃描和空閑頁掃描碰頭時,會設置zone->compact_blockskip_flush標志,此標志會導致kswapd准備睡眠時,對此zone的所有pageblock清除PB_migrate_skip
- 在非kswapd調用中,如果此zone的推遲次數達到最大值時(zone->compact_defer_shift == COMPACT_MAX_DEFER_SHIFT並且zone->compact_considered >= 1UL << zone->compact_defer_shift)導致的內存壓縮,則清除zone所有pageblock的PB_migrate_skip
第一種情況,這個對zone的所有pageblock的PB_migrate_skip清除的工作是異步的,而第二種情況,則是同步的。
總結來說,就是zone完成了一次完整內存壓縮(兩個掃描相會)和此zone內存壓縮推遲次數達到最大值這兩種情況下,會清除zone所有pageblock的PB_migrate_skip。而清除時,都會將兩個掃描起始位置重置為zone的開始頁框和結束頁框位置。
實現代碼
先看看內存壓縮控制結構struct compact_control,當需要進行內存壓縮時,總是需要初始化一個這個結構:
struct compact_control { /* 掃描到的空閑頁的頁的鏈表 */ struct list_head freepages; /* List of free pages to migrate to */ /* 掃描到的可移動的頁的鏈表 */ struct list_head migratepages; /* List of pages being migrated */ /* 空閑頁鏈表中的頁數量 */ unsigned long nr_freepages; /* Number of isolated free pages */ /* 可移動頁鏈表中的頁數量 */ unsigned long nr_migratepages; /* Number of pages to migrate */ /* 空閑頁框掃描所在頁框號 */ unsigned long free_pfn; /* isolate_freepages search base */ /* 可移動頁框掃描所在頁框號 */ unsigned long migrate_pfn; /* isolate_migratepages search base */ /* 內存壓縮使用的模式: 同步,輕同步,異步 */ enum migrate_mode mode; /* Async or sync migration mode */ /* 是否忽略pageblock的PB_migrate_skip標志對需要跳過的pageblock進行掃描 ,並且也不會對pageblock設置跳過 * 只有兩種情況會使用 * 1.調用alloc_contig_range()嘗試分配一段指定了開始頁框號和結束頁框號的連續頁框時; * 2.通過寫入1到sysfs中的/vm/compact_memory文件手動實現同步內存壓縮。 */ bool ignore_skip_hint; /* Scan blocks even if marked skip */ /* 本次內存壓縮是否隔離到了空閑頁框,會影響zone的空閑頁掃描起始位置 */ bool finished_update_free; /* True when the zone cached pfns are * no longer being updated */ /* 本次內存壓縮是否隔離到了可移動頁框,會影響zone的可移動頁掃描起始位置 */ bool finished_update_migrate; /* 申請內存時需要的頁框的order值 */ int order; /* order a direct compactor needs */ const gfp_t gfp_mask; /* gfp mask of a direct compactor */ /* 掃描的管理區 */ struct zone *zone; /* 保存結果,比如異步模式下是否因為需要阻塞而結束了本次內存壓縮 */ int contended; /* Signal need_sched() or lock * contention detected during * compaction */ };
結構體中每個成員變量的作用都在注釋中寫明了。
實際上無論喚醒kswapd執行內存壓縮還是連續頁框不足執行內存壓縮,它們的入口都是alloc_pages()函數,因為kswapd不是間斷性自動喚醒,而是在分配頁框時頁框不足的情況下被主動喚醒,在內存足夠的情況下,kswapd是不會被喚醒的,而分配頁框的函數入口就是alloc_pages(),會在此函數里面判斷頁框是否足夠。所以從alloc_pages往下跟,可以看到內存壓縮的代碼主要函數是try_to_compact_pages(),在這個函數中,需要傳入一個zonelist,然后對zonelist中的每個zone都進行內存壓縮:
/* 嘗試zonelist中的每個zone進行內存壓縮 * order: 2的次方,如果是分配時調用到,這個就是分配時希望獲取的order,如果是通過寫入/proc/sys/vm/compact_memory文件進行強制內存壓縮,order就是-1 */ unsigned long try_to_compact_pages(struct zonelist *zonelist, int order, gfp_t gfp_mask, nodemask_t *nodemask, enum migrate_mode mode, int *contended, struct zone **candidate_zone) { enum zone_type high_zoneidx = gfp_zone(gfp_mask); /* 表示能夠使用文件系統的IO操作 */ int may_enter_fs = gfp_mask & __GFP_FS; /* 表示可以使用磁盤的IO操作 */ int may_perform_io = gfp_mask & __GFP_IO; struct zoneref *z; struct zone *zone; int rc = COMPACT_DEFERRED; int alloc_flags = 0; int all_zones_contended = COMPACT_CONTENDED_LOCK; /* init for &= op */ *contended = COMPACT_CONTENDED_NONE; /* Check if the GFP flags allow compaction */ /* 如果order = 0或者不允許使用文件系統IO和磁盤IO,則跳過本次壓縮,因為不使用IO有可能導致死鎖 */ if (!order || !may_enter_fs || !may_perform_io) return COMPACT_SKIPPED; #ifdef CONFIG_CMA /* 在啟動了CMA的情況下,如果標記了需要的內存為MIGRATE_MOVABLE,則添加一個ALLOC_CMA標志 */ if (gfpflags_to_migratetype(gfp_mask) == MIGRATE_MOVABLE) alloc_flags |= ALLOC_CMA; #endif /* Compact each zone in the list */ /* 遍歷zonelist中的每一個zone */ for_each_zone_zonelist_nodemask(zone, z, zonelist, high_zoneidx, nodemask) { int status; int zone_contended; /* 檢查管理區設置中是否需要跳過此次壓縮,當order < zone->compact_order_failed時是不需要跳過的 * 判斷標准是: * zone->compact_considered是否小於1UL << zone->compact_defer_shift * 小於則推遲,並且zone->compact_considered++,也就是這個函數會主動去推遲此管理區的內存壓縮 * 本次請求的order值小於之前失敗時的order值,那這次壓縮必須要進行 * zone->compact_considered和zone->compact_defer_shift會只有在內存壓縮完成后,從此zone獲取到了連續的1 << order個頁框的情況下會重置為0。 */ if (compaction_deferred(zone, order)) continue; /* 對遍歷到的zone進行內存壓縮 */ status = compact_zone_order(zone, order, gfp_mask, mode, &zone_contended); rc = max(status, rc); all_zones_contended &= zone_contended; /* If a normal allocation would succeed, stop compacting */ /* 判斷壓縮后此zone 分配1 << order個頁框后剩余的頁框數量 是否 大於 此zone的低閥值 + 保留的頁框數量 */ if (zone_watermark_ok(zone, order, low_wmark_pages(zone), 0, alloc_flags)) { *candidate_zone = zone; /* 當zone內存滿足low閥值 + (1 << order) + 保留的內存 時,則將compact_order_failed設置為本次壓縮的order + 1 * 因為這里還不確定內存壓縮是否成功了,只是此zone的剩余頁框滿足了要求 */ compaction_defer_reset(zone, order, false); /* 異步情況下需要被調度時會設置 */ if (zone_contended == COMPACT_CONTENDED_SCHED) *contended = COMPACT_CONTENDED_SCHED; /* 當zone中空閑內存達到 low閥值 + (1 << order) + 保留的內存 時,就不對下面的zone進行內存壓縮了 */ goto break_loop; } /* 以下的代碼就是zone本次內存壓縮后,剩余頁框數量還是沒達到 zone的low閥值 + 本次需要分配的頁框數量 + zone的保留頁框數量 */ /* 如果是同步壓縮或者輕同步壓縮,則增加推遲計數器閥值zone->compact_defer_shift */ if (mode != MIGRATE_ASYNC) { /* 提高內存壓縮計數器的閥值,zone的內存壓縮計數器閥值 ,也就是只有同步壓縮會增加推遲計數器的閥值 * 重置zone->compact_considered = 0 * 如果zone->compact_defer_shift < COMPACT_MAX_DEFER_SHIFT,那么zone->compact_defer_shift++ * 如果order < zone->compact_order_failed,那么zone->compact_order_failed = order */ defer_compaction(zone, order); } if ((zone_contended == COMPACT_CONTENDED_SCHED) || fatal_signal_pending(current)) { *contended = COMPACT_CONTENDED_SCHED; goto break_loop; } continue; break_loop: all_zones_contended = 0; break; } if (rc > COMPACT_SKIPPED && all_zones_contended) *contended = COMPACT_CONTENDED_LOCK; return rc; }
在此函數中,遍歷zonlist中的每個zone,對每個zone都進行內存壓縮處理,在內存壓縮處理中,第一件首要事情就是判斷此zone的內存壓縮是否需要推遲,不需要推遲可以進行內存壓縮的兩個情況是:
- 本次內存壓縮使用的order值小於zone->compact_order_failed。
- 如果order值大於zone->compact_order_failed,那么對zone的內存壓縮推遲計數器++,如果zone的內存壓縮推遲計數器++后數值大於等於了(1 << zone的內存壓縮最大推遲計數),那么也會對zone進行內存壓縮。但是這種情況會清除zone所有pageblock的PB_migrate_skip標志和重置掃描起始位置。
在上述兩種情況下,可以對此zone進行內存壓縮,之后,compact_zone_order(),這個函數里主要初始化一個struct compact_control結構體,然后調用compact_zone():
static unsigned long compact_zone_order(struct zone *zone, int order, gfp_t gfp_mask, enum migrate_mode mode, int *contended) { unsigned long ret; struct compact_control cc = { /* 壓縮結束后空閑頁框數量 */ .nr_freepages = 0, /* 壓縮結束后移動的頁框數量 */ .nr_migratepages = 0, .order = order, /* 表示需要移動的頁框類型,有movable和reclaimable兩種,可以同時設置 */ .gfp_mask = gfp_mask, /* 管理區 */ .zone = zone, /* 同步或異步 */ .mode = mode, }; /* 初始化一個空閑頁框鏈表頭 */ INIT_LIST_HEAD(&cc.freepages); /* 初始化一個movable頁框鏈表頭 */ INIT_LIST_HEAD(&cc.migratepages); /* 進行內存壓縮 */ ret = compact_zone(zone, &cc); VM_BUG_ON(!list_empty(&cc.freepages)); VM_BUG_ON(!list_empty(&cc.migratepages)); *contended = cc.contended; return ret; }
這里面又調用了compact_zone(),這個函數里首先會在此判斷是否進行內存壓縮,有三種情況:
- COMPACT_PARTICAL: 此zone內存足夠用於分配要求的2^order個頁框,不用進行內存壓縮。
- COMPACT_SKIPPED: 此zone內存不足以進行內存壓縮,判斷條件是此zone的空閑頁框數量少於 zone的低閥值 + (2 << order)。
- COMPACT_CONTINUE: 此zone可以進行內存壓縮。
所以,對一個zone能否進行內存壓縮有兩個判斷,一個是是否需要推遲的判斷,一個是zone的內存頁數量是否滿足進行內存壓縮。
判斷完zone能否進行內存壓縮后,還需要判斷是否要重置所有pageblock的PB_migrate_skip和掃描起始位置,只有當不處於kswapd內存壓縮時,並且zone的內存壓縮推遲次數超過了最大值的情況下,才會重置。之后會初始化可移動頁框掃描和空閑頁框掃描的起始位置,這個位置就是使用zone->compact_cached_migrate_pfn和zone->compact_cached_free_pfn決定,需要注意同步和異步的可移動頁掃描使用的是不同的位置。之后如上面所說,循環掃描pageblock,對每個pageblock進行可移動頁的掃描和空閑頁的掃描,將可移動頁的數據和頁描述符復制到空閑頁中,最后就將已經完成移動的可移動頁釋放掉,如下:
/* 對zone進行內存壓縮主要實現函數 */ static int compact_zone(struct zone *zone, struct compact_control *cc) { int ret; /* 管理區開始頁框號 */ unsigned long start_pfn = zone->zone_start_pfn; /* 管理區結束頁框號 */ unsigned long end_pfn = zone_end_pfn(zone); /* 獲取可進行移動的頁框類型(__GFP_RECLAIMABLE、__GFP_MOVABLE) */ const int migratetype = gfpflags_to_migratetype(cc->gfp_mask); /* 同步還是異步 * 同步為1,異步為0 * 輕同步和同步都是同步 */ const bool sync = cc->mode != MIGRATE_ASYNC; /* 根據傳入的cc->order判斷此次壓縮是否能夠進行,主要是因為壓縮需要部分內存,這里面會判斷內存是否足夠 */ ret = compaction_suitable(zone, cc->order); switch (ret) { /* 內存足夠用於分配,所以此次壓縮直接跳過 */ case COMPACT_PARTIAL: /* 內存數量不足以進行內存壓縮 */ case COMPACT_SKIPPED: /* Compaction is likely to fail */ return ret; /* 可以進行內存壓縮 */ case COMPACT_CONTINUE: /* Fall through to compaction */ ; } /* 如果不是在kswapd線程中並且此zone的內存壓縮推遲次數超過了最大推遲次數 */ if (compaction_restarting(zone, cc->order) && !current_is_kswapd()) /* 只有不是在kswapd線程中並且此zone的內存壓縮推遲次數超過了最大推遲次數的時候才會執行如下操作 * zone->compact_cached_migrate_pfn[sync/async]設置為此zone的起始頁框,compact_cached_free_pfn設置為此zone的結束頁框 * zone->compact_blockskip_flush = false * 將zone中所有pageblock的PB_migrate_skip清空 */ __reset_isolation_suitable(zone); /* 將可移動頁框掃描起始頁框號設為zone->compact_cached_migrate_pfn[sync/async] */ cc->migrate_pfn = zone->compact_cached_migrate_pfn[sync]; /* 空閑頁框掃描起始頁框號設置為zone->compact_cached_free_pfn */ cc->free_pfn = zone->compact_cached_free_pfn; /* 檢查cc->free_pfn,如果空閑頁框掃描起始頁框不在zone的范圍內,則將空閑頁框掃描起始頁框設置為zone的最后一個頁框 * 並且也會將zone->compact_cached_free_pfn設置為zone的最后一個頁框 */ if (cc->free_pfn < start_pfn || cc->free_pfn > end_pfn) { cc->free_pfn = end_pfn & ~(pageblock_nr_pages-1); zone->compact_cached_free_pfn = cc->free_pfn; } /* 同上,檢查cc->migrate_pfn,如果可移動頁框掃描起始頁框不在zone的范圍內,則將可移動頁框掃描起始頁框設置為zone的第一個頁框 * 並且也會將zone->compact_cached_free_pfn設置為zone的第一個頁框 */ if (cc->migrate_pfn < start_pfn || cc->migrate_pfn > end_pfn) { cc->migrate_pfn = start_pfn; zone->compact_cached_migrate_pfn[0] = cc->migrate_pfn; zone->compact_cached_migrate_pfn[1] = cc->migrate_pfn; } trace_mm_compaction_begin(start_pfn, cc->migrate_pfn, cc->free_pfn, end_pfn); /* 將處於pagevec中的頁都放回原本所屬的lru中,這一步很重要 */ migrate_prep_local(); /* 判斷是否結束本次內存壓縮 * 1.可移動頁框掃描的位置是否已經超過了空閑頁框掃描的位置,超過則結束壓縮,並且會重置zone->compact_cached_free_pfn和zone->compact_cached_migrate_pfn * 2.判斷zone的空閑頁框數量是否達到標准,如果沒達到zone的low閥值標准則繼續 * 3.判斷伙伴系統中是否有比order值大的空閑連續頁框塊,有則結束壓縮 * 如果是管理員寫入到/proc/sys/vm/compact_memory進行強制內存壓縮的情況,則判斷條件只有第1條 */ while ((ret = compact_finished(zone, cc, migratetype)) == COMPACT_CONTINUE) { int err; /* 將可移動頁(MOVABLE和CMA和RECLAIMABLE)從zone->lru隔離出來,存到cc->migratepages這個鏈表,一個一個pageblock進行掃描,當一個pageblock掃描成功獲取到可移動頁后就返回 * 一次掃描最多32*1024個頁框 */ /* 異步不處理RECLAIMABLE頁 */ switch (isolate_migratepages(zone, cc)) { case ISOLATE_ABORT: /* 失敗,把這些頁放回到lru或者原來的地方 */ ret = COMPACT_PARTIAL; putback_movable_pages(&cc->migratepages); cc->nr_migratepages = 0; goto out; case ISOLATE_NONE: continue; case ISOLATE_SUCCESS: ; } /* 將隔離出來的頁進行遷移,如果到這里,cc->migratepages中最多也只有一個pageblock的頁框數量,並且這些頁框都是可移動的 * 空閑頁框會在compaction_alloc中獲取 * 也就是把隔離出來的一個pageblock中可移動頁進行移動 */ err = migrate_pages(&cc->migratepages, compaction_alloc, compaction_free, (unsigned long)cc, cc->mode, MR_COMPACTION); trace_mm_compaction_migratepages(cc->nr_migratepages, err, &cc->migratepages); /* All pages were either migrated or will be released */ /* 設置所有可移動頁框為0 */ cc->nr_migratepages = 0; if (err) { /* 將剩余的可移動頁框返回原來的位置 */ putback_movable_pages(&cc->migratepages); if (err == -ENOMEM && cc->free_pfn > cc->migrate_pfn) { ret = COMPACT_PARTIAL; goto out; } } } out: /* 將剩余的空閑頁框放回伙伴系統 */ cc->nr_freepages -= release_freepages(&cc->freepages); VM_BUG_ON(cc->nr_freepages != 0); trace_mm_compaction_end(ret); return ret; }
隔離可移動頁框
每次進行隔離可移動頁框是以一個pageblock為單位,也就是從一個pageblock中將可以移動頁進行隔離,最多也就只能隔離出一個pageblock中的所有頁框,主要實現函數為isolate_migratepages():
/* 從cc->migrate_pfn(保存的是掃描可移動頁框指針所在的頁框號)開始到第一個獲取到可移動頁框的pageblock結束,獲取可移動頁框,並放入到cc->migratepages */ static isolate_migrate_t isolate_migratepages(struct zone *zone, struct compact_control *cc) { unsigned long low_pfn, end_pfn; struct page *page; /* 保存同步/異步方式,只有異步的情況下能進行移動頁框ISOLATE_ASYNC_MIGRATE */ const isolate_mode_t isolate_mode = (cc->mode == MIGRATE_ASYNC ? ISOLATE_ASYNC_MIGRATE : 0); /* 掃描起始頁框 */ low_pfn = cc->migrate_pfn; /* Only scan within a pageblock boundary */ /* 以pageblock大小對齊 */ end_pfn = ALIGN(low_pfn + 1, pageblock_nr_pages); for (; end_pfn <= cc->free_pfn; low_pfn = end_pfn, end_pfn += pageblock_nr_pages) { /* 由於需要掃描很多頁框,所以這里做個檢查,執行時間過長則睡眠,一般是32個1024頁框休眠一下,異步的情況還需要判斷運行進程是否需要被調度 */ if (!(low_pfn % (SWAP_CLUSTER_MAX * pageblock_nr_pages)) && compact_should_abort(cc)) break; /* 獲取第一個頁框,需要檢查是否屬於此zone */ page = pageblock_pfn_to_page(low_pfn, end_pfn, zone); if (!page) continue; /* If isolation recently failed, do not retry */ /* 獲取頁框的PB_migrate_skip標志,如果設置了則跳過這個1024個頁框 */ if (!isolation_suitable(cc, page)) continue; /* 異步情況,如果不是MIGRATE_MOVABLE或MIGRATE_CMA類型則跳過這段頁框塊 */ /* 異步不處理RECLAIMABLE的頁 */ if (cc->mode == MIGRATE_ASYNC && !migrate_async_suitable(get_pageblock_migratetype(page))) continue; /* Perform the isolation */ /* 執行完隔離,將low_pfn到end_pfn中正在使用的頁框從zone->lru中取出來,返回的是可移動頁掃描掃描到的頁框號 * 而UNMOVABLE類型的頁框是不會處於lru鏈表中的,所以所有不在lru鏈表中的頁都會被跳過 * 返回的是掃描到的最后的頁 */ low_pfn = isolate_migratepages_block(cc, low_pfn, end_pfn, isolate_mode); if (!low_pfn || cc->contended) return ISOLATE_ABORT; /* 跳出,說明這里如果成功只會掃描一個pageblock */ break; } /* 統計,里面會再次遍歷cc中所有可移動的頁,判斷它們是RECLAIMABLE還是MOVABLE的頁 */ acct_isolated(zone, cc); /* 可移動頁掃描到的頁框修正 */ cc->migrate_pfn = (end_pfn <= cc->free_pfn) ? low_pfn : cc->free_pfn; return cc->nr_migratepages ? ISOLATE_SUCCESS : ISOLATE_NONE; }
注意上面代碼里循環最后的break,當獲取到可移動頁框時,就break跳出循環。之后主要看isolate_migratepages_block(),里面是針對一個pageblock的掃描:
/* 將一個pageblock中所有可以移動的頁框隔離出來 */ static unsigned long isolate_migratepages_block(struct compact_control *cc, unsigned long low_pfn, unsigned long end_pfn, isolate_mode_t isolate_mode) { struct zone *zone = cc->zone; unsigned long nr_scanned = 0, nr_isolated = 0; /* 待移動的頁框鏈表 */ struct list_head *migratelist = &cc->migratepages; struct lruvec *lruvec; unsigned long flags = 0; bool locked = false; struct page *page = NULL, *valid_page = NULL; /* 檢查isolated是否小於LRU鏈表的(inactive + active) / 2,超過了則表示已經將許多頁框隔離出來 */ while (unlikely(too_many_isolated(zone))) { /* async migration should just abort */ if (cc->mode == MIGRATE_ASYNC) return 0; /* 進行100ms的休眠,等待設備沒那么繁忙 */ congestion_wait(BLK_RW_ASYNC, HZ/10); if (fatal_signal_pending(current)) return 0; } /* 如果是異步調用,並且當前進程需要調度的話,返回真 */ if (compact_should_abort(cc)) return 0; /* Time to isolate some pages for migration */ /* 遍歷每一個頁框 */ for (; low_pfn < end_pfn; low_pfn++) { /* 這里會釋放掉zone->lru_lock這個鎖 */ if (!(low_pfn % SWAP_CLUSTER_MAX) && compact_unlock_should_abort(&zone->lru_lock, flags, &locked, cc)) break; if (!pfn_valid_within(low_pfn)) continue; /* 掃描次數++ */ nr_scanned++; /* 根據頁框號獲取頁描述符 */ page = pfn_to_page(low_pfn); /* 設置valid_page */ if (!valid_page) valid_page = page; /* 檢查此頁是否處於伙伴系統中,主要是通過page->_mapcount判斷,如果在伙伴系統中,則跳過這塊空閑內存 */ if (PageBuddy(page)) { /* 獲取這個頁開始的order次方個頁框為伙伴系統的一塊內存 */ unsigned long freepage_order = page_order_unsafe(page); if (freepage_order > 0 && freepage_order < MAX_ORDER) low_pfn += (1UL << freepage_order) - 1; continue; } /* 以下處理此頁不在伙伴系統中的情況,表明此頁是在使用的頁*/ /* 如果頁不處於lru中的處理,isolated的頁是不處於lru中的,用於balloon的頁也不處於lru中? * 可移動的頁都會在LRU中,不在LRU中的頁都會被跳過,這里就把UNMOVABLE進行跳過 */ if (!PageLRU(page)) { if (unlikely(balloon_page_movable(page))) { if (balloon_page_isolate(page)) { /* Successfully isolated */ goto isolate_success; } } continue; } /* 如果此頁是透明大頁的處理,也是跳過。透明大頁是在系統活動時可以實時配置,不需要重啟生效 */ if (PageTransHuge(page)) { if (!locked) low_pfn = ALIGN(low_pfn + 1, pageblock_nr_pages) - 1; else low_pfn += (1 << compound_order(page)) - 1; continue; } /* 如果是一個匿名頁,並且被引用次數大於page->_mapcount,則跳過此頁,注釋說此頁很有可能被鎖定在內存中不允許換出,但不知道如何判斷的 */ if (!page_mapping(page) && page_count(page) > page_mapcount(page)) continue; /* If we already hold the lock, we can skip some rechecking */ /* 檢查是否有上鎖,這個鎖是zone->lru_lock */ if (!locked) { locked = compact_trylock_irqsave(&zone->lru_lock, &flags, cc); if (!locked) break; /* 沒上鎖的情況,需要檢查是否處於LRU中 */ /* Recheck PageLRU and PageTransHuge under lock */ if (!PageLRU(page)) continue; /* 如果在lru中,檢查是否是大頁,做個對齊,防止low_pfn不是頁首 */ if (PageTransHuge(page)) { low_pfn += (1 << compound_order(page)) - 1; continue; } } lruvec = mem_cgroup_page_lruvec(page, zone); /* Try isolate the page */ /* 將此頁從lru中隔離出來 */ if (__isolate_lru_page(page, isolate_mode) != 0) continue; VM_BUG_ON_PAGE(PageTransCompound(page), page); /* Successfully isolated */ /* 如果在cgroup的lru緩沖區,則將此頁從lru緩沖區中拿出來 */ del_page_from_lru_list(page, lruvec, page_lru(page)); isolate_success: /* 隔離成功,此頁已不處於lru中 */ cc->finished_update_migrate = true; /* 將此頁加入到本次壓縮需要移動頁鏈表中 */ list_add(&page->lru, migratelist); /* 需要移動的頁框數量++ */ cc->nr_migratepages++; /* 隔離數量++ */ nr_isolated++; /* Avoid isolating too much */ /* COMPACT_CLUSTER_MAX代表每次內存壓縮所能移動的最大頁框數量 */ if (cc->nr_migratepages == COMPACT_CLUSTER_MAX) { ++low_pfn; break; } }
if (unlikely(low_pfn > end_pfn)) low_pfn = end_pfn; /* 解鎖 */ if (locked) spin_unlock_irqrestore(&zone->lru_lock, flags); /* 如果全部的頁框塊都掃描過了,並且沒有隔離任何一個頁,則標記最后這個頁所在的pageblock為PB_migrate_skip,然后 * if (pfn > zone->compact_cached_migrate_pfn[0]) zone->compact_cached_migrate_pfn[0] = pfn; if (cc->mode != MIGRATE_ASYNC && pfn > zone->compact_cached_migrate_pfn[1]) zone->compact_cached_migrate_pfn[1] = pfn; * */ if (low_pfn == end_pfn) update_pageblock_skip(cc, valid_page, nr_isolated, true); trace_mm_compaction_isolate_migratepages(nr_scanned, nr_isolated); /* 統計 */ count_compact_events(COMPACTMIGRATE_SCANNED, nr_scanned); if (nr_isolated) count_compact_events(COMPACTISOLATED, nr_isolated); return low_pfn; }
到這里,已經完成了從一個pageblock獲取可移動頁框,並放入struct compact_control中的migratepages鏈表中。從之前的代碼看,當調用isolate_migratepages()將一個pageblock的可移動頁隔離出來之后,會調用到migrate_pages()進行可移動頁框的移動,之后就是詳細說明此函數。
可移動頁框的移動
我們先看看migrate_pages()函數原型:
static inline int migrate_pages(struct list_head *l, new_page_t new, free_page_t free, unsigned long private, enum migrate_mode mode, int reason)
比較重要的兩個參數是
- new_page_t new: 是一個函數指針,指向獲取空閑頁框的函數
- free_page_t free: 也是一個函數指針,指向釋放空閑頁框的函數
我們要先看看這兩個指針指向的函數,這兩個函數指針分別指向compaction_alloc()和compaction_free(),compaction_alloc()是我們主要分析的函數,如下:
static struct page *compaction_alloc(struct page *migratepage, unsigned long data, int **result) { /* 獲取cc */ struct compact_control *cc = (struct compact_control *)data; struct page *freepage; /* * Isolate free pages if necessary, and if we are not aborting due to * contention. */ /* 如果cc中的空閑頁框鏈表為空 */ if (list_empty(&cc->freepages)) { /* 並且cc->contended沒有記錄錯誤代碼 */ if (!cc->contended) /* 從cc->free_pfn開始向前獲取空閑頁 */ isolate_freepages(cc); if (list_empty(&cc->freepages)) return NULL; } /* 從cc->freepages鏈表中拿出一個空閑page */ freepage = list_entry(cc->freepages.next, struct page, lru); list_del(&freepage->lru); cc->nr_freepages--; /* 返回空閑頁框 */ return freepage; }
代碼很簡單,主要還是里面的isolate_freepages()函數,在這個函數中,會從cc->free_pfn開始向前掃描空閑頁框,但是注意以pageblock向前掃描,但是在pageblock內部是從前向后掃描的,最后遇到cc->migrate_pfn后或者cc->nr_freepages >= cc->nr_migratepages的情況下會停止,如下:
/* 隔離出空閑頁框 */ static void isolate_freepages(struct compact_control *cc) { struct zone *zone = cc->zone; struct page *page; unsigned long block_start_pfn; /* start of current pageblock */ unsigned long isolate_start_pfn; /* exact pfn we start at */ unsigned long block_end_pfn; /* end of current pageblock */ unsigned long low_pfn; /* lowest pfn scanner is able to scan */ int nr_freepages = cc->nr_freepages; struct list_head *freelist = &cc->freepages; /* 獲取開始掃描頁框所在的pageblock,並且設置為此pageblock的最后一個頁框或者管理區最后一個頁框 */ isolate_start_pfn = cc->free_pfn; block_start_pfn = cc->free_pfn & ~(pageblock_nr_pages-1); block_end_pfn = min(block_start_pfn + pageblock_nr_pages, zone_end_pfn(zone)); /* 按pageblock_nr_pages對齊,low_pfn保存的是可遷移頁框掃描所在的頁框號,但是這里有可能migrate_pfn == free_pfn */ low_pfn = ALIGN(cc->migrate_pfn + 1, pageblock_nr_pages); /* 開始掃描空閑頁框,從管理區最后一個pageblock向migrate_pfn所在的pageblock掃描 * block_start_pfn是pageblock開始頁框號 * block_end_pfn是pageblock結束頁框號 */ /* 循環條件, * 掃描到low_pfn所在pageblokc或者其后一個pageblock,low_pfn是low_pfn保存的是可遷移頁框掃描所在的頁框號,並按照pageblock_nr_pages對齊。 * 並且cc中可移動的頁框數量多於cc中空閑頁框的數量。由於隔離可移動頁是以一個一個pageblock為單位的,所以剛開始時更多是判斷cc->nr_migratepages > nr_freepages來決定是否結束 * 當掃描到可移動頁框掃描所在的pageblock后,則會停止 */ for (; block_start_pfn >= low_pfn && cc->nr_migratepages > nr_freepages; block_end_pfn = block_start_pfn, block_start_pfn -= pageblock_nr_pages, isolate_start_pfn = block_start_pfn) { unsigned long isolated; if (!(block_start_pfn % (SWAP_CLUSTER_MAX * pageblock_nr_pages)) && compact_should_abort(cc)) break; /* 檢查block_start_pfn和block_end_pfn,如果沒問題,返回block_start_pfn所指的頁描述符,也就是pageblock第一頁描述符 */ page = pageblock_pfn_to_page(block_start_pfn, block_end_pfn, zone); if (!page) continue; /* Check the block is suitable for migration */ /* 判斷是否能夠用於遷移頁框 * 判斷條件1: 如果處於伙伴系統中,它所代表的這段連續頁框的order值必須小於pageblock的order值 * 判斷條件2: 此pageblock必須為MIGRATE_MOVABLE或者MIGRATE_CMA類型,而為MIGRATE_RECLAIMABLE類型的pageblock則跳過 */ if (!suitable_migration_target(page)) continue; /* If isolation recently failed, do not retry */ /* 檢查cc中是否標記了即使pageblock標記了跳過也對pageblock進行掃描,並且檢查此pageblock是否被標記為跳過 */ if (!isolation_suitable(cc, page)) continue; /* Found a block suitable for isolating free pages from. */ /* 掃描從isolate_start_pfn到block_end_pfn的空閑頁框,並把它們放入到freelist中,返回此pageblock中總共獲得的空閑頁框數量 * 第一輪掃描可能會跳過,應該第一次isolate_start_pfn是等於zone最后一個頁框的 */ isolated = isolate_freepages_block(cc, &isolate_start_pfn, block_end_pfn, freelist, false); /* 統計freelist中空閑頁框數量 */ nr_freepages += isolated; /* 下次循環開始的頁框 */ cc->free_pfn = (isolate_start_pfn < block_end_pfn) ? isolate_start_pfn : block_start_pfn - pageblock_nr_pages; /* 設置cc->finished_update_free為true,即表明此次cc獲取到了空閑頁框鏈表 */ if (isolated) cc->finished_update_free = true; /* 檢查contended,此用於表明是否需要終止 */ if (cc->contended) break; } /* split_free_page does not map the pages */ /* 設置頁表項,設置為內核使用 */ map_pages(freelist); /* 保證free_pfn不超過migrate_pfn */ if (block_start_pfn < low_pfn) cc->free_pfn = cc->migrate_pfn; cc->nr_freepages = nr_freepages; }
對於單個pageblock里的操作,就在isolate_freepages_block()中:
/* 掃描從start_pfn到end_pfn的空閑頁框,一般都是一個pageblock的開始頁框ID和結束頁框ID,並把它們放入到freelist中,返回此pageblock中總共獲得的空閑頁框數量 */ static unsigned long isolate_freepages_block(struct compact_control *cc, unsigned long *start_pfn, unsigned long end_pfn, struct list_head *freelist, bool strict) { int nr_scanned = 0, total_isolated = 0; struct page *cursor, *valid_page = NULL; unsigned long flags = 0; bool locked = false; unsigned long blockpfn = *start_pfn; cursor = pfn_to_page(blockpfn); /* Isolate free pages. */ /* 從pageblock的start向end進行掃描 */ for (; blockpfn < end_pfn; blockpfn++, cursor++) { int isolated, i; /* 當前頁框 */ struct page *page = cursor; if (!(blockpfn % SWAP_CLUSTER_MAX) && compact_unlock_should_abort(&cc->zone->lock, flags, &locked, cc)) break; nr_scanned++; /* 檢查此頁框號是否正確 */ if (!pfn_valid_within(blockpfn)) goto isolate_fail; /* valid_page是開始掃描的頁框 */ if (!valid_page) valid_page = page; /* 檢查此頁是否在伙伴系統中,不在說明是正在使用的頁框,則跳過 */ if (!PageBuddy(page)) goto isolate_fail; /* 獲取鎖 */ if (!locked) { locked = compact_trylock_irqsave(&cc->zone->lock, &flags, cc); if (!locked) break; /* Recheck this is a buddy page under lock */ if (!PageBuddy(page)) goto isolate_fail; } /* Found a free page, break it into order-0 pages */ /* 將page開始的連續空閑頁框拆分為連續的單個頁框,返回數量,order值會在page的頁描述符中,這里有可能會設置pageblock的類型 */ isolated = split_free_page(page); /* 更新總共隔離的空閑頁框數量 */ total_isolated += isolated; /* 將isolated數量個單個頁框放入freelist中 */ for (i = 0; i < isolated; i++) { list_add(&page->lru, freelist); page++; } /* If a page was split, advance to the end of it */ /* 跳過這段連續空閑頁框,因為上面把這段空閑頁框全部加入到了freelist中 */ if (isolated) { blockpfn += isolated - 1; cursor += isolated - 1; continue; } isolate_fail: if (strict) break; else continue; } /* Record how far we have got within the block */ *start_pfn = blockpfn; trace_mm_compaction_isolate_freepages(nr_scanned, total_isolated); if (strict && blockpfn < end_pfn) total_isolated = 0; /* 如果占有鎖則釋放掉 */ if (locked) spin_unlock_irqrestore(&cc->zone->lock, flags); /* Update the pageblock-skip if the whole pageblock was scanned */ /* 掃描完了此pageblock,如果此pageblock中沒有隔離出空閑頁框,則標記此pageblock為跳過 */ if (blockpfn == end_pfn) update_pageblock_skip(cc, valid_page, total_isolated, false); /* 統計 */ count_compact_events(COMPACTFREE_SCANNED, nr_scanned); if (total_isolated) count_compact_events(COMPACTISOLATED, total_isolated); /* 返回總共獲得的空閑頁框 */ return total_isolated; }
這里結束就是從一個pageblock中獲取到了空閑頁框,最后會返回在此pageblock中總共獲得的空閑頁框數量。這里看完了compaction_alloc()中的調用過程,再看看compaction_free()的調用過程,這個函數主要用於當從compaction_alloc()獲取一個空閑頁框用於移動時,但是因為某些原因失敗,就要把這個空閑頁框重新放回cc->freepages鏈表中,實現也很簡單:
/* 將page釋放回到cc中 */ static void compaction_free(struct page *page, unsigned long data) { struct compact_control *cc = (struct compact_control *)data; list_add(&page->lru, &cc->freepages); cc->nr_freepages++; }
看完了compaction_alloc()和compaction_free(),現在看最主要的函數migrate_pages()此函數就是用於將隔離出來的可移動頁框進行移動到空閑頁框中,在里面每進行一個頁框的處理前,都會先判斷當前進程是否需要調度,然后再進行處理:
/* 將from中的頁移到新頁上,新頁會在get_new_page中獲取 * from = &cc->migratepages * get_new_page = compaction_alloc * put_new_page = compaction_free * private = (unsigned long)cc * mode = cc->mode * reson = MR_COMPACTION */ int migrate_pages(struct list_head *from, new_page_t get_new_page, free_page_t put_new_page, unsigned long private, enum migrate_mode mode, int reason) { int retry = 1; int nr_failed = 0; int nr_succeeded = 0; int pass = 0; struct page *page; struct page *page2; /* 獲取當前進程是否允許將頁寫到swap */ int swapwrite = current->flags & PF_SWAPWRITE; int rc; /* 如果當前進程不支持將頁寫到swap,要強制其支持 */ if (!swapwrite) current->flags |= PF_SWAPWRITE; for(pass = 0; pass < 10 && retry; pass++) { retry = 0; /* page是主要遍歷的頁,page2是page在from中的下一個頁 */ list_for_each_entry_safe(page, page2, from, lru) { /* 如果進程需要調度,則調度 */ cond_resched(); if (PageHuge(page)) /* 大頁處理 * 這里看得不太懂,不知道怎么保證從from中拿出來的頁就一定是大頁 */ rc = unmap_and_move_huge_page(get_new_page, put_new_page, private, page, pass > 2, mode); else /* 此頁為非大頁處理 */ rc = unmap_and_move(get_new_page, put_new_page, private, page, pass > 2, mode); /* 返回值處理 */ switch(rc) { case -ENOMEM: goto out; case -EAGAIN: /* 重試 */ retry++; break; case MIGRATEPAGE_SUCCESS: /* 成功 */ nr_succeeded++; break; default: /* * Permanent failure (-EBUSY, -ENOSYS, etc.): * unlike -EAGAIN case, the failed page is * removed from migration page list and not * retried in the next outer loop. */ nr_failed++; break; } } } rc = nr_failed + retry; out: /* 統計 */ if (nr_succeeded) count_vm_events(PGMIGRATE_SUCCESS, nr_succeeded); if (nr_failed) count_vm_events(PGMIGRATE_FAIL, nr_failed); trace_mm_migrate_pages(nr_succeeded, nr_failed, mode, reason); /* 恢復PF_SWAPWRITE標記 */ if (!swapwrite) current->flags &= ~PF_SWAPWRITE; return rc; }
大頁的情況下有些我暫時還沒看懂,這里就先分析常規頁的情況,常規頁的情況的處理主要是umap_and_move()函數,具體看函數實現吧,如下:
/* 從get_new_page中獲取一個新頁,然后將page取消映射,並把page的數據復制到新頁上 */ static int unmap_and_move(new_page_t get_new_page, free_page_t put_new_page, unsigned long private, struct page *page, int force, enum migrate_mode mode) { int rc = 0; int *result = NULL; /* 獲取一個空閑頁,具體見compaction_alloc() */ struct page *newpage = get_new_page(page, private, &result); if (!newpage) return -ENOMEM; /* 如果頁的page_count == 1,說明此頁必定是非文件頁而且沒有進程映射了此頁,此頁可以直接釋放掉 */ if (page_count(page) == 1) { /* page was freed from under us. So we are done. */ goto out; } /* 此頁是透明大頁則跳過 */ if (unlikely(PageTransHuge(page))) if (unlikely(split_huge_page(page))) goto out; /* 將page取消映射,並把page的數據復制到newpage中 */ rc = __unmap_and_move(page, newpage, force, mode); out: if (rc != -EAGAIN) { /* 遷移成功,將舊的page放回到LRU鏈表中,為什么放入lru鏈表,因為舊的page已經是一個空閑的page了 */ list_del(&page->lru); dec_zone_page_state(page, NR_ISOLATED_ANON + page_is_file_cache(page)); /* 在這里會把新頁放回到原來的地方 */ putback_lru_page(page); } /* 遷移不成功 */ if (rc != MIGRATEPAGE_SUCCESS && put_new_page) { ClearPageSwapBacked(newpage); /* 將新頁放回到cc的空閑頁鏈表中,具體見compaction_free() */ put_new_page(newpage, private); } else if (unlikely(__is_movable_balloon_page(newpage))) { /* drop our reference, page already in the balloon */ /* 如果新頁是屬於balloon使用的頁,則放回到相應地方 */ put_page(newpage); } else /* 在這里會把新頁放回到原來的地方 */ putback_lru_page(newpage); if (result) { if (rc) *result = rc; else *result = page_to_nid(newpage); } return rc; }
這里面,調用傳入的compaction_alloc()函數獲取掃描到的空閑頁框中的一個頁框,之后最重要的就是會調用__unmap_and_move()函數進行將page的頁描述符數據和頁內數據移動到new_page上,調用結束,如果遷移成功了,則會將舊頁釋放到伙伴系統中的每CPU頁高速緩存中。
對於_umap_and_move()函數的分析,可以見linux內存源碼分析 - 內存壓縮(同步關系),。