一篇讓你弄明白Linux內(nèi)核內(nèi)存管理-內(nèi)存碎片整理(實現(xiàn)流程)
我們知道內(nèi)存是以頁框為單位,每個頁框大小默認(rèn)是4K(大頁除外),而在系統(tǒng)運(yùn)行時間長后就會出現(xiàn)內(nèi)存碎片,內(nèi)存碎片的意思就是一段空閑頁框中,會有零散的一些正在使用的頁框,導(dǎo)致此段頁框被這些正在使用的零散頁框分為一小段一小段連續(xù)頁框,這樣當(dāng)需要大段連續(xù)頁框時就沒辦法分配了,這些空閑頁框就成了一些碎片,不能合并起來作為一段大的空閑頁框使用,如下圖:

白色的為空閑頁框,而有斜線的為已經(jīng)在使用的頁框,在這個圖中,空閑頁框都是零散的,它們沒辦法組成一塊連續(xù)的空閑頁框,它們只能單個單個進(jìn)行分配,當(dāng)內(nèi)核需要分配連續(xù)頁框時則沒辦法從這里分配。為了解決這個問題,內(nèi)核實現(xiàn)了內(nèi)存碎片整理功能,其原理很簡單,就是從這塊內(nèi)存區(qū)段的前面掃描可移動的頁框,從內(nèi)存區(qū)段后面向前掃描空閑的頁框,兩邊掃描結(jié)束后,將可移動的頁框放入到空閑頁框中,最后最理想的結(jié)果就如下圖:

這樣移動之后就把前面的頁框整理為了一大段連續(xù)的物理頁框了,當(dāng)然這只是理想情況,因為并不是所有頁框都可以進(jìn)行移動,像內(nèi)核使用的頁框大部分都不能夠移動,而用戶進(jìn)程的頁框大部分是可以移動了。
一、內(nèi)存碎片整理
對于內(nèi)存碎片整理來說,只會正對三種類型的頁進(jìn)行整理,分別是:MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE、MIGRATE_CMA。并且內(nèi)存碎片整理是耗費(fèi)一定的內(nèi)存、CPU和IO的。
在內(nèi)存碎片整理中,可以移動的頁框有MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE與MIGRATE_CMA這三種類型的頁框,而因為內(nèi)存碎片整理分為同步和異步,在異步過程中,只會移動MIGRATE_MOVABLE和MIGRATE_CMA這兩種類型的頁框。因為這兩種類型的頁框處理,是不會涉及到IO操作的。而在同步過程中,這三種類型的頁框都會進(jìn)行移動,因為MIGRATE_RECLAIMABLE基本上都是文件頁,在移動過程中,有可能要將臟頁回寫,會涉及到IO操作,也就是在同步過程中,是會涉及到IO操作的。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。?!前100名進(jìn)群領(lǐng)取,額外贈送一份價值699的內(nèi)核資料包(含視頻教程、電子書、實戰(zhàn)項目及代碼)?
?

二、內(nèi)存碎片整理模式
內(nèi)存碎片整理分為三種模式,三種模式耗費(fèi)的資源和對整個系統(tǒng)的壓力不一樣,如下:
異步模式:內(nèi)存碎片整理最常用的模式,在此模式中不會進(jìn)行阻塞(但是時間片到了可以進(jìn)行主動調(diào)度),也就是此種模式不會對文件頁進(jìn)行處理,文件頁用于映射文件數(shù)據(jù)使用,這種模式也是對整體系統(tǒng)壓力較小的模式。
輕同步模式:當(dāng)異步模式整理不了更多內(nèi)存時,有兩種情況下會使用輕同步模式再次整理內(nèi)存:1.明確表示分配的不是透明大頁的情況下;2.當(dāng)前進(jìn)程是內(nèi)核線程的情況下。這個模式中允許大多數(shù)操作進(jìn)行阻塞(比如隔離了太多頁,需要阻塞等待一段時間)。這種模式會處理匿名頁和文件頁,但是不會對臟文件頁執(zhí)行回寫操作,而當(dāng)處理的頁正在回寫時,也不會等待其回寫結(jié)束。
同步模式:所有操作都可以進(jìn)行阻塞,并且會等待處理的頁回寫結(jié)束,并會對文件頁、匿名頁進(jìn)行回寫到磁盤,所以導(dǎo)致最耗費(fèi)系統(tǒng)資源,對系統(tǒng)造成的壓力最大。它會在三種情況下發(fā)生:1.從cma中分配內(nèi)存時;2.調(diào)用alloc_contig_range()嘗試分配一段指定了開始頁框號和結(jié)束頁框號的連續(xù)頁框時;3.通過寫入1到sysfs中的/vm/compact_memory文件手動實現(xiàn)同步內(nèi)存碎片整理。
在內(nèi)存不足以分配連續(xù)頁框后導(dǎo)致內(nèi)存碎片整理時,首先會進(jìn)行異步的內(nèi)存碎片整理,如果異步的內(nèi)存碎片整理后還是不能夠獲取連續(xù)的頁框(這種情況發(fā)生在很多離散的頁的類型是MIGRATE_RECLAIMABLE),并且gfp_mask明確表示不處理透明大頁的情況或者該進(jìn)程是個內(nèi)核線程時,則進(jìn)行輕同步的內(nèi)存碎片整理。
在kswapd中,永遠(yuǎn)只進(jìn)行異步的內(nèi)存碎片整理,不會進(jìn)行同步的內(nèi)存碎片整理,并且在kswapd中會跳過標(biāo)記了PB_migrate_skip的pageblock。相反非kswapd中的內(nèi)存碎片整理,當(dāng)推遲次數(shù)超過了推遲閥值時,會將pageblock的PB_migrate_skip標(biāo)記清除,也就是會掃描之前有PB_migrate_skip標(biāo)記的pageblock。
在同步內(nèi)存碎片整理時,會忽略所有標(biāo)記了PB_migrate_skip的pageblock,強(qiáng)制對這段內(nèi)存中所有pageblock進(jìn)行掃描(當(dāng)然除了MIGRATE_UNMOVEABLE的pageblock)。
異步是用得最多的,它整理的速度最快,因為它只處理MIGRATE_MOVABLE和MIGRATE_CMA兩種類型,并且不處理臟頁和阻塞的情況,遇到需要阻塞的情況就返回。而輕同步的情況是在異步無法有效的整理足夠內(nèi)存時使用,它會處理MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE、MIGRATE_CMA三種類型的頁框,在一些阻塞情況也會等待阻塞完成(比如磁盤設(shè)備回寫繁忙,待移動的頁正在回寫),但是它不會對臟文件頁進(jìn)行回寫操作。同步整理的情況就是在輕同步的基礎(chǔ)上會對臟文件頁進(jìn)行回寫操作。
這里需要說明一下,非文件映射頁也是有可能被當(dāng)成臟頁的,當(dāng)它加入swapcache后會被標(biāo)記為臟頁,不過在內(nèi)存碎片整理時,即使匿名頁被標(biāo)記為臟頁也不會被回寫,它只有在內(nèi)存回收時才會對臟匿名頁進(jìn)行回寫到swap分區(qū)。在臟匿名頁進(jìn)行回寫到swap分區(qū)后,基本上此匿名頁占用的頁框也快被釋放到伙伴系統(tǒng)中作為空閑頁框了。
三、內(nèi)存碎片整理算法
先說一下內(nèi)存碎片整理的算法,首先,內(nèi)存碎片整理是以zone為單位的,而zone中又以pageblock為單位。在內(nèi)存碎片整理開始前,會在zone的頭和尾各設(shè)置一個指針,頭指針從頭向尾掃描可移動的頁,而尾指針從尾向頭掃描空閑的頁,當(dāng)他們相遇時終止整理。下圖就是簡要的說明圖:
初始時內(nèi)存狀態(tài)(默認(rèn)所有正在使用的頁框都為可移動):

從頭掃描可移動頁框:

從尾掃描空閑頁框:

結(jié)果:

但是實際情況并不是與上面圖示的情況完全一致。頭指針每次掃描一個符合要求的pageblock里的所有頁框,當(dāng)pageblock不為MIGRATE_MOVABLE、MIGRATE_CMA、MIGRATE_RECLAIMABLE時會跳過這些pageblock,當(dāng)掃描完這個pageblock后有可移動的頁框時,會變?yōu)槲仓羔樢詐ageblock為單位向前掃描可移動頁框數(shù)量的空閑頁框,但是在pageblock中也是從開始頁框向結(jié)束頁框進(jìn)行掃描,最后會將前面的頁框內(nèi)容復(fù)制到這些空閑頁框中。

需要注意,掃描可移動頁框是要先判斷pageblock的類型是否符合,符合的pageblock才在里面找可移動的頁框,當(dāng)掃描了一個符合的pageblock后本次掃描可移動頁框會停止,轉(zhuǎn)到掃描空閑頁框。而掃描空閑頁框時也會根據(jù)pageblock進(jìn)行掃描,只是從最后一個pageblock向前掃描,而在每個pageblock里面,也是從此pageblock開始頁框向pageblock結(jié)束頁框進(jìn)行掃描。當(dāng)需要的空閑頁框數(shù)量=掃描到的一個pageblock中可移動的頁框數(shù)量時,則會停止。
四、內(nèi)存碎片整理發(fā)生時機(jī)
現(xiàn)在再來說說什么時候會進(jìn)行內(nèi)存碎片整理。它會在四個地方調(diào)用到:
內(nèi)核從伙伴系統(tǒng)以min閥值獲取連續(xù)頁框,但是連續(xù)頁框又不足時。
當(dāng)需要從指定地方獲取連續(xù)頁框,但是中間有頁框正在使用時。
因為內(nèi)存短缺導(dǎo)致kswapd被喚醒時,在進(jìn)行內(nèi)存回收之后會進(jìn)行內(nèi)存碎片整理。
將1寫入sysfs中的/vm/compact_memory時,系統(tǒng)會對所有zone進(jìn)行內(nèi)存碎片整理。
而內(nèi)存碎片整理是一個相當(dāng)耗費(fèi)資源的事情,它并不會經(jīng)常會執(zhí)行,即使因為內(nèi)存短缺導(dǎo)致代碼中經(jīng)常調(diào)用到內(nèi)存碎片整理函數(shù),它也會根據(jù)調(diào)用次數(shù)選擇性地忽略一些執(zhí)行請求,見內(nèi)存碎片整理推遲。
系統(tǒng)判定是否執(zhí)行內(nèi)存碎片整理的標(biāo)準(zhǔn)是
在分配頁框過程中,zone顯示是有足夠的空閑頁框供于本次分配的,但是伙伴系統(tǒng)鏈表中又沒有連續(xù)頁框段用于本次分配。原因就是過多分散的空閑頁框,它們沒辦法組成一塊連續(xù)頁框存放在伙伴系統(tǒng)的鏈表中。
在kswapd喚醒后會對zone的頁框閥值進(jìn)行檢查,如果可用頁框少于高閥值則會進(jìn)行內(nèi)存回收,每次進(jìn)行內(nèi)存回收之后會進(jìn)行內(nèi)存碎片整理。
即使?jié)M足標(biāo)準(zhǔn),也不一定會執(zhí)行內(nèi)存碎片整理,具體見后面的內(nèi)存碎片整理推遲和compact_zone()函數(shù)。
內(nèi)存碎片整理結(jié)束時機(jī)
在內(nèi)存碎片整理中,一次zone的內(nèi)存碎片整理結(jié)束條件有三條:
可移動頁框掃描的位置是否已經(jīng)超過了空閑頁框掃描的位置,超過則結(jié)束整理,并且會重置zone->compact_cached_free_pfn和zone->compact_cached_migrate_pfn,并且不是kswap時,會設(shè)置zone->compact_blockskip_flush為真
zone的空閑頁框數(shù)量滿足了 (zone的low閥值 + 1<<order + zone的保留頁框) 條件。
判斷伙伴系統(tǒng)中是否有比order值大的空閑連續(xù)頁框塊,有則結(jié)束整理,如果order為-1,則忽略此條件
不過有例外,通過寫入到/proc/sys/vm/compact_memory進(jìn)行強(qiáng)制內(nèi)存碎片整理的情況,則判斷條件只有第1條。對于zone來說,可移動頁掃描和空閑頁掃描交匯,也就是第一種情況時,才算是對zone進(jìn)行了一次完整的內(nèi)存碎片整理,這個完整的內(nèi)存碎片整理并不代表一次內(nèi)存碎片整理就能實現(xiàn),也有可能是對zone進(jìn)行多次內(nèi)存碎片整理才達(dá)到的,因為每次內(nèi)存碎片整理結(jié)束時機(jī)還有另外兩種。當(dāng)zone達(dá)到一次完整的內(nèi)存碎片整理時,會重置兩個掃描的起始為zone的第一個頁和最后一個頁,并且不是處于kswap中時,會設(shè)置zone->compact_blockskip_flush為真,這個zone->compact_blockskip_flush在kswapd準(zhǔn)備睡眠時,會將zone的所有pageblock的PB_migrate_skip標(biāo)志清除。
五、內(nèi)存碎片整理推遲
內(nèi)存碎片整理雖然是針對每個zone的,但是執(zhí)行的時候傳入的是一個zonelist,這樣就會有一種情況,就是可能某個zone剛進(jìn)行過內(nèi)存碎片整理,而系統(tǒng)因為內(nèi)存不足又進(jìn)行了內(nèi)存碎片整理,導(dǎo)致這個剛進(jìn)行內(nèi)存碎片整理的zone又要執(zhí)行內(nèi)存碎片整理,為了避免這種情況,內(nèi)核會為每個zone做一個整理推遲計數(shù),這個計數(shù)是每個zone都會有的,在struct zone里:
compact_considered:稱為內(nèi)存碎片整理推遲計數(shù)器,每次zone的內(nèi)存碎片整理推遲了,此值會+1
compact_defer_shift:稱為內(nèi)存碎片整理推遲閥值,內(nèi)存碎片整理推遲計數(shù)器達(dá)到 1 << compact_defer_shift 后,就不能對zone進(jìn)行內(nèi)存碎片整理推遲了。
compact_order_failed:稱為內(nèi)存碎片整理失敗最大order值,記錄著此zone進(jìn)行內(nèi)存碎片整理失敗時使用的最大的order值
當(dāng)一個zone要進(jìn)行內(nèi)存碎片整理時,首先會判斷本次整理需不需要推遲,如果本次內(nèi)存碎片整理使用的order值小于zone內(nèi)存碎片整理失敗最大order值時,不用進(jìn)行推遲,可以直接進(jìn)行內(nèi)存碎片整理;但是當(dāng)order值大于zone內(nèi)存碎片整理失敗最大order值時,會增加內(nèi)存碎片整理推遲計數(shù)器,當(dāng)內(nèi)存碎片整理推遲計數(shù)器未達(dá)到內(nèi)存碎片整理推遲閥值,則會跳過本次內(nèi)存碎片整理,如果達(dá)到了,那就需要進(jìn)行內(nèi)存碎片整理。也就是當(dāng)order小于zone內(nèi)存碎片整理失敗最大order值時,不用進(jìn)行推遲,而order大于zone內(nèi)存碎片整理失敗最大order值時,才考慮是否進(jìn)行推遲。
在對一個zone進(jìn)行內(nèi)存碎片整理時,結(jié)果一般分為三種:
整理結(jié)束后,zone的空閑頁框數(shù)量達(dá)到了 (low閥值 + 1 << order + 保留的頁框數(shù)量),這種情況就稱為內(nèi)存碎片整理半成功
整理結(jié)束后,順利從zone中獲取到鏈入1 << order個連續(xù)頁框,這種情況稱為內(nèi)存碎片整理成功
整理結(jié)束后,zone的空閑頁框數(shù)量沒達(dá)到 (low閥值 + 1 << order + 保留的頁框數(shù)量),這種情況稱為內(nèi)存碎片整理失敗
當(dāng)內(nèi)存碎片整理實現(xiàn)半成功時,如果使用的order大于等于zone的內(nèi)存碎片整理失敗最大order值,則將內(nèi)存碎片整理失敗最大order值設(shè)置為本次內(nèi)存碎片整理使用的order值+1。
當(dāng)內(nèi)存碎片整理實現(xiàn)成功時,重置內(nèi)存碎片整理推遲計數(shù)器和內(nèi)存碎片整理推遲閥值計數(shù)為0并且如果使用的order大于等于zone的內(nèi)存碎片整理失敗最大order值,則將內(nèi)存碎片整理失敗最大order值設(shè)置為本次內(nèi)存碎片整理使用的order值+1。
當(dāng)內(nèi)存碎片整理失敗時,在輕同步和同步模式下,會對內(nèi)存碎片整理推遲閥值計數(shù)+1,因為計算內(nèi)存碎片整理推遲量時,是使用1 << zone->compact_defer_shift計算的,所以這個+1,實際上是讓原來的推遲量*2。 如上,代碼中只有一個地方會讓zone重置推遲計數(shù)器,就是在內(nèi)存碎片整理完成后,從此zone中分配到2^order個連續(xù)頁框,那么就會重置zone->compact_considered和zone->compact_defer_shift為0,但zone->compact_order_failed并不會被重置也永遠(yuǎn)不會被重置。
六、內(nèi)存碎片整理掃描起始位置與pageblock的跳過
在系統(tǒng)初始化過程中,就會將zone的可移動頁掃描起始位置設(shè)置為zone的第一個頁框,而空閑頁掃描起始位置設(shè)置為zone的最后一個頁框,這兩個數(shù)值保存在struct zone中的:
每次對zone進(jìn)行內(nèi)存碎片整理,都是使用這兩個值初始化本次內(nèi)存碎片整理的可移動頁掃描起始位置和空閑頁掃描起始位置。
對于保存可移動頁掃描起始位置,同步和異步是分開保存到。這兩個值在初始化時會被設(shè)置為zone的結(jié)束頁框和開始頁框,之后從內(nèi)存碎片整理開始到pageblock結(jié)束時,都沒有隔離出頁的情況下,會被更新為pageblock結(jié)束頁框。
之前說了,內(nèi)存是以一個一個連續(xù)的pageblock組織起來的,當(dāng)進(jìn)行內(nèi)存碎片整理時,一次掃描是以一個pageblock為單位,比如系統(tǒng)正在對zone進(jìn)行內(nèi)存碎片整理,首先,會從可移動頁框開始位置向后掃描一個pageblock,得到一些可移動頁框,然后空閑頁框從開始位置向前掃描一個pageblock,得到一些空閑頁框,然后將可移動頁框移動到空閑頁框中,之后再繼續(xù)循環(huán)掃描。對一個pageblock進(jìn)行掃描后,如果無法從此pageblock隔離出一個要求的頁框,這時候就會將此pageblock標(biāo)記為跳過,主要通過設(shè)置pageblock在zone的pageblock位圖中的PB_migrate_skip標(biāo)志實現(xiàn)的。而標(biāo)記之后會有兩種情況:
本次內(nèi)存碎片整理在之前的pageblock已經(jīng)隔離出了此種頁框(可移動頁/空閑頁),這種情況就是設(shè)置pageblock的PB_migrate_skip標(biāo)記。
本次內(nèi)存碎片整理在之前的pageblock中沒有隔離出過此種頁框(可移動頁/空閑頁),說明之前的pageblock都被標(biāo)記了跳過,這種情況不止設(shè)置pageblock的PB_migrate_skip標(biāo)記,還會設(shè)置對于的內(nèi)存碎片整理掃描起始位置。
對于第二種情況,以掃描可移動頁為例子,本次內(nèi)存碎片整理可移動頁掃描是從zone的第一個頁框開始,掃描完一個pageblock后,沒有隔離出可移動頁框,則標(biāo)記此pageblock的跳過標(biāo)記PB_migrate_skip,然后將zone->compact_cached_migrate_pfn設(shè)置為此pageblock的結(jié)束頁框,這樣,在下次對此zone進(jìn)行內(nèi)存碎片整理時,就會直接從此pageblock的下一個pageblock開始,把此pageblock跳過了。同理,對于空閑頁掃描也是一樣。如下圖:

在貼著掃描起始位置的pageblock被連續(xù)標(biāo)記為跳過時,就會將掃描起始位置設(shè)置到這段連續(xù)被標(biāo)記為跳過的pageblock的最后一個一頁,而當(dāng)從pageblock隔離出需要頁框時,pageblock就不會被標(biāo)記為跳過,之后又有pageblock被標(biāo)記跳過時,就不會修正掃描起始位置了,因為中間有pageblock隔離出了頁框。本次整理結(jié)束后,如上圖,修正了可移動頁掃描起始位置和空閑頁掃描起始位置,當(dāng)下一次對此zone進(jìn)行內(nèi)存碎片整理時,則從這兩個位置開始:

可以看到,再次對此zone進(jìn)行內(nèi)存碎片整理時,就會從修正后的掃描起始位置開始,并且掃描過程中會跳過被標(biāo)記了跳過的pageblock。
如果一直這樣,那不是那些被標(biāo)記為跳過的pageblock在進(jìn)行內(nèi)存碎片整理時都會被跳過,然后一直不能夠?qū)λ鼈冞M(jìn)行掃描?實際上并不是,當(dāng)進(jìn)行同步內(nèi)存碎片整理時,都會設(shè)置忽略pageblock的PB_migrate_skip標(biāo)記,也就是會對跳過的pageblock進(jìn)行掃描。但是僅僅只有在同步內(nèi)存碎片整理時才對跳過的pageblock進(jìn)行掃描也不行,畢竟同步內(nèi)存碎片整理只是一些特殊情況下才會使用。所以,在一些情況下,內(nèi)核會將zone的所有pageblock的PB_migrate_skip清除,也就是之后的內(nèi)存碎片整理掃描,又會從最開始的狀態(tài)開始進(jìn)行,
有以下兩種情況會發(fā)生,如下:
在可移動頁掃描和空閑頁掃描碰頭時,會設(shè)置zone->compact_blockskip_flush標(biāo)志,此標(biāo)志會導(dǎo)致kswapd準(zhǔn)備睡眠時,對此zone的所有pageblock清除PB_migrate_skip
在非kswapd調(diào)用中,如果此zone的推遲次數(shù)達(dá)到最大值時(zone->compact_defer_shift == COMPACT_MAX_DEFER_SHIFT并且zone->compact_considered >= 1UL << zone->compact_defer_shift)導(dǎo)致的內(nèi)存碎片整理,則清除zone所有pageblock的PB_migrate_skip
第一種情況,這個對zone的所有pageblock的PB_migrate_skip清除的工作是異步的,而第二種情況,則是同步的。
總結(jié)來說,就是zone完成了一次完整內(nèi)存碎片整理(兩個掃描相會)和此zone內(nèi)存碎片整理推遲次數(shù)達(dá)到最大值這兩種情況下,會清除zone所有pageblock的PB_migrate_skip。而清除時,都會將兩個掃描起始位置重置為zone的開始頁框和結(jié)束頁框位置。
七、實現(xiàn)代碼
先看看內(nèi)存碎片整理控制結(jié)構(gòu)struct compact_control,當(dāng)需要進(jìn)行內(nèi)存碎片整理時,總是需要初始化一個這個結(jié)構(gòu):
結(jié)構(gòu)體中每個成員變量的作用都在注釋中寫明了。
實際上無論喚醒kswapd執(zhí)行內(nèi)存碎片整理還是連續(xù)頁框不足執(zhí)行內(nèi)存碎片整理,它們的入口都是alloc_pages()函數(shù),因為kswapd不是間斷性自動喚醒,而是在分配頁框時頁框不足的情況下被主動喚醒,在內(nèi)存足夠的情況下,kswapd是不會被喚醒的,而分配頁框的函數(shù)入口就是alloc_pages(),會在此函數(shù)里面判斷頁框是否足夠。所以從alloc_pages往下跟,可以看到內(nèi)存碎片整理的代碼主要函數(shù)是try_to_compact_pages(),在這個函數(shù)中,需要傳入一個zonelist,然后對zonelist中的每個zone都進(jìn)行內(nèi)存碎片整理:
在此函數(shù)中,遍歷zonlist中的每個zone,對每個zone都進(jìn)行內(nèi)存碎片整理處理,在內(nèi)存碎片整理處理中,第一件首要事情就是判斷此zone的內(nèi)存碎片整理是否需要推遲,不需要推遲可以進(jìn)行內(nèi)存碎片整理的兩個情況是:
本次內(nèi)存碎片整理使用的order值小于zone->compact_order_failed。
如果order值大于zone->compact_order_failed,那么對zone的內(nèi)存碎片整理推遲計數(shù)器++,如果zone的內(nèi)存碎片整理推遲計數(shù)器++后數(shù)值大于等于了(1 << zone的內(nèi)存碎片整理最大推遲計數(shù)),那么也會對zone進(jìn)行內(nèi)存碎片整理。但是這種情況會清除zone所有pageblock的PB_migrate_skip標(biāo)志和重置掃描起始位置。
在上述兩種情況下,可以對此zone進(jìn)行內(nèi)存碎片整理,之后,compact_zone_order(),這個函數(shù)里主要初始化一個struct compact_control結(jié)構(gòu)體,然后調(diào)用compact_zone():
這里面又調(diào)用了compact_zone(),這個函數(shù)里首先會在此判斷是否進(jìn)行內(nèi)存碎片整理,有三種情況:
COMPACT_PARTICAL: 此zone內(nèi)存足夠用于分配要求的2^order個頁框,不用進(jìn)行內(nèi)存碎片整理。
COMPACT_SKIPPED: 此zone內(nèi)存不足以進(jìn)行內(nèi)存碎片整理,判斷條件是此zone的空閑頁框數(shù)量少于 zone的低閥值 + (2 << order)。
COMPACT_CONTINUE: 此zone可以進(jìn)行內(nèi)存碎片整理。
所以,對一個zone能否進(jìn)行內(nèi)存碎片整理有兩個判斷,一個是是否需要推遲的判斷,一個是zone的內(nèi)存頁數(shù)量是否滿足進(jìn)行內(nèi)存碎片整理。
判斷完zone能否進(jìn)行內(nèi)存碎片整理后,還需要判斷是否要重置所有pageblock的PB_migrate_skip和掃描起始位置,只有當(dāng)不處于kswapd內(nèi)存碎片整理時,并且zone的內(nèi)存碎片整理推遲次數(shù)超過了最大值的情況下,才會重置。之后會初始化可移動頁框掃描和空閑頁框掃描的起始位置,這個位置就是使用zone->compact_cached_migrate_pfn和zone->compact_cached_free_pfn決定,需要注意同步和異步的可移動頁掃描使用的是不同的位置。之后如上面所說,循環(huán)掃描pageblock,對每個pageblock進(jìn)行可移動頁的掃描和空閑頁的掃描,將可移動頁的數(shù)據(jù)和頁描述符復(fù)制到空閑頁中,最后就將已經(jīng)完成移動的可移動頁釋放掉,如下:
八、隔離可移動頁框
每次進(jìn)行隔離可移動頁框是以一個pageblock為單位,也就是從一個pageblock中將可以移動頁進(jìn)行隔離,最多也就只能隔離出一個pageblock中的所有頁框,主要實現(xiàn)函數(shù)為isolate_migratepages():
到這里,已經(jīng)完成了從一個pageblock獲取可移動頁框,并放入struct compact_control中的migratepages鏈表中。從之前的代碼看,當(dāng)調(diào)用isolate_migratepages()將一個pageblock的可移動頁隔離出來之后,會調(diào)用到migrate_pages()進(jìn)行可移動頁框的移動,之后就是詳細(xì)說明此函數(shù)。
九、可移動頁框的移動
我們先看看migrate_pages()函數(shù)原型:
比較重要的兩個參數(shù)是
new_page_t new: 是一個函數(shù)指針,指向獲取空閑頁框的函數(shù)
free_page_t free: 也是一個函數(shù)指針,指向釋放空閑頁框的函數(shù)
我們要先看看這兩個指針指向的函數(shù),這兩個函數(shù)指針分別指向compaction_alloc()和compaction_free(),compaction_alloc()是我們主要分析的函數(shù),如下:
代碼很簡單,主要還是里面的isolate_freepages()函數(shù),在這個函數(shù)中,會從cc->free_pfn開始向前掃描空閑頁框,但是注意以pageblock向前掃描,但是在pageblock內(nèi)部是從前向后掃描的,最后遇到cc->migrate_pfn后或者cc->nr_freepages >= cc->nr_migratepages的情況下會停止,如下:
這里結(jié)束就是從一個pageblock中獲取到了空閑頁框,最后會返回在此pageblock中總共獲得的空閑頁框數(shù)量。這里看完了compaction_alloc()中的調(diào)用過程,再看看compaction_free()的調(diào)用過程,這個函數(shù)主要用于當(dāng)從compaction_alloc()獲取一個空閑頁框用于移動時,但是因為某些原因失敗,就要把這個空閑頁框重新放回cc->freepages鏈表中,實現(xiàn)也很簡單:
看完了compaction_alloc()和compaction_free(),現(xiàn)在看最主要的函數(shù)migrate_pages()此函數(shù)就是用于將隔離出來的可移動頁框進(jìn)行移動到空閑頁框中,在里面每進(jìn)行一個頁框的處理前,都會先判斷當(dāng)前進(jìn)程是否需要調(diào)度,然后再進(jìn)行處理:
大頁的情況下有些我暫時還沒看懂,這里就先分析常規(guī)頁的情況,常規(guī)頁的情況的處理主要是umap_and_move()函數(shù),具體看函數(shù)實現(xiàn)吧,如下:
這里面,調(diào)用傳入的compaction_alloc()函數(shù)獲取掃描到的空閑頁框中的一個頁框,之后最重要的就是會調(diào)用__unmap_and_move()函數(shù)進(jìn)行將page的頁描述符數(shù)據(jù)和頁內(nèi)數(shù)據(jù)移動到new_page上,調(diào)用結(jié)束,如果遷移成功了,則會將舊頁釋放到伙伴系統(tǒng)中的每CPU頁高速緩存中。
十、觸發(fā)內(nèi)存頁遷移
注意區(qū)分內(nèi)存頁遷移和內(nèi)存碎片整理,內(nèi)存頁遷移是將內(nèi)存頁數(shù)據(jù)copy到另一個內(nèi)存頁的方法(同時保證內(nèi)存頁屬性不變,映射到此內(nèi)存頁的進(jìn)程也會映射到新的內(nèi)存頁),內(nèi)存頁遷移是可以跨zone和node的。 而內(nèi)存碎片整理是使用的內(nèi)存頁遷移的方式進(jìn)行的,它只會在當(dāng)前zone中執(zhí)行。 在內(nèi)核中,以下行為會觸發(fā)內(nèi)存頁面遷移
CMA:CMA(Contiguous Memory Allocator)內(nèi)存區(qū)域是kernel用于預(yù)留給設(shè)備做dma使用的,因為有一些設(shè)備在做DMA時,必須需要連續(xù)足夠長的物理內(nèi)存,所以kernel支持在啟動階段設(shè)置CMA區(qū)域的長度。但是系統(tǒng)會在CMA區(qū)域的內(nèi)存在沒有被設(shè)備使用時,將其當(dāng)做MIGRATE_MOVEABLE分配出去(也必須保證一定是MOVEABLE的),當(dāng)內(nèi)核模塊調(diào)用dma_alloc_coherent()需要更多的CMA內(nèi)存時,就會對已經(jīng)分配出去的page做內(nèi)存遷移操作,將這段物理內(nèi)存釋放回CMA區(qū)域。
/proc/sys/vm/compact_memory:這個文件允許用戶主動去做內(nèi)存碎片整理的,可以通過echo 1主動讓kernel進(jìn)行內(nèi)存碎片整理。
alloc_page:就是當(dāng)連續(xù)內(nèi)存不足時,就會觸發(fā)內(nèi)存遷移。
設(shè)置transparent hugepage可用數(shù)量:transparent hugepage中文是透明大頁,意思是用戶不需要顯式地告訴kernel,程序需要使用大頁,而是程序能夠自己向kernel申請到大頁。mmap()+MAP_HUGEPAGE這個flag。不過前提是用戶需要提前先設(shè)置好系統(tǒng)中可供使用的大頁數(shù)量,echo <nums> > /sys/kernel/mm/hugepages/hugepages-<size>/nr_hugepages。在用戶這樣設(shè)置的過程中,就會可能進(jìn)行頁遷移。
