Linux內(nèi)存回收機(jī)制:系統(tǒng)性能的幕后守護(hù)者
在Linux 系統(tǒng)的龐大體系中,內(nèi)存扮演著極為關(guān)鍵的角色,堪稱系統(tǒng)運(yùn)行的 “血液”。就像血液對(duì)于人體,一刻不停地循環(huán)流動(dòng),為各個(gè)器官輸送氧氣和營(yíng)養(yǎng)物質(zhì),維持人體正常運(yùn)轉(zhuǎn)一樣,內(nèi)存為 Linux 系統(tǒng)中的各個(gè)進(jìn)程輸送數(shù)據(jù)和指令,保障系統(tǒng)的穩(wěn)定運(yùn)行。
當(dāng)我們?cè)?Linux 系統(tǒng)中啟動(dòng)一個(gè)應(yīng)用程序時(shí),程序的代碼和數(shù)據(jù)會(huì)被加載到內(nèi)存中。CPU 從內(nèi)存中讀取這些指令和數(shù)據(jù)進(jìn)行處理,處理結(jié)果也會(huì)暫時(shí)存儲(chǔ)在內(nèi)存中??梢哉f(shuō),內(nèi)存是連接 CPU 與外部存儲(chǔ)設(shè)備(如硬盤)的橋梁。由于 CPU 的運(yùn)算速度極快,而硬盤等外部存儲(chǔ)設(shè)備的讀寫(xiě)速度相對(duì)較慢,如果沒(méi)有內(nèi)存作為數(shù)據(jù)的臨時(shí)存儲(chǔ)和快速交換區(qū)域,CPU 在等待數(shù)據(jù)從硬盤傳輸?shù)倪^(guò)程中會(huì)處于空閑狀態(tài),極大地降低系統(tǒng)的整體性能。內(nèi)存的存在使得 CPU 能夠高效地與外部存儲(chǔ)設(shè)備協(xié)同工作,讓系統(tǒng)能夠快速響應(yīng)用戶的操作。
一、內(nèi)存緊張引發(fā)的 “危機(jī)”
在 Linux 系統(tǒng)的運(yùn)行過(guò)程中,內(nèi)存資源并非總是充足的。當(dāng)系統(tǒng)中運(yùn)行的進(jìn)程過(guò)多,或者某些進(jìn)程占用了大量?jī)?nèi)存時(shí),內(nèi)存緊張的情況就會(huì)出現(xiàn),這如同人體血液循環(huán)不暢,會(huì)給系統(tǒng)帶來(lái)一系列 “危機(jī)”。
最直觀的表現(xiàn)就是系統(tǒng)運(yùn)行卡頓。當(dāng)內(nèi)存緊張時(shí),系統(tǒng)不得不頻繁地將內(nèi)存中的數(shù)據(jù)交換到磁盤的虛擬內(nèi)存(Swap Space)中,這種操作被稱為 Swap。由于磁盤的讀寫(xiě)速度遠(yuǎn)遠(yuǎn)低于內(nèi)存,頻繁的 Swap 會(huì)導(dǎo)致系統(tǒng)響應(yīng)速度大幅下降。比如,在使用 Linux 系統(tǒng)進(jìn)行多任務(wù)處理時(shí),同時(shí)打開(kāi)多個(gè)大型文件、運(yùn)行多個(gè)程序,如果內(nèi)存不足,系統(tǒng)就會(huì)出現(xiàn)明顯的卡頓,打開(kāi)文件的速度變慢,程序之間的切換也變得遲緩,原本流暢的操作變得磕磕絆絆,嚴(yán)重影響用戶體驗(yàn)。
更為嚴(yán)重的是,內(nèi)存緊張還可能導(dǎo)致系統(tǒng)崩潰。當(dāng)內(nèi)存資源耗盡,系統(tǒng)無(wú)法為新的進(jìn)程分配內(nèi)存,也無(wú)法滿足現(xiàn)有進(jìn)程對(duì)內(nèi)存的進(jìn)一步需求時(shí),就會(huì)觸發(fā) OOM(Out Of Memory)機(jī)制 ,即內(nèi)存溢出。OOM Killer 會(huì)根據(jù)一定的算法選擇并殺死一些占用內(nèi)存較多的進(jìn)程,試圖釋放內(nèi)存資源。但在某些極端情況下,這種方式可能無(wú)法有效解決問(wèn)題,最終導(dǎo)致整個(gè)系統(tǒng)崩潰,所有正在運(yùn)行的程序都將被迫終止,數(shù)據(jù)丟失,給用戶帶來(lái)巨大的損失。
為了避免這些 “危機(jī)” 的發(fā)生,Linux 系統(tǒng)需要一套高效的內(nèi)存回收機(jī)制,就像人體擁有強(qiáng)大的自我調(diào)節(jié)能力一樣,及時(shí)清理和回收不再使用的內(nèi)存資源,確保系統(tǒng)的穩(wěn)定運(yùn)行。
二、什么時(shí)候回收內(nèi)存?
因?yàn)樵诓煌膬?nèi)存分配路徑中,會(huì)觸發(fā)不同的內(nèi)存回收方式,內(nèi)存回收針對(duì)的目標(biāo)有兩種,一種是針對(duì)zone的,另一種是針對(duì)一個(gè)memcg的,而這里我們只討論針對(duì)zone的內(nèi)存回收,個(gè)人把針對(duì)zone的內(nèi)存回收方式分為三種,分別是快速內(nèi)存回收、直接內(nèi)存回收、kswapd內(nèi)存回收。
- 快速內(nèi)存回收:處于get_page_from_freelist()函數(shù)中,在遍歷zonelist過(guò)程中,對(duì)每個(gè)zone都在分配前進(jìn)行判斷,如果分配后zone的空閑內(nèi)存數(shù)量 < 閥值 + 保留頁(yè)框數(shù)量,那么此zone就會(huì)進(jìn)行快速內(nèi)存回收,即使分配前此zone空閑頁(yè)框數(shù)量都沒(méi)有達(dá)到閥值,都會(huì)進(jìn)行此zone的快速內(nèi)存回收。注意閥值可能是min/low/high的任何一種,因?yàn)樵诳焖賰?nèi)存分配,慢速內(nèi)存分配和oom分配過(guò)程中如果回收的頁(yè)框足夠,都會(huì)調(diào)用到get_page_from_freelist()函數(shù),所以快速內(nèi)存回收不僅僅發(fā)生在快速內(nèi)存分配中,在慢速內(nèi)存分配過(guò)程中也會(huì)發(fā)生。
- 直接內(nèi)存回收:處于慢速分配過(guò)程中,直接內(nèi)存回收只有一種情況下會(huì)使用,在慢速分配中無(wú)法從zonelist的所有zone中以min閥值分配頁(yè)框,并且進(jìn)行異步內(nèi)存壓縮后,還是無(wú)法分配到頁(yè)框的時(shí)候,就對(duì)zonelist中的所有zone進(jìn)行一次直接內(nèi)存回收。注意,直接內(nèi)存回收是針對(duì)zonelist中的所有zone的,它并不像快速內(nèi)存回收和kswapd內(nèi)存回收,只會(huì)對(duì)zonelist中空閑頁(yè)框不達(dá)標(biāo)的zone進(jìn)行內(nèi)存回收。并且在直接內(nèi)存回收中,有可能喚醒flush內(nèi)核線程。
- kswapd內(nèi)存回收:發(fā)生在kswapd內(nèi)核線程中,每個(gè)node有一個(gè)swapd內(nèi)核線程,也就是kswapd內(nèi)核線程中的內(nèi)存回收,是只針對(duì)所在node的,并且只會(huì)對(duì) 分配了order頁(yè)框數(shù)量后空閑頁(yè)框數(shù)量 < 此zone的high閥值 + 保留頁(yè)框數(shù)量 的zone進(jìn)行內(nèi)存回收,并不會(huì)對(duì)此node的所有zone進(jìn)行內(nèi)存回收。
這三種內(nèi)存回收雖然是在不同狀態(tài)下會(huì)被觸發(fā),但是如果當(dāng)內(nèi)存不足時(shí),kswapd內(nèi)存回收和直接內(nèi)存回收很大可能是在并發(fā)的進(jìn)行內(nèi)存回收的。而實(shí)際上,這三種回收再怎么不同,進(jìn)行內(nèi)存回收的執(zhí)行代碼是一樣的,只是在內(nèi)存回收前做的一些處理和判斷不同。
2.1快速內(nèi)存回收
無(wú)論是在快速分配還是慢速分配過(guò)程中,只要內(nèi)核希望從一個(gè)zonelist中獲取連續(xù)頁(yè)框,就必須調(diào)用get_page_from_freelist()函數(shù),在此函數(shù)中會(huì)對(duì)zonelist中的所有zone進(jìn)行判斷,判斷能否從此zone分配連續(xù)頁(yè)框,而判斷一個(gè)zone能否進(jìn)行分配的唯一標(biāo)準(zhǔn)是:分配后剩余的頁(yè)框數(shù)量 > 閥值 + 此zone的保留頁(yè)框數(shù)量。當(dāng)zone不滿足這個(gè)標(biāo)準(zhǔn),內(nèi)核會(huì)對(duì)zone進(jìn)行快速內(nèi)存回收,這個(gè)快速內(nèi)存回收的執(zhí)行路徑是:
get_page_from_freelist() -> zone_reclaim() -> __zone_reclaim() ->shrink_zone()
由于篇幅關(guān)系,就不列代碼了,之前也說(shuō)了,/proc/sys/vm/zone_reclaim_mode會(huì)影響快速內(nèi)存回收,在get_page_from_freelist()函數(shù)中就有這么一段:
/*
* 判斷是否對(duì)此zone進(jìn)行內(nèi)存回收,如果開(kāi)啟了內(nèi)存回收,則會(huì)對(duì)此zone進(jìn)行內(nèi)存回收,否則,通過(guò)距離判斷是否進(jìn)行內(nèi)存回收
* zone_allows_reclaim()函數(shù)實(shí)際上就是判斷zone所在node是否與preferred_zone所在node的距離 < RECLAIM_DISTANCE(30或10)
* 當(dāng)內(nèi)存回收未開(kāi)啟的情況下,只會(huì)對(duì)距離比較近的zone進(jìn)行回收
*/
if (zone_reclaim_mode == 0 ||
!zone_allows_reclaim(preferred_zone, zone))
goto this_zone_full;
zone_allows_reclaim()用于計(jì)算zone與preferred_zone之間的距離,這個(gè)跟node距離有關(guān),當(dāng)距離不滿足時(shí),則不會(huì)對(duì)此zone進(jìn)行快速內(nèi)存回收,也就是當(dāng)zone_reclaim_mode開(kāi)啟后,才會(huì)對(duì)zonelist中的所有zone進(jìn)行內(nèi)存回收。
需要注意閥值,之前也說(shuō)了,在一次分配過(guò)程中,可能很多地方會(huì)調(diào)用get_page_from_freelist()函數(shù),而每次傳入的閥值很可能是不同的,在第一次進(jìn)行快速分配時(shí),使用的是zone的low閥值進(jìn)行g(shù)et_page_from_freelist()調(diào)用,在慢速分配過(guò)程中,會(huì)使用zone的min閥值進(jìn)行g(shù)et_page_from_freelist()調(diào)用,而在oomkill進(jìn)行分配過(guò)程中,會(huì)使用high閥值調(diào)用get_page_from_freelist(),當(dāng)zone的分配后剩余的頁(yè)框數(shù)量 < 閥值 + 此zone的保留頁(yè)框數(shù)量 時(shí),則會(huì)調(diào)用zone_reclaim()對(duì)此zone進(jìn)行內(nèi)存回收而zone_reclaim()又會(huì)調(diào)用到__zone_relcaim()。
在__zone_reclaim()中,主要做三件事:初始化一個(gè)struct scan_control結(jié)構(gòu)、循環(huán)調(diào)用shrink_zone()進(jìn)行對(duì)zone的內(nèi)存回收、從調(diào)用shrink_slab()對(duì)slab進(jìn)行回收,struct scan_ control結(jié)構(gòu)初始化如下:
struct scan_control sc = {
/* 最少一次回收SWAP_CLUSTER_MAX,最多一次回收1 << order個(gè),應(yīng)該是1024個(gè) */
.nr_to_reclaim = max(nr_pages, SWAP_CLUSTER_MAX),
/* 當(dāng)前進(jìn)程明確禁止分配內(nèi)存的IO操作(禁止__GFP_IO,__GFP_FS標(biāo)志),那么則清除__GFP_IO,__GFP_FS標(biāo)志,表示不進(jìn)行IO操作 */
.gfp_mask = (gfp_mask = memalloc_noio_flags(gfp_mask)),
.order = order,
/* 優(yōu)先級(jí)為4,默認(rèn)是12,會(huì)比12一次掃描更多l(xiāng)ru鏈表中的頁(yè)框,而且掃描次數(shù)會(huì)比優(yōu)先級(jí)為12的少,并且如果回收過(guò)程中回收到了足夠頁(yè)框,就會(huì)返回 */
.priority = ZONE_RECLAIM_PRIORITY,
/* 通過(guò)/proc/sys/vm/zone_reclaim_mode文件設(shè)置是否允許將臟頁(yè)回寫(xiě)到磁盤,即使設(shè)為允許,快速內(nèi)存回收也不能對(duì)臟文件頁(yè)進(jìn)行回寫(xiě)操作。
* 當(dāng)zone_reclaim_mode為0時(shí),在這里是不允許頁(yè)框回寫(xiě)的,
*/
.may_writepage = !!(zone_reclaim_mode & RECLAIM_WRITE),
/* 通過(guò)/proc/sys/vm/zone_reclaim_mode文件設(shè)置是否允許將匿名頁(yè)回寫(xiě)到swap分區(qū)
* 當(dāng)zone_reclaim_mode為0時(shí),在這里是不允許匿名頁(yè)回寫(xiě)的,我們這里假設(shè)允許
*/
.may_unmap = !!(zone_reclaim_mode & RECLAIM_SWAP),
/* 允許對(duì)匿名頁(yè)lru鏈表操作 */
.may_swap = 1,
/* 本結(jié)構(gòu)還有一個(gè)
* .target_mem_cgroup 表示是針對(duì)某個(gè)memcg,還是針對(duì)整個(gè)zone進(jìn)行內(nèi)存回收的,這里為空,也就是說(shuō)這里是針對(duì)整個(gè)zone進(jìn)行內(nèi)存回收的
*/
};
nr_pages是1<<order。可以看到優(yōu)先級(jí)為4,sc->may_writepage和sc->may_unmap與zone_reclaim_mode有關(guān),這個(gè)sc是針對(duì)一個(gè)zone的,上面也說(shuō)了,只有當(dāng)zone不滿足 分配后剩余的頁(yè)框數(shù)量 > 閥值 + 此zone保留的頁(yè)框數(shù)量 時(shí),才會(huì)對(duì)zone進(jìn)行內(nèi)存回收,也就是它不是針對(duì)整個(gè)zonelist進(jìn)行內(nèi)存回收的,而是針對(duì)不滿足情況的zone進(jìn)行。再看看循環(huán)調(diào)用shrink_zone():
do {
/* 對(duì)此zone進(jìn)行內(nèi)存回收,內(nèi)存回收的主要函數(shù) */
shrink_zone(zone, &sc);
/* 沒(méi)有回收到足夠頁(yè)框,并且循環(huán)次數(shù)沒(méi)達(dá)到優(yōu)先級(jí)次數(shù),繼續(xù) */
} while (sc.nr_reclaimed < nr_pages && --sc.priority >= 0);
可以看到,每次調(diào)用shrink_zone后都會(huì)sc.priority--,也就是最多進(jìn)行4次調(diào)用shrink_zone(),并且每次調(diào)用shrink_zone()掃描的頁(yè)框會(huì)越來(lái)越多,直到回收到了1<<order個(gè)頁(yè)框?yàn)橹埂?/span>
注意:在快速內(nèi)存回收中,即使zone_reclaim_mode允許回寫(xiě),也不會(huì)對(duì)臟文件頁(yè)進(jìn)行回寫(xiě)操作的,但是如果zone_reclaim_mode允許,會(huì)對(duì)非文件頁(yè)進(jìn)行回寫(xiě)操作。
可以對(duì)快速內(nèi)存回收總結(jié)出:
- 開(kāi)始標(biāo)志是:此zone分配后剩余的頁(yè)框數(shù)量 > 此zone的閥值 + 此zone的保留頁(yè)框數(shù)量(閥值可能是:min,low,high其中一個(gè))。
- 結(jié)束標(biāo)志是:對(duì)此zone回收到了本次分配時(shí)需要的頁(yè)框數(shù)量 或者 sc->priority降為0(可能會(huì)進(jìn)行多次shrink_zone()的調(diào)用)。
- 回收對(duì)象:zone的干凈文件頁(yè)、slab、可能會(huì)回寫(xiě)匿名頁(yè)
2.2直接內(nèi)存回收
調(diào)用流程:
__alloc_pages_slowpath()
-> __alloc_pages_direct_reclaim()
-> __perform_reclaim()
-> try_to_free_pages()
-> do_try_to_free_pages()
-> shrink_zones() -> shrink_zone()
直接內(nèi)存回收發(fā)生在慢速分配中,在慢速分配中,首先喚醒所有node結(jié)點(diǎn)的kswap內(nèi)核線程,然后會(huì)調(diào)用get_page_from_freelist()嘗試用min閥值從zonelist的zone中獲取連續(xù)頁(yè)框,如果失敗,則對(duì)zonelist的zone進(jìn)行異步壓縮,異步壓縮之后再次調(diào)用get_page_from_freelist()嘗試使用min閥值從zonelist的zone中獲取連續(xù)頁(yè)框,如果還是失敗,就會(huì)進(jìn)入到直接內(nèi)存回收。
在進(jìn)行直接內(nèi)存回收時(shí),進(jìn)程是有可能加入到node的pgdat->pfmemalloc_wait這個(gè)等待隊(duì)列中,當(dāng)kswapd進(jìn)行內(nèi)存回收后如果node空閑內(nèi)存達(dá)到平衡,那么就會(huì)喚醒pgdat->pfmemalloc_wait中的進(jìn)程,其實(shí)也就是,加入到pgdat->pfmemalloc_wait這個(gè)等待隊(duì)列的進(jìn)程,自身就不會(huì)進(jìn)行直接內(nèi)存回收,而是讓kswapd進(jìn)行,之后kswapd會(huì)喚醒它們。之后的文章會(huì)詳細(xì)說(shuō)明這種情況。
先看初始化的struct scan_control,是在try_to_free_pages()中進(jìn)行初始化的:
struct scan_control sc = {
/* 打算回收32個(gè)頁(yè)框 */
.nr_to_reclaim = SWAP_CLUSTER_MAX,
.gfp_mask = (gfp_mask = memalloc_noio_flags(gfp_mask)),
/* 本次內(nèi)存分配的order值 */
.order = order,
/* 允許進(jìn)行回收的node掩碼 */
.nodemask = nodemask,
/* 優(yōu)先級(jí)為默認(rèn)的12 */
.priority = DEF_PRIORITY,
/* 與/proc/sys/vm/laptop_mode文件有關(guān)
* laptop_mode為0,則允許進(jìn)行回寫(xiě)操作,即使允許回寫(xiě),直接內(nèi)存回收也不能對(duì)臟文件頁(yè)進(jìn)行回寫(xiě)
* 不過(guò)允許回寫(xiě)時(shí),可以對(duì)非文件頁(yè)進(jìn)行回寫(xiě)
*/
.may_writepage = !laptop_mode,
/* 允許進(jìn)行unmap操作 */
.may_unmap = 1,
/* 允許進(jìn)行非文件頁(yè)的操作 */
.may_swap = 1,
};
在直接內(nèi)存回收過(guò)程中,這個(gè)sc結(jié)構(gòu)是對(duì)zonelist中所有zone使用的,而不是像快速內(nèi)存回收,是針對(duì)zonelist中不滿足條件的一個(gè)一個(gè)zone進(jìn)行使用,對(duì)于直接內(nèi)存回收,以下需要注意:
sc的c初始使用的是默認(rèn)的優(yōu)先級(jí)12,那么就會(huì)對(duì)遍歷12遍zonelist中的所有zone,每次遍歷后sc->priority--,相當(dāng)于讓每個(gè)zone執(zhí)行12次shrink_zone()
只有sc->priority == 12時(shí)會(huì)對(duì)zonelist中的所有zone強(qiáng)制執(zhí)行shrink_zone(),而當(dāng)sc->priority == 12這輪循環(huán)過(guò)后,會(huì)通過(guò)判斷來(lái)確定zone是否要執(zhí)行shrink_zone(),這個(gè)判斷標(biāo)志就是:此zone已經(jīng)掃描的頁(yè)數(shù) < (此zone所有沒(méi)有鎖在內(nèi)存中的文件頁(yè)和非文件頁(yè)之和 * 6) 。如果掃描頁(yè)數(shù)超過(guò)此值,就說(shuō)明已經(jīng)對(duì)此zone掃描過(guò)太多頁(yè)框了,就不對(duì)此zone進(jìn)行shrink_zone()了。
并且當(dāng)優(yōu)先級(jí)降到10以下時(shí),即使原來(lái)sc->may_writepage不允許回寫(xiě),這時(shí)候會(huì)開(kāi)始允許回寫(xiě)。這樣做是因?yàn)椴换貙?xiě)很難回收到頁(yè)框。
只打算回收的頁(yè)框?yàn)?2個(gè),并且在此期間,如果掃描頁(yè)數(shù)超過(guò)(sc->nr_to_reclaim + sc->nr_to_reclaim / 2),則是會(huì)根據(jù)laptop_mode的情況喚醒flush內(nèi)核線程的。
直接內(nèi)存回收無(wú)論如何都不會(huì)對(duì)臟文件頁(yè)進(jìn)行回寫(xiě)操作,如果sc->may_writepage為1,那么會(huì)對(duì)非文件頁(yè)進(jìn)行回寫(xiě)操作
- 會(huì)對(duì)文件頁(yè)和非文件頁(yè)進(jìn)行unmap操作
- 會(huì)對(duì)非文件頁(yè)處理(加入swap cache,unmap,回寫(xiě))
- 會(huì)先回收在memcg中并且超過(guò)所在memcg的soft_limit_in_bytes的進(jìn)程的內(nèi)存
- 也會(huì)調(diào)用shrink_slab()對(duì)slab進(jìn)行回收
個(gè)人認(rèn)為直接內(nèi)存回收是為了讓更多的頁(yè)得到掃描,然后進(jìn)行回寫(xiě)操作,也可能是為了后面的內(nèi)存壓縮回收一些頁(yè)框,其實(shí)這里不太理解,為什么只回收32個(gè)頁(yè)框,它并不像直接內(nèi)存回收,打算回收的頁(yè)框數(shù)量是1<<order。
可以對(duì)直接內(nèi)存回收總結(jié)出:
- 開(kāi)始標(biāo)志是:zonelist的所有zone都不能通過(guò)min閥值獲取到頁(yè)框時(shí)。
- 結(jié)束標(biāo)志:回收到32個(gè)頁(yè)框,或者sc->priority降到0,或者空閑頁(yè)框足夠進(jìn)行內(nèi)存壓縮了(可能會(huì)進(jìn)行多次shrink_zone()的調(diào)用)。
- 回收對(duì)象:超過(guò)所在memcg的soft_limit_in_bytes的進(jìn)程的內(nèi)存、zone的干凈文件頁(yè)、slab、匿名頁(yè)swap
2.3kswapd內(nèi)存回收
調(diào)用過(guò)程:
-> balance_pgdat() -> kswapd_shrink_zone() -> shrink_zone()
在分配過(guò)程中,只要get_page_from_freelist()函數(shù)無(wú)法以low閥值從zonelist的zone中獲取到連續(xù)頁(yè)框,并且分配內(nèi)存標(biāo)志gfp_mask沒(méi)有標(biāo)記__GFP_NO_KSWAPD,則會(huì)喚醒kswapd內(nèi)核線程,在當(dāng)中執(zhí)行kswapd內(nèi)存回收,先看初始化的sc結(jié)構(gòu):
/* 掃描控制結(jié)構(gòu) */
struct scan_control sc = {
/* (__GFP_WAIT | __GFP_IO | __GFP_FS)
* 此次內(nèi)存回收允許進(jìn)行IO和文件系統(tǒng)操作,有可能阻塞
*/
.gfp_mask = GFP_KERNEL,
/* 分配內(nèi)存失敗時(shí)使用的order值,因?yàn)橹挥蟹峙鋬?nèi)存失敗才會(huì)喚醒kswapd */
.order = order,
/* 這個(gè)優(yōu)先級(jí)決定了一次掃描多少隊(duì)列 */
.priority = DEF_PRIORITY,
.may_writepage = !laptop_mode,
.may_unmap = 1,
.may_swap = 1,
};
由于此sc是針對(duì)整個(gè)node的所有zone的,這里沒(méi)有設(shè)置sc->nr_to_reclaim,在確定對(duì)某個(gè)zone進(jìn)行內(nèi)存回收時(shí),這個(gè)sc->nr_to_reclaim被設(shè)置為:
sc->nr_to_reclaim = max(SWAP_CLUSTER_MAX, high_wmark_pages(zone));
可以看到,如果回收的頁(yè)框數(shù)量達(dá)到了zone的high閥值,其實(shí)意思就是盡可能的回收頁(yè)框了,kswapd內(nèi)核線程是每個(gè)node有一個(gè)的,那也意味著,此node的kswapd只會(huì)對(duì)此node的zone進(jìn)行內(nèi)存回收工作,也就不需要zonelist了。
要點(diǎn):
優(yōu)先級(jí)使用默認(rèn)為的12,會(huì)執(zhí)行多次遍歷node(并不是node中的所有zone),但并不會(huì)每次遍歷都進(jìn)行sc->priority--,當(dāng)能夠回收的內(nèi)存時(shí),才進(jìn)行sc->priority--以ZONE_HIGHMEM -> ZONE_NORMAL ->ZONE_DMA的順序找出第一個(gè)不平衡的zone,平衡條件是: 此zone分配頁(yè)框后剩余的頁(yè)框數(shù)量 > 此zone的high閥值 + 此zone保留的頁(yè)框數(shù)量。不滿足則表示此zone不平衡。
對(duì)第一個(gè)不平衡的zone及其后面的zone進(jìn)行回收在memcg中并且超過(guò)所在memcg的soft_limit_in_bytes的進(jìn)程的內(nèi)存,比如第一個(gè)不平衡的zone是ZONE_NORMAL,那么執(zhí)行內(nèi)存回收的zone就是ZONE_NORMAL和ZONE_DMA。
如果zone是平衡的,則不對(duì)zone進(jìn)行內(nèi)存回收(但是上面那部不會(huì)因?yàn)閦one平衡而不執(zhí)行),而如果zone是不平衡的,那么會(huì)調(diào)用shrink_zone()進(jìn)行內(nèi)存回收,以及調(diào)用shrink_slab()進(jìn)行slab的回收。
對(duì)于node中所有 zone分配后剩余內(nèi)存 < zone的low閥值 + zone保留的頁(yè)框數(shù)量 的zone,會(huì)進(jìn)行內(nèi)存壓縮
檢查node中所有zone是否都平衡,沒(méi)有平衡則繼續(xù)循環(huán)
如果laptop == 0,那么會(huì)對(duì)文件頁(yè)和非文件頁(yè)進(jìn)行回寫(xiě)操作,如果laptop == 1,那么只有當(dāng)sc->priority < 10時(shí)才會(huì)對(duì)文件頁(yè)和非文件頁(yè)進(jìn)行回寫(xiě)操作
會(huì)對(duì)文件頁(yè)和非文件頁(yè)進(jìn)行回寫(xiě)unmap操作
會(huì)對(duì)非文件頁(yè)進(jìn)行處理(加入swapcache,unmap,回寫(xiě))
可以看出來(lái),kswapd內(nèi)存回收會(huì)將node結(jié)點(diǎn)中的所有zone的空閑頁(yè)框都至少拉高h(yuǎn)igh閥值。
可以對(duì)kswapd內(nèi)存回收總結(jié)出:
- 開(kāi)始標(biāo)志:zonelist的所有zone都不能通過(guò)min閥值獲取到頁(yè)框時(shí),會(huì)喚醒所有node的kswapd內(nèi)核線程,然后在kswapd中會(huì)對(duì)不滿足 zone分配頁(yè)框后剩余的頁(yè)框數(shù)量 > 此zone的high閥值 + 此zone保留的頁(yè)框數(shù)量 的zone進(jìn)行內(nèi)存回收。
- 結(jié)束標(biāo)志:node中所有zone都滿足 zone分配頁(yè)框后剩余的頁(yè)框數(shù)量 > 此zone的high閥值 + 此zone保留的頁(yè)框數(shù)量(可能會(huì)進(jìn)行多次shrink_zone()的調(diào)用)。
- 回收對(duì)象:超過(guò)所在memcg的soft_limit_in_bytes的進(jìn)程的內(nèi)存、zone的干凈的文件頁(yè)、zone的臟的文件頁(yè)、slab、匿名頁(yè)swap
2.4回收哪些內(nèi)存
(1)Page Cache
CPU如果要訪問(wèn)外部磁盤上的文件,需要首先將這些文件的內(nèi)容拷貝到內(nèi)存中,由于硬件的限制,從磁盤到內(nèi)存的數(shù)據(jù)傳輸速度是很慢的,如果現(xiàn)在物理內(nèi)存有空余,干嘛不用這些空閑內(nèi)存來(lái)緩存一些磁盤的文件內(nèi)容呢,這部分用作緩存磁盤文件的內(nèi)存就叫做page cache。
用戶進(jìn)程啟動(dòng)read()系統(tǒng)調(diào)用后,內(nèi)核會(huì)首先查看page cache里有沒(méi)有用戶要讀取的文件內(nèi)容,如果有(cache hit),那就直接讀取,沒(méi)有的話(cache miss)再啟動(dòng)I/O操作從磁盤上讀取,然后放到page cache中,下次再訪問(wèn)這部分內(nèi)容的時(shí)候,就又可以cache hit,不用忍受磁盤的龜速了(比內(nèi)存慢幾個(gè)數(shù)量級(jí))。
和CPU里的硬件cache是不是很像??jī)烧咂鋵?shí)都是利用的局部性原理,只不過(guò)硬件cache是CPU緩存內(nèi)存的數(shù)據(jù),而page cache是內(nèi)存緩存磁盤的數(shù)據(jù),這也體現(xiàn)了memory hierarchy分級(jí)的思想。
相對(duì)于磁盤,內(nèi)存的容量還是很有限的,所以沒(méi)必要緩存整個(gè)文件,只需要當(dāng)文件的某部分內(nèi)容真正被訪問(wèn)到時(shí),再將這部分內(nèi)容調(diào)入內(nèi)存緩存起來(lái)就可以了,這種方式叫做demand paging(按需調(diào)頁(yè)),把對(duì)需求的滿足延遲到最后一刻,很懶很實(shí)用。
page cache中那么多的page frames,怎么管理和查找呢?這就要說(shuō)到之前的文章提到的address_space結(jié)構(gòu)體,一個(gè)address_space管理了一個(gè)文件在內(nèi)存中緩存的所有pages。這個(gè)address_space可不是進(jìn)程虛擬地址空間的address space,但是兩者之間也是由很多聯(lián)系的。
上文講到,mmap映射可以將文件的一部分區(qū)域映射到虛擬地址空間的一個(gè)VMA,如果有5個(gè)進(jìn)程,每個(gè)進(jìn)程mmap同一個(gè)文件兩次(文件的兩個(gè)不同部分),那么就有10個(gè)VMA,但address_space只有一個(gè)。每個(gè)進(jìn)程打開(kāi)一個(gè)文件的時(shí)候,都會(huì)生成一個(gè)表示這個(gè)文件的strut file,但是文件的struct inode只有一個(gè),inode才是文件的唯一標(biāo)識(shí),指向address_space的指針就是內(nèi)嵌在inode結(jié)構(gòu)體中的。在page cache中,每個(gè)page都有對(duì)應(yīng)的文件,這個(gè)文件就是這個(gè)page的owner,address_space將屬于同一owner的pages聯(lián)系起來(lái),將這些pages的操作方法與文件所屬的文件系統(tǒng)聯(lián)系起來(lái)。
來(lái)看下address_space結(jié)構(gòu)體具體是怎樣構(gòu)成的:
struct address_space {
struct inode *host; /* Owner, either the inode or the block_device */
struct radix_tree_root page_tree; /* Cached pages */
spinlock_t tree_lock; /* page_tree lock */
struct prio_tree_root i_mmap; /* Tree of private and shared mappings */
struct spinlock_t i_mmap_lock; /* Protects @i_mmap */
unsigned long nrpages; /* total number of pages */
struct address_space_operations *a_ops; /* operations table */
...
}
- host指向address_space對(duì)應(yīng)文件的inode。
- address_space中的page cache之前一直是用radix tree的數(shù)據(jù)結(jié)構(gòu)組織的,tree_lock是訪問(wèn)這個(gè)radix tree的spinlcok(現(xiàn)在已換成xarray)。
- i_mmap是管理address_space所屬文件的多個(gè)VMA映射的,用priority search tree的數(shù)據(jù)結(jié)構(gòu)組織,i_mmap_lock是訪問(wèn)這個(gè)priority search tree的spinlcok。
- nr_pages是address_space中含有的page frames的總數(shù)。
- a_ops是關(guān)于page cache如何與磁盤(backing store)交互的一系列operations。
(2)從Radix Tree到XArray
radix tree的每個(gè)節(jié)點(diǎn)可以存放64個(gè)slots(由RADIX_TREE_MAP_SHIFT設(shè)定,小型系統(tǒng)為了節(jié)省內(nèi)存可以配置為16),每個(gè)slot的指針指向下一層節(jié)點(diǎn),最后一層slot的指針指向struct page(關(guān)于struct page請(qǐng)參考這篇文章),因此一個(gè)高度為2的radix tree可以容納64個(gè)pages,高度為3則可以容納4096個(gè)pages。
如何在radix tree中找到一個(gè)指定的page呢?那就要回顧下struct page中的mapping和index了,mapping指向page所屬文件對(duì)應(yīng)的address_space,進(jìn)而可以找到address_space的radix tree,index既是page在文件內(nèi)的offset,也可作為查找這個(gè)radix tree的索引,因?yàn)閞adix tree就是按page的index來(lái)組織struct page的。這里是用page index中的一部分bit位作為radix tree第一層的索引,另一部分bit位作為第二層的索引,以此類推。因?yàn)橐粋€(gè)radix tree節(jié)點(diǎn)存放64個(gè)slots,因此一層索引需要6個(gè)bits,如果radix tree高度為2,則需要12個(gè)bits。
內(nèi)核中具體的查找函數(shù)是find_get_page(mapping, offset),如果在page cache中沒(méi)有找到,就會(huì)觸發(fā)page fault,調(diào)用__page_cache_alloc()在內(nèi)存中分配若干物理頁(yè)面,然后將數(shù)據(jù)從磁盤對(duì)應(yīng)位置copy過(guò)來(lái),通過(guò)add_to_page_cache()-->radix_tree_insert()放入radix tree中。在將一個(gè)page添加到page cache和從page cache移除時(shí),需要將page和對(duì)應(yīng)的radix tree都上鎖。
linux中radix tree的每個(gè)slot除了存放指針,還存放著標(biāo)志page和磁盤文件同步狀態(tài)的tag。如果page cache中一個(gè)page在內(nèi)存中被修改后沒(méi)有同步到磁盤,就說(shuō)這個(gè)page是dirty的,此時(shí)tag就是PAGE_CACHE_DIRTY。如果正在同步,tag就是PAGE_CACHE_WRITEBACK。只要下一層中有一個(gè)slot指向的page是dirty的,那么上一層的這個(gè)slot的tag就是PAGE_CACHE_DIRTY的,就像一滴墨水一樣,放入清水后,清水也就不再完全清澈了。
前面介紹struct page中的flags時(shí)提到,flags可以是PG_dirty或PG_writeback,既然struct page中已經(jīng)有了標(biāo)識(shí)同步狀態(tài)的信息,為什么這里radix tree還要再加上tag來(lái)標(biāo)記呢?這是為了管理的方便,內(nèi)核可以據(jù)此快速判斷某個(gè)區(qū)域中是否有dirty page或正在write back的page,而無(wú)須掃描該區(qū)域中的所有pages。
(3)Reverse Mapping
要回收一個(gè)page,可不僅僅是釋放掉那么簡(jiǎn)單,別忘了linux中進(jìn)程和內(nèi)核都是使用虛擬地址的,多少個(gè)PTE頁(yè)表項(xiàng)還指向這個(gè)page呢,回收之前,需要將這些PTE中P標(biāo)志位設(shè)為0(not present),同時(shí)將page的物理頁(yè)面號(hào)PFN也全部設(shè)成0,要不然下次PTE指向的位置存放的就是無(wú)效的數(shù)據(jù)了??墒莝truct page中好像并沒(méi)有一個(gè)維護(hù)所有指向這個(gè)page的PTE組成的鏈表。
前面的文章說(shuō)過(guò),struct page數(shù)量極其龐大,如果每個(gè)page都有這樣一個(gè)鏈表,那將顯著增加內(nèi)存占用,而且PTE中的內(nèi)容是在不斷變化的,維護(hù)這一鏈表的開(kāi)銷也是不小的。那如何找到這些PTE呢?從虛擬地址映射到物理地址是正向映射,而通過(guò)物理頁(yè)面尋址映射它的虛擬地址,叫reverse mapping(逆向映射)。page的確沒(méi)有直接指向PTE的反向指針,但是page所屬的文件是和VMA有mmap線性映射關(guān)系的啊,通過(guò)page在文件中的offset/index,就可以知道VMA中的哪個(gè)虛擬地址映射了這個(gè)page。
在代碼中的實(shí)現(xiàn)是這樣的:
__vma_address(struct page *page, struct vm_area_struct *vma)
{
pgoff_t pgoff = page_to_pgoff(page);
return vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT);
}
映射了某個(gè)address_space中至少一個(gè)page的所有進(jìn)程的所有VMA,就共同構(gòu)成了這個(gè)address_space的priority search tree(PST)。PST是一種糅合了radix tree和heap的數(shù)據(jù)結(jié)構(gòu),其實(shí)現(xiàn)較為復(fù)雜,現(xiàn)在已經(jīng)被基于augmented rbtree的interval tree所取代。
對(duì)比一下,一個(gè)進(jìn)程所含有的所有VMA是通過(guò)鏈表和紅黑樹(shù)組織起來(lái)的,一個(gè)文件所對(duì)應(yīng)的所有VMA是通過(guò)基于紅黑樹(shù)的interval tree組織起來(lái)的。因此,一個(gè)VMA被創(chuàng)建之后,需要通過(guò)vma_link()插入到這3種數(shù)據(jù)結(jié)構(gòu)中。
三、Linux內(nèi)存回收機(jī)制
2.1回收對(duì)象:匿名頁(yè)與文件頁(yè)
在 Linux 系統(tǒng)中,內(nèi)存回收主要針對(duì)匿名頁(yè)和文件頁(yè)展開(kāi)。匿名頁(yè)是一種比較特殊的內(nèi)存頁(yè),它不像文件頁(yè)那樣與磁盤上的文件存在直接映射關(guān)系,通常用于存儲(chǔ)進(jìn)程的堆、棧數(shù)據(jù)等 。當(dāng)系統(tǒng)需要回收匿名頁(yè)時(shí),會(huì)篩選出那些訪問(wèn)頻率較低、不經(jīng)常使用的匿名頁(yè),將它們寫(xiě)入到 swap 分區(qū)中。swap 分區(qū)就像是內(nèi)存的 “臨時(shí)倉(cāng)庫(kù)”,當(dāng)內(nèi)存空間緊張時(shí),把暫時(shí)不用的數(shù)據(jù)存放到這里,等需要時(shí)再取回來(lái)。寫(xiě)入 swap 分區(qū)后,這些匿名頁(yè)就可以作為空閑頁(yè)框釋放到伙伴系統(tǒng),供其他進(jìn)程申請(qǐng)使用,從而有效緩解內(nèi)存壓力。
文件頁(yè)則涵蓋了內(nèi)核緩存的磁盤數(shù)據(jù)(Buffer)以及內(nèi)核緩存的文件數(shù)據(jù)(Cache)。在回收文件頁(yè)時(shí),系統(tǒng)會(huì)先判斷文件頁(yè)的狀態(tài)。如果文件頁(yè)保存的內(nèi)容與磁盤中文件對(duì)應(yīng)內(nèi)容一致,即該文件頁(yè)是干凈的,那么無(wú)需進(jìn)行回寫(xiě)操作,可直接將其作為空閑頁(yè)框釋放到伙伴系統(tǒng);反之,如果文件頁(yè)保存的數(shù)據(jù)和磁盤中文件對(duì)應(yīng)的數(shù)據(jù)不一致,這樣的文件頁(yè)被稱為臟頁(yè),就需要先將其回寫(xiě)到磁盤中對(duì)應(yīng)數(shù)據(jù)所在的位置,確保數(shù)據(jù)的一致性,然后才能作為空閑頁(yè)框釋放 。
例如,當(dāng)我們編輯一個(gè)文本文件時(shí),在保存之前,文件在內(nèi)存中的對(duì)應(yīng)頁(yè)就是臟頁(yè),只有保存后,數(shù)據(jù)寫(xiě)入磁盤,相應(yīng)的文件頁(yè)才會(huì)變成干凈頁(yè)。通過(guò)這種有針對(duì)性的回收策略,系統(tǒng)能夠合理地管理內(nèi)存資源,提高內(nèi)存的使用效率。
2.2zone:內(nèi)存回收的基本單位
在 Linux 系統(tǒng)中,內(nèi)存回收是以 zone 為基本單位進(jìn)行的。zone 是對(duì)內(nèi)存的一種邏輯劃分,它將物理內(nèi)存按照不同的特性和用途進(jìn)行分類管理,主要包括 DMA zone、Normal zone 和 HighMem zone 等 。不同的 zone 適用于不同類型的內(nèi)存訪問(wèn)需求,例如,DMA zone 主要用于直接內(nèi)存訪問(wèn)設(shè)備,Normal zone 用于常規(guī)的內(nèi)存分配,而 HighMem zone 用于高端內(nèi)存的管理。
在每個(gè) zone 中,都有三條重要的閾值線,即 watermark [WMARK_MIN](最小閾值)、watermark [WMARK_LOW](低閾值)和 watermark [WMARK_HIGH](高閾值),它們?cè)趦?nèi)存分配和回收過(guò)程中起著關(guān)鍵的判斷和觸發(fā)作用。當(dāng)系統(tǒng)進(jìn)行內(nèi)存分配時(shí),如果是快速分配,默認(rèn)會(huì)以 watermark [WMARK_LOW] 作為閾值進(jìn)行判斷。
如果某個(gè) zone 的空閑頁(yè)數(shù)量低于這個(gè)低閾值,說(shuō)明該 zone 的內(nèi)存資源較為緊張,系統(tǒng)會(huì)立即對(duì)該 zone 執(zhí)行快速內(nèi)存回收操作,以獲取更多的空閑內(nèi)存,滿足當(dāng)前的內(nèi)存分配請(qǐng)求 。比如,當(dāng)一個(gè)新的進(jìn)程啟動(dòng)需要申請(qǐng)內(nèi)存時(shí),如果發(fā)現(xiàn)所在 zone 的空閑頁(yè)數(shù)量低于低閾值,系統(tǒng)就會(huì)迅速啟動(dòng)快速內(nèi)存回收,優(yōu)先保障新進(jìn)程的內(nèi)存需求。
若快速內(nèi)存分配失敗,系統(tǒng)會(huì)進(jìn)入慢速分配階段,此時(shí)會(huì)使用 watermark [WMARK_MIN] 這個(gè)最小閾值進(jìn)行內(nèi)存分配。如果即使使用最小閾值也無(wú)法完成內(nèi)存分配,那就意味著系統(tǒng)內(nèi)存極度緊張,會(huì)觸發(fā)直接內(nèi)存回收以及快速內(nèi)存回收機(jī)制,盡力從各個(gè)方面回收內(nèi)存,避免因內(nèi)存不足導(dǎo)致系統(tǒng)出現(xiàn)異常。
而 watermark [WMARK_HIGH] 代表著 zone 對(duì)于空閑頁(yè)數(shù)量比較滿意的一個(gè)數(shù)值狀態(tài) 。當(dāng) zone 的空閑頁(yè)數(shù)量高于這個(gè)高閾值時(shí),說(shuō)明該 zone 的內(nèi)存資源充足,系統(tǒng)處于比較良好的運(yùn)行狀態(tài);當(dāng)對(duì) zone 進(jìn)行內(nèi)存回收時(shí),通常會(huì)將目標(biāo)設(shè)定為把 zone 的空閑頁(yè)數(shù)量提高到此高閾值以上,使內(nèi)存資源達(dá)到一個(gè)較為理想的平衡狀態(tài) 。在系統(tǒng)運(yùn)行過(guò)程中,通過(guò)不斷地根據(jù)這三條閾值線對(duì)內(nèi)存進(jìn)行監(jiān)控和調(diào)整,Linux 系統(tǒng)能夠有效地管理內(nèi)存資源,保障系統(tǒng)的穩(wěn)定運(yùn)行和高效性能。我們可以通過(guò)/proc/zoneinfo文件查看各個(gè) zone 的這三個(gè)閾值的具體數(shù)值,以便更好地了解系統(tǒng)內(nèi)存狀態(tài)。
四、Linux內(nèi)存回收的方式
4.1zone的閥值
內(nèi)存回收是以zone為單位進(jìn)行的(也會(huì)以memcg為單位,這里不討論這種情況),而系統(tǒng)判斷一個(gè)zone需不需要進(jìn)行內(nèi)存回收,如上面所說(shuō),為zone設(shè)置一條線,當(dāng)此zone的空閑頁(yè)框不足以到達(dá)這條線時(shí),就會(huì)對(duì)此zone進(jìn)行內(nèi)存回收,實(shí)際上一個(gè)zone有三條線,這三條線分別是最小閥值(WMARK_MIN),低閥值(WMARK_LOW),高閥值(WMARK_HIGH),它們都保存在zone的watermark[NR_WMARK]數(shù)組中,這個(gè)數(shù)組中保存的是各個(gè)閥值要求的頁(yè)框數(shù)量,而每個(gè)閥值都會(huì)對(duì)內(nèi)存回收造成影響。而它們的描述如下:
- watermark[WMARK_MIN](min閥值):在快速分配失敗后的慢速分配中會(huì)使用此閥值進(jìn)行分配,如果慢速分配過(guò)程中使用此值還是無(wú)法進(jìn)行分配,那就會(huì)執(zhí)行直接內(nèi)存回收和快速內(nèi)存回收
- watermark[WMARK_LOW](low閥值):也叫低閥值,是快速分配的默認(rèn)閥值,在分配內(nèi)存過(guò)程中,如果zone的空閑頁(yè)框數(shù)量低于此閥值,系統(tǒng)會(huì)對(duì)zone執(zhí)行快速內(nèi)存回收
- watermark[WMARK_HIGH](high閥值):也叫高閥值,是zone對(duì)于空閑頁(yè)框數(shù)量比較滿意的一個(gè)值,當(dāng)zone的空閑頁(yè)框數(shù)量高于這個(gè)值時(shí),表示zone的空閑頁(yè)框較多。所以對(duì)zone進(jìn)行內(nèi)存回收時(shí),目標(biāo)也是希望將zone的空閑頁(yè)框數(shù)量提高到此值以上,系統(tǒng)會(huì)使用此閥值用于oomkill進(jìn)行內(nèi)存回收。
這三個(gè)閥值的關(guān)系是:min閥值 < low閥值 < high閥值。在系統(tǒng)初始化期間,根據(jù)系統(tǒng)中整個(gè)內(nèi)存的數(shù)量與每個(gè)zone管理的頁(yè)框數(shù)量,計(jì)算出每個(gè)zone的min閥值,然后low閥值 = min閥值 + (min閥值 / 4),high閥值 = min閥值 + (min閥值 / 2)。這樣就得出了這三個(gè)閥值的數(shù)值,我們可以通過(guò)/proc/zoneinfo中查看這三個(gè)閥值的數(shù)值:
可以很明顯看出來(lái),相對(duì)于整個(gè)zone管理的總頁(yè)框數(shù)量(managed),這三個(gè)值是非常非常小的,連managed的1%都不到,這些都是在系統(tǒng)初始化期間進(jìn)行設(shè)置的,具體設(shè)置函數(shù)是__setup_per_zone_wmarks()。有興趣的可以去看看。這個(gè)閥值對(duì)內(nèi)存回收的進(jìn)行具有很重要的意義,后面會(huì)詳細(xì)進(jìn)行說(shuō)明。
對(duì)于zone的內(nèi)存回收,它針對(duì)三樣?xùn)|西進(jìn)程回收:slab、lru鏈表中的頁(yè)、buffer_head。這里只討論內(nèi)存回收針對(duì)lru鏈表中的頁(yè)是如何進(jìn)行回收的。lru鏈表主要用于管理進(jìn)程空間中使用的內(nèi)存頁(yè),它主要管理三種類型的頁(yè):匿名頁(yè)、文件頁(yè)以及shmem使用的頁(yè)。在內(nèi)存回收過(guò)程中,說(shuō)簡(jiǎn)單些,就是將lru鏈表中的一些頁(yè)數(shù)據(jù)放到磁盤中,然后將這些頁(yè)釋放,當(dāng)然實(shí)際上可沒(méi)有那么簡(jiǎn)單,這個(gè)后面會(huì)詳細(xì)說(shuō)明。
在說(shuō)內(nèi)存回收前,要先補(bǔ)充一些知識(shí),因?yàn)閮?nèi)存回收并不是一個(gè)孤立的功能,它內(nèi)部會(huì)涉及到其他很多東西,比如內(nèi)存分配、lru鏈表、反向映射、swapcache、pagecache等。
(1)頁(yè)描述符頁(yè)描述符中對(duì)內(nèi)存回收來(lái)說(shuō)非常必要的標(biāo)志:
- PG_lru:表示頁(yè)在lru鏈表中
- PG_referenced: 表示頁(yè)最近被訪問(wèn)(只有文件頁(yè)使用)
- PG_dirty:頁(yè)為臟頁(yè),文件頁(yè)被修改,以及非文件頁(yè)加入到swap cache后,就會(huì)被標(biāo)記為臟頁(yè)。在此頁(yè)回寫(xiě)前會(huì)被清除,但是回寫(xiě)失敗時(shí)又會(huì)被置位
- PG_active:頁(yè)為活動(dòng)頁(yè),配合PG_lru就可以得出頁(yè)是處于非活動(dòng)頁(yè)lru鏈表還是活動(dòng)頁(yè)lru鏈表
- PG_private:頁(yè)描述符中的page->private保存有數(shù)據(jù)
- PG_writeback:頁(yè)正在進(jìn)行回寫(xiě)
- PG_swapbacked:此頁(yè)可寫(xiě)入swap分區(qū),一般用于表示此頁(yè)是非文件頁(yè)
- PG_swapcache:頁(yè)已經(jīng)加入到了swap cache中(只有非文件頁(yè)使用)
- PG_reclaim:頁(yè)正在進(jìn)行回收,只有在內(nèi)存回收時(shí)才會(huì)對(duì)需要回收的頁(yè)進(jìn)行此標(biāo)記
- PG_mlocked:頁(yè)被鎖在內(nèi)存中
在內(nèi)核中,只有一種頁(yè)能夠進(jìn)行回收,就是頁(yè)描述符中的_count為0的頁(yè),每個(gè)頁(yè)都有自己唯一的頁(yè)描述符,而每個(gè)頁(yè)描述符中都有一個(gè)_count,這個(gè)_count代表的是此頁(yè)的引用計(jì)數(shù),當(dāng)_count為-1時(shí),說(shuō)明此頁(yè)是空閑的,存放在伙伴系統(tǒng)中,每當(dāng)有一個(gè)進(jìn)程映射了此頁(yè)時(shí),此頁(yè)的_count就會(huì)++,也就是當(dāng)某個(gè)頁(yè)被10個(gè)進(jìn)程映射了,它的page->_count肯定大于10(不等于10是因?yàn)榭赡苓€有其他模塊引用了此頁(yè),比如塊層、驅(qū)動(dòng)等),所以也可以反過(guò)來(lái)說(shuō),如果某個(gè)頁(yè)的page->_count == 0,那就說(shuō)明此頁(yè)可以直接釋放回收了。
也就是說(shuō),內(nèi)核實(shí)際上回收的是那些page->_count == 0的頁(yè),但是如果真的是這樣,內(nèi)存回收這就沒(méi)有任何意義了,因?yàn)楫?dāng)最后一個(gè)引用此頁(yè)的模塊釋放掉此頁(yè)的引用時(shí),如果page->_count為0,肯定會(huì)釋放回收此頁(yè)的。實(shí)際上內(nèi)存回收做的事情,就是想辦法將一些page->_count不為0的頁(yè),嘗試將它們的page->_count降到0,這樣系統(tǒng)就可以回收這些頁(yè)了。下面是我總結(jié)出來(lái)在內(nèi)存回收過(guò)程中會(huì)對(duì)頁(yè)的page->_count產(chǎn)生影響的操作:
- 一個(gè)進(jìn)程映射此頁(yè),page->_count++
- 一個(gè)進(jìn)程取消映射此頁(yè),page->_count--
- 此頁(yè)加入到lru緩存中,page->_count++
- 此頁(yè)從lru緩存加入到lru鏈表中,page->_count--
- 此頁(yè)被加入到一個(gè)address_space中,page->_count++
- 此頁(yè)從address_space中移除時(shí),page->_count--
- 文件頁(yè)添加了buffer_heads,page->_count++
- 文件頁(yè)刪除了buffer_heads,page->_count--
- swap分區(qū)
4.2lru鏈表
lru鏈表主要作用就是將頁(yè)排序,將最應(yīng)該回收的頁(yè)放到最后面,最不應(yīng)該回收的頁(yè)放到最前面,,然后進(jìn)行內(nèi)存回收時(shí),就會(huì)從后面向前面進(jìn)行掃描,將掃描到的頁(yè)嘗試進(jìn)行回收。這里只需要記住一點(diǎn),回收的頁(yè)都是非活動(dòng)匿名頁(yè)lru鏈表或者非活動(dòng)文件頁(yè)lru鏈表上的頁(yè)。這些頁(yè)包括:進(jìn)程堆、棧、匿名mmap共享內(nèi)存映射、shmem共享內(nèi)存映射使用的頁(yè)、映射磁盤文件的頁(yè)。
(1)頁(yè)的換入換出
首先先說(shuō)明一下頁(yè)描述符中對(duì)內(nèi)存回收來(lái)說(shuō)非常必要的標(biāo)志:
- PG_lru:表示頁(yè)在lru鏈表中
- PG_referenced: 表示頁(yè)最近被訪問(wèn)(只有文件頁(yè)使用)
- PG_dirty:頁(yè)為臟頁(yè),文件頁(yè)被修改,以及非文件頁(yè)加入到swap cache后,就會(huì)被標(biāo)記為臟頁(yè)。在此頁(yè)回寫(xiě)前會(huì)被清除,但是回寫(xiě)失敗時(shí)又會(huì)被置位
- PG_active:頁(yè)為活動(dòng)頁(yè),配合PG_lru就可以得出頁(yè)是處于非活動(dòng)頁(yè)lru鏈表還是活動(dòng)頁(yè)lru鏈表
- PG_private:頁(yè)描述符中的page->private保存有數(shù)據(jù)
- PG_writeback:頁(yè)正在進(jìn)行回寫(xiě)
- PG_swapbacked:此頁(yè)可寫(xiě)入swap分區(qū),一般用于表示此頁(yè)是非文件頁(yè)
- PG_swapcache:頁(yè)已經(jīng)加入到了swap cache中(只有非文件頁(yè)使用)
- PG_reclaim:頁(yè)正在進(jìn)行回收,只有在內(nèi)存回收時(shí)才會(huì)對(duì)需要回收的頁(yè)進(jìn)行此標(biāo)記
- PG_mlocked:頁(yè)被鎖在內(nèi)存中(此標(biāo)志可以保證不被換出,但是無(wú)法保證不被被做內(nèi)存遷移)
內(nèi)存回收做的事情就是想辦法將目標(biāo)頁(yè)的page->_count降到0,對(duì)于那些沒(méi)有進(jìn)程映射了頁(yè),釋放起來(lái)就很簡(jiǎn)單,如果頁(yè)映射了磁盤文件,并且頁(yè)為臟頁(yè)(被寫(xiě)過(guò)),那就就把頁(yè)中的數(shù)據(jù)回寫(xiě)到磁盤中映射的文件中,而如果頁(yè)沒(méi)有映射磁盤文件,那么直接釋放即可。但是對(duì)于有進(jìn)程映射的頁(yè),如果此頁(yè)映射了磁盤文件,并且頁(yè)為臟頁(yè),那么和之前一樣,將此頁(yè)進(jìn)行回寫(xiě),然后釋放回收即可,但是此頁(yè)沒(méi)有映射磁盤文件,情況就會(huì)稍微復(fù)雜,會(huì)將頁(yè)數(shù)據(jù)寫(xiě)入到swap分區(qū)中,然后將此頁(yè)釋放回收??偨Y(jié)如下:
- 干凈頁(yè),并且映射了磁盤文件的頁(yè),直接回收
- 臟頁(yè)(PG_dirty置位),回寫(xiě)到對(duì)應(yīng)磁盤文件中,然后回收
- 沒(méi)有進(jìn)程映射,并且沒(méi)有映射磁盤文件的頁(yè),直接回收
- 有進(jìn)程映射,并且沒(méi)有映射磁盤文件的頁(yè),回寫(xiě)到swap分區(qū)中,然后回收
接下來(lái)會(huì)分為非活動(dòng)匿名頁(yè)lru鏈表的頁(yè)的換入換出,非活動(dòng)文件頁(yè)lru鏈表的頁(yè)的換入換出進(jìn)行描述。
匿名頁(yè)lru鏈表上保存的頁(yè)為:進(jìn)程堆、棧、數(shù)據(jù)段,匿名mmap共享內(nèi)存映射,shmem映射。這些類型的頁(yè)都有個(gè)特點(diǎn),在磁盤上沒(méi)有映射對(duì)應(yīng)的文件(shmem有對(duì)應(yīng)的文件,是/dev/zero,但它不是映射此設(shè)備文件)。而在內(nèi)存回收時(shí),會(huì)從非活動(dòng)匿名頁(yè)lru鏈表末尾向前掃描一定數(shù)量的頁(yè)框,然后嘗試將這些頁(yè)框進(jìn)行回收,而如果這些頁(yè)框沒(méi)有進(jìn)程映射它們,那么它們可以直接釋放,而如果有進(jìn)程映射了它們,那么系統(tǒng)就必須將這些頁(yè)框回寫(xiě)到磁盤上。在linux系統(tǒng)中,你可以給系統(tǒng)掛載一個(gè)swap分區(qū),這個(gè)分區(qū)就是專門用于保存這些類型的頁(yè)的。
當(dāng)這些頁(yè)需要回收,并且有進(jìn)程映射了它們時(shí),系統(tǒng)就會(huì)將這些頁(yè)寫(xiě)入swap分區(qū),需要注意,它們需要回收只有在內(nèi)存不足進(jìn)行內(nèi)存回收時(shí)才會(huì)發(fā)生,也就是當(dāng)系統(tǒng)內(nèi)存充足時(shí),是不會(huì)將這些類型的頁(yè)寫(xiě)入到swap分區(qū)中的(使用memcg除外),在磁盤上,一個(gè)swap分區(qū)是一組連續(xù)的物理扇區(qū),比如一個(gè)1G大小的swap分區(qū),那么它在磁盤上會(huì)占有1G大小磁盤塊,然后這塊磁盤塊的第一個(gè)4K,專門用于存swap分區(qū)描述結(jié)構(gòu)的,而之后的磁盤塊,會(huì)被劃分為一個(gè)一個(gè)4K大小的頁(yè)槽(正好與普通頁(yè)大小一致),然后將它們標(biāo)以ID,如下:
每個(gè)頁(yè)槽可以保存一個(gè)頁(yè)的數(shù)據(jù),這樣,一個(gè)被換出的頁(yè)就可以寫(xiě)入到磁盤中,系統(tǒng)也能夠?qū)⑦@些頁(yè)組織起來(lái)了。雖然是叫swap分區(qū),但是內(nèi)核似乎并不將swap分區(qū)當(dāng)做一個(gè)磁盤分區(qū)來(lái)看待,更像的是將其當(dāng)做一個(gè)文件來(lái)看待,因?yàn)檫@個(gè),每個(gè)swap分區(qū)都有一個(gè)address_space結(jié)構(gòu),這個(gè)結(jié)構(gòu)是每個(gè)磁盤文件都會(huì)有一個(gè)的,這個(gè)address_space結(jié)構(gòu)中最重要的是有一個(gè)基樹(shù)和一個(gè)address_space操作集。而這里swap分區(qū)有一個(gè),swap分區(qū)的address_space叫做swap cache,它的作用是從非文件頁(yè)在回寫(xiě)到swap分區(qū)到此非文件頁(yè)被回收前的這段時(shí)間里,起到一個(gè)將swap類型的頁(yè)表項(xiàng)與此頁(yè)關(guān)聯(lián)的作用和同步的作用。在這個(gè)swap cache的基樹(shù)中,將此swap分區(qū)的所有頁(yè)槽組織在了一起。當(dāng)非活動(dòng)匿名頁(yè)lru鏈表中的一個(gè)頁(yè)需要寫(xiě)入到swap分區(qū)時(shí),步驟如下:
- swap分配一個(gè)空閑的頁(yè)槽
- 根據(jù)這個(gè)空閑頁(yè)槽的ID,從swap分區(qū)的swap cache的基樹(shù)中找到此頁(yè)槽ID對(duì)應(yīng)的結(jié)點(diǎn),將此頁(yè)的頁(yè)描述符存入當(dāng)中
- 內(nèi)核以頁(yè)槽ID作為偏移量生成一個(gè)swap頁(yè)表項(xiàng),并將這個(gè)swap頁(yè)表項(xiàng)保存到頁(yè)描述符中的private中
- 對(duì)頁(yè)進(jìn)行反向映射,將所有映射了此頁(yè)的進(jìn)程頁(yè)表項(xiàng)改為此swap頁(yè)表項(xiàng)
- 將此頁(yè)的mapping改為指向此swap分區(qū)的address_space,并將此頁(yè)設(shè)置為臟頁(yè)
- 通過(guò)swap cache中的address_space操作集將此頁(yè)回寫(xiě)到swap分區(qū)中
- 回寫(xiě)完成
- 此頁(yè)要被回收,將此頁(yè)從swap cache中拿出來(lái)
當(dāng)一個(gè)進(jìn)程需要訪問(wèn)此頁(yè)時(shí),系統(tǒng)則會(huì)將此頁(yè)從swap分區(qū)換入內(nèi)存中,具體步驟如下:
- 一個(gè)進(jìn)行訪問(wèn)了此頁(yè),會(huì)先訪問(wèn)到之前設(shè)置的swap頁(yè)表項(xiàng)
- 產(chǎn)生缺頁(yè)異常,在缺頁(yè)異常中判斷此頁(yè)在swap分區(qū)中,而不在內(nèi)存中
- 分配一個(gè)新頁(yè)
- 根據(jù)進(jìn)程的頁(yè)表項(xiàng)中的swap頁(yè)表項(xiàng)找到對(duì)應(yīng)的頁(yè)槽和swap cache
- 如果以頁(yè)槽ID在swap cache中沒(méi)有找到此頁(yè),說(shuō)明此頁(yè)已被回收,從分區(qū)中將此頁(yè)讀取進(jìn)來(lái)
- 如果以頁(yè)槽ID在swap cache中找到了此頁(yè),說(shuō)明此頁(yè)還在內(nèi)存中,還沒(méi)有被回收,則直接映射此頁(yè)
這樣再此頁(yè)沒(méi)有被換出或者正在換出的情況下,所有映射了此頁(yè)的進(jìn)程又可以重新訪問(wèn)此頁(yè)了,而當(dāng)此頁(yè)被完全換出到swap分區(qū)然后被回收后,此頁(yè)就會(huì)從swap cache中移除,之后如果進(jìn)程想要訪問(wèn)此頁(yè),就需要等此頁(yè)被完全換入之后才行了。也就是這個(gè)swap cache完全為了提高效率,在頁(yè)沒(méi)有被回收前,即使此頁(yè)已經(jīng)回寫(xiě)到swap分區(qū)了,只要有進(jìn)映射此頁(yè),就可以直接映射內(nèi)存中的頁(yè),而不需要將頁(yè)從磁盤讀進(jìn)來(lái)。對(duì)于非活動(dòng)匿名頁(yè)lru鏈表上的頁(yè)進(jìn)行換入換出這里就算是說(shuō)完了。記住對(duì)于非活動(dòng)匿名頁(yè)lru鏈表上的頁(yè)來(lái)說(shuō),當(dāng)此頁(yè)加入到swap cache中時(shí),那么就意味著這個(gè)頁(yè)已經(jīng)被要求換出,然后進(jìn)行回收了。
但是相反文件頁(yè)則不是這樣,接下來(lái)簡(jiǎn)單說(shuō)說(shuō)映射了磁盤文件的文件頁(yè)的換入換出,實(shí)際上與非活動(dòng)匿名頁(yè)lru鏈表上的頁(yè)進(jìn)行換入換出是一模一樣的,因?yàn)槊總€(gè)磁盤文件都有一個(gè)自己的address_space,這個(gè)address_space就是swap分區(qū)的address_space,磁盤文件的address_space稱為page cache,接下來(lái)的處理就是差不多的,區(qū)別為以下三點(diǎn):
- 對(duì)于磁盤文件來(lái)說(shuō),它的數(shù)據(jù)并不像swap分區(qū)這樣是連續(xù)的。
- 當(dāng)文件數(shù)據(jù)讀入到一個(gè)頁(yè)時(shí),此文件頁(yè)就需要在文件的page cache中做關(guān)聯(lián),這樣當(dāng)其他進(jìn)程也需要訪問(wèn)文件的這塊數(shù)據(jù)時(shí),通過(guò)page cache就可以知道此頁(yè)在不在內(nèi)存中了。
- 并不會(huì)為映射了此文件頁(yè)的進(jìn)程頁(yè)表項(xiàng)生成一個(gè)新的頁(yè)表項(xiàng),會(huì)將所有映射了此頁(yè)的頁(yè)表項(xiàng)清空,因?yàn)樵谌表?yè)異常中通過(guò)vma就可以判斷發(fā)生缺頁(yè)的頁(yè)是映射了文件的哪一部分,然后通過(guò)文件系統(tǒng)可以查到此頁(yè)在不在內(nèi)存中。而對(duì)于匿名頁(yè)的vma來(lái)說(shuō),則無(wú)法做到這一點(diǎn)。
4.3內(nèi)存分配過(guò)程
要說(shuō)清楚內(nèi)存回收,就必須要先理清楚內(nèi)存分配過(guò)程,在調(diào)用alloc_page()或者alloc_pages()等接口進(jìn)行一次內(nèi)存分配時(shí),最后都會(huì)調(diào)用到__alloc_pages_nodemask()函數(shù),這個(gè)函數(shù)是內(nèi)存分配的心臟,對(duì)內(nèi)存分配流程做了一個(gè)整體的組織。主要需要注意的,就是在__alloc_pages_nodemask()中會(huì)進(jìn)行一次使用low閥值的快速內(nèi)存分配和一次使用min閥值的慢速內(nèi)存分配,快速內(nèi)存分配使用的函數(shù)是get_page_from_freelist(),這個(gè)函數(shù)是分配頁(yè)框的基本函數(shù),也就是說(shuō),在慢速內(nèi)存分配過(guò)程中,收集到和足夠數(shù)量的頁(yè)框后,也需要調(diào)用這個(gè)函數(shù)進(jìn)行分配。先簡(jiǎn)單說(shuō)明快速內(nèi)存分配和慢速內(nèi)存分配:
- 快速內(nèi)存分配:是get_page_from_freelist()函數(shù),通過(guò)low閥值從zonelist中獲取合適的zone進(jìn)行分配,如果zone沒(méi)有達(dá)到low閥值,則會(huì)進(jìn)行快速內(nèi)存回收,快速內(nèi)存回收后再嘗試分配。
- 慢速內(nèi)存分配:當(dāng)快速分配失敗后,也就是zonelist中所有zone在快速分配中都沒(méi)有獲取到內(nèi)存,則會(huì)使用min閥值進(jìn)行慢速分配,在慢速分配過(guò)程中主要做三件事,異步內(nèi)存壓縮、直接內(nèi)存回收以及輕同步內(nèi)存壓縮,最后視情況進(jìn)行oom分配。并且在這些操作完成后,都會(huì)調(diào)用一次快速內(nèi)存分配嘗試獲取頁(yè)框。
通過(guò)以下這幅圖,來(lái)說(shuō)明流程:
說(shuō)到內(nèi)存分配過(guò)程,就必須要說(shuō)說(shuō)中的preferred_zone和zonelist,preferred_zone可以理解為內(nèi)存分配時(shí),最希望從這個(gè)zone進(jìn)行分配,而zonelist理解為,當(dāng)沒(méi)辦法從preferred_zone分配內(nèi)存時(shí),則根據(jù)zonelist中zone的順序嘗試進(jìn)行分配,為什么會(huì)有這兩個(gè)參數(shù),是因?yàn)閚uma架構(gòu)導(dǎo)致的,我們知道,當(dāng)有多個(gè)node結(jié)點(diǎn)時(shí),CPU跨結(jié)點(diǎn)訪問(wèn)內(nèi)存是效率比較低的工作,所以CPU會(huì)優(yōu)先在本node上的zone進(jìn)行內(nèi)存分配工作,如果本node上實(shí)在分配不出內(nèi)存,那就嘗試在離本node最近的node上分配,如果還是無(wú)法分配到,那就找再下一個(gè)node。這樣每個(gè)node會(huì)將其他node的距離進(jìn)行一個(gè)排序形成了其他node的一個(gè)鏈表,這個(gè)鏈表越前面的node就表示里本node越近,越后面的node就離本node越遠(yuǎn)。
而在32位系統(tǒng)中,每個(gè)node有3個(gè)zone,分別是ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA。每個(gè)區(qū)管理的內(nèi)存數(shù)量不一樣,導(dǎo)致每個(gè)區(qū)的優(yōu)先級(jí)不同,優(yōu)先級(jí)為ZONE_HIGHMEM > ZONE_NORMAL > ZONE_DMA,對(duì)于進(jìn)程使用的頁(yè),系統(tǒng)優(yōu)先分配ZONE_HIGHMEM的頁(yè)框,如果ZONE_HIGHMEM無(wú)法分配頁(yè)框,則從ZONE_NORMAL進(jìn)行分配,當(dāng)然,對(duì)于內(nèi)核使用的頁(yè)來(lái)說(shuō),大部分只會(huì)從ZONE_NORMAL和ZONE_DMA進(jìn)行分配,這樣,將這個(gè)zone優(yōu)先級(jí)與node鏈表結(jié)合,就得到zonelist鏈表了,比如對(duì)于node0,它完整的zonelist鏈表就可能如下:
node0的管理區(qū) node1的管理區(qū)
ZONE_HIGHMEM(0) -> ZONE_NORMAL(0) -> ZONE_DMA(0) -> ZONE_HIGHMEM(1) -> ZONE_NORMAL(1) -> ZONE_DMA(1)
因?yàn)槊總€(gè)node都有自己完整的zonelist鏈表,所以對(duì)于node1,它的鏈表時(shí)這樣的
node1的管理區(qū) node0的管理區(qū)
ZONE_HIGHMEM(1) -> ZONE_NORMAL(1) -> ZONE_DMA(1) -> ZONE_HIGHMEM(0) -> ZONE_NORMAL(0) -> ZONE_DMA(0)
這樣得到了兩個(gè)node自己的zonelist,但是在內(nèi)存分配中,還不一定會(huì)使用node自己的zonelist,因?yàn)橛行﹥?nèi)存只希望從ZONE_NORMAL和ZONE_DMA中進(jìn)行分配,所以,在每次進(jìn)行內(nèi)存分配時(shí),都會(huì)此次內(nèi)存分配形成一個(gè)滿足的zonelist,比如:某次內(nèi)存分配在node0的CPU上執(zhí)行了,希望從ZONE_NORMAL和ZONEDMA區(qū)中進(jìn)行分配,那么就會(huì)形成下面這個(gè)鏈表
node0的管理區(qū) node1的管理區(qū)
ZONE_NORMAL(0) -> ZONE_DMA(0) -> ZONE_NORMAL(1) -> ZONE_DMA(1)
這樣就是preferred_zone和zonelist,preferred_zone一般都是指向zonelist中的第一個(gè)zone,當(dāng)然這個(gè)還會(huì)跟nodemask有關(guān),這個(gè)就不細(xì)說(shuō)了。
4.4掃描控制結(jié)構(gòu)
之前說(shuō)內(nèi)存壓縮的文章也有涉及這個(gè)結(jié)構(gòu),現(xiàn)在詳細(xì)說(shuō)明一下,掃描控制結(jié)構(gòu)用于內(nèi)存回收和內(nèi)存壓縮,它的主要作用時(shí)保存對(duì)一次內(nèi)存回收或者內(nèi)存壓縮的變量和參數(shù),一些處理結(jié)果也會(huì)保存在里面,結(jié)構(gòu)如下:
/* 掃描控制結(jié)構(gòu),用于內(nèi)存回收和內(nèi)存壓縮 */
struct scan_control {
/* 需要回收的頁(yè)框數(shù)量 */
unsigned long nr_to_reclaim;
/* 申請(qǐng)內(nèi)存時(shí)使用的分配標(biāo)志 */
gfp_t gfp_mask;
/* 申請(qǐng)內(nèi)存時(shí)使用的order值,因?yàn)橹挥猩暾?qǐng)內(nèi)存,然后內(nèi)存不足時(shí)才會(huì)進(jìn)行掃描 */
int order;
/* 允許執(zhí)行掃描的node結(jié)點(diǎn)掩碼 */
nodemask_t *nodemask;
/* 目標(biāo)memcg,如果是針對(duì)整個(gè)zone進(jìn)行的,則此為NULL */
struct mem_cgroup *target_mem_cgroup;
/* 掃描優(yōu)先級(jí),代表一次掃描(total_size >> priority)個(gè)頁(yè)框
* 優(yōu)先級(jí)越低,一次掃描的頁(yè)框數(shù)量就越多
* 優(yōu)先級(jí)越高,一次掃描的數(shù)量就越少
* 默認(rèn)優(yōu)先級(jí)為12
*/
int priority;
/* 是否能夠進(jìn)行回寫(xiě)操作(與分配標(biāo)志的__GFP_IO和__GFP_FS有關(guān)) */
unsigned int may_writepage:1;
/* 能否進(jìn)行unmap操作,就是將所有映射了此頁(yè)的頁(yè)表項(xiàng)清空 */
unsigned int may_unmap:1;
/* 是否能夠進(jìn)行swap交換,如果不能,在內(nèi)存回收時(shí)則不掃描匿名頁(yè)lru鏈表 */
unsigned int may_swap:1;
unsigned int hibernation_mode:1;
/* 掃描結(jié)束后會(huì)標(biāo)記,用于內(nèi)存回收判斷是否需要進(jìn)行內(nèi)存壓縮 */
unsigned int compaction_ready:1;
/* 已經(jīng)掃描的頁(yè)框數(shù)量 */
unsigned long nr_scanned;
/* 已經(jīng)回收的頁(yè)框數(shù)量 */
unsigned long nr_reclaimed;
};
結(jié)構(gòu)很簡(jiǎn)單,主要就是保存一些參數(shù),在內(nèi)存回收和內(nèi)存壓縮時(shí)就會(huì)根據(jù)這個(gè)結(jié)構(gòu)中的這些參數(shù),做不同的處理,后面代碼會(huì)詳細(xì)說(shuō)明。這里我們只說(shuō)說(shuō)會(huì)幾個(gè)特別的參數(shù):
- priority:優(yōu)先級(jí),這個(gè)參數(shù)主要會(huì)影響內(nèi)存回收時(shí)一次掃描的頁(yè)框數(shù)量、在shrink_lruvec()中回收到足夠頁(yè)框后是否繼續(xù)回收、內(nèi)存回收時(shí)的回寫(xiě)、是否取消對(duì)zone進(jìn)行回收判斷而直接開(kāi)始回收,一共四個(gè)地方。
- may_unmap:是否能夠進(jìn)行unmap操作,如果不能進(jìn)行unmap操作,就只能對(duì)沒(méi)有進(jìn)程映射的頁(yè)進(jìn)行回收。
- may_writepage:是否能夠進(jìn)行將頁(yè)回寫(xiě)到磁盤的操作,這個(gè)值會(huì)影響臟的文件頁(yè)與匿名頁(yè)lru鏈表中的頁(yè)的回收,如果不能進(jìn)行回寫(xiě)操作,臟頁(yè)和匿名頁(yè)lru鏈表中的頁(yè)都不能進(jìn)行回收(已經(jīng)回寫(xiě)完成的頁(yè)除外,后面解釋)
- may_swap:能否進(jìn)行swap交換,同樣影響匿名頁(yè)lru鏈表中的頁(yè)的回收,如果不能進(jìn)行swap交換,就不會(huì)對(duì)匿名頁(yè)lru鏈表進(jìn)行掃描,也就是在本次內(nèi)存回收中,完全不會(huì)回收匿名頁(yè)lru鏈表中的頁(yè)(進(jìn)程堆、棧、shmem共享內(nèi)存、匿名mmap共享內(nèi)存使用的頁(yè))
在快速內(nèi)存回收、直接內(nèi)存回收、kswapd內(nèi)存回收中,這幾個(gè)值的設(shè)置不一定會(huì)一致,也導(dǎo)致了它們對(duì)不同類型的頁(yè)處理方式也不同。除了sc->may_writepage會(huì)影響頁(yè)的回寫(xiě)外,還有進(jìn)行內(nèi)存分配時(shí)使用的分配標(biāo)志gfp_mask中的__GFP_IO和__GFP_FS會(huì)影響頁(yè)的回寫(xiě),具體如下:
- 掃描到的非活動(dòng)匿名頁(yè)lru鏈表中的頁(yè)如果還沒(méi)有加入到swapcache中,需要有__GFP_IO標(biāo)記才允許加入swapcache和回寫(xiě)。
- 掃描到的非活動(dòng)匿名頁(yè)lru鏈表中的頁(yè)如果已經(jīng)加入到了swapcache中,需要有__GFP_FS才允許進(jìn)行回寫(xiě)。
- 掃描到的非活動(dòng)文件頁(yè)lru鏈表中的頁(yè)需要有__GFP_FS才允許進(jìn)行回寫(xiě)。
這里還需要說(shuō)說(shuō)三個(gè)重要的內(nèi)核配置:
/proc/sys/vm/zone_reclaim_mode
這個(gè)參數(shù)只會(huì)影響快速內(nèi)存回收,其值有三種,
- 0x1:開(kāi)啟zone的內(nèi)存回收
- 0x2:開(kāi)啟zone的內(nèi)存回收,并且允許回寫(xiě)
- 0x4:開(kāi)啟zone的內(nèi)存回收,允許進(jìn)行unmap操作
當(dāng)此參數(shù)為0時(shí),會(huì)導(dǎo)致快速內(nèi)存回收只會(huì)對(duì)最優(yōu)zone附近的幾個(gè)需要進(jìn)行內(nèi)存回收的zone進(jìn)行內(nèi)存回收(說(shuō)快速內(nèi)存會(huì)解釋),而只要不為0,就會(huì)對(duì)zonelist中所有應(yīng)該進(jìn)行內(nèi)存回收的zone進(jìn)行內(nèi)存回收。
當(dāng)此參數(shù)為0x1(001)時(shí),就如上面一行所說(shuō),允許快速內(nèi)存回收對(duì)zonelist中所有應(yīng)該進(jìn)行內(nèi)存回收的zone進(jìn)行內(nèi)存回收。
當(dāng)此參數(shù)為0x2(010)時(shí),在0x1的基礎(chǔ)上,允許快速內(nèi)存回收進(jìn)行匿名頁(yè)lru鏈表中的頁(yè)的回寫(xiě)操作。
當(dāng)此參數(shù)0x4(100)時(shí),在0x1的基礎(chǔ)上,允許快速內(nèi)存回收進(jìn)行頁(yè)的unmap操作。
/proc/sys/vm/laptop_mode
此參數(shù)只會(huì)影響直接內(nèi)存回收,只有兩個(gè)值:
- 0:允許直接內(nèi)存回收對(duì)匿名頁(yè)lru鏈表中的頁(yè)進(jìn)行回寫(xiě)操作,并且允許直接內(nèi)存回收喚醒flush內(nèi)核線程
- 非0:直接內(nèi)存回收不會(huì)對(duì)匿名頁(yè)lru鏈表中的頁(yè)進(jìn)行回寫(xiě)操作
/proc/sys/vm/swapiness
此參數(shù)影響進(jìn)行內(nèi)存回收時(shí),掃描匿名頁(yè)lru鏈表和掃描文件頁(yè)lru鏈表的比例,范圍是0~200,系統(tǒng)默認(rèn)是30:
- 接近0:進(jìn)行內(nèi)存回收時(shí),更多地去掃描文件頁(yè)lru鏈表,如果為0,那么就不會(huì)去掃描匿名頁(yè)lru鏈表。
- 接近200:進(jìn)行內(nèi)存回收時(shí),更多地去掃描匿名頁(yè)lru鏈表。
五、內(nèi)存回收實(shí)現(xiàn)方式
5.1頁(yè)面回收與LRU算法
頁(yè)面回收是 Linux 內(nèi)存回收機(jī)制的基礎(chǔ)環(huán)節(jié),其核心在于精準(zhǔn)地識(shí)別并釋放那些不再被頻繁使用的內(nèi)存頁(yè)面,而 LRU(Least Recently Used)算法則在這一過(guò)程中扮演著 “篩選器” 的關(guān)鍵角色 。LRU 算法基于一個(gè)簡(jiǎn)單而有效的假設(shè):如果一個(gè)頁(yè)面在過(guò)去很長(zhǎng)一段時(shí)間內(nèi)都未被訪問(wèn),那么在未來(lái)的短時(shí)間內(nèi),它被訪問(wèn)的概率也相對(duì)較低。這就好比圖書(shū)館里的書(shū)籍,如果某本書(shū)籍長(zhǎng)時(shí)間無(wú)人借閱,那么在接下來(lái)的一段時(shí)間里,它被借閱的可能性也不大,就可以考慮將其從常用書(shū)架上移除,為其他更受歡迎的書(shū)籍騰出空間。
在 Linux 系統(tǒng)中,內(nèi)核通過(guò)維護(hù)一個(gè)雙向鏈表來(lái)實(shí)現(xiàn) LRU 算法。鏈表中的每個(gè)節(jié)點(diǎn)都代表一個(gè)內(nèi)存頁(yè)面,每當(dāng)一個(gè)頁(yè)面被訪問(wèn)時(shí),它就會(huì)被移動(dòng)到鏈表的頭部,表示它是最近被使用的頁(yè)面;而鏈表尾部的頁(yè)面則是最近最少使用的,當(dāng)系統(tǒng)需要回收內(nèi)存時(shí),就會(huì)優(yōu)先從鏈表尾部選擇頁(yè)面進(jìn)行回收 。以瀏覽器的頁(yè)面緩存為例,當(dāng)我們頻繁瀏覽不同的網(wǎng)頁(yè)時(shí),瀏覽器會(huì)將最近訪問(wèn)的網(wǎng)頁(yè)頁(yè)面緩存到內(nèi)存中,采用 LRU 算法管理這些緩存頁(yè)面。
如果內(nèi)存不足,瀏覽器就會(huì)將鏈表尾部,也就是那些長(zhǎng)時(shí)間未被訪問(wèn)的網(wǎng)頁(yè)頁(yè)面緩存回收,釋放出內(nèi)存空間,以便緩存新的網(wǎng)頁(yè)頁(yè)面,確保瀏覽器能夠高效運(yùn)行。通過(guò)這種方式,LRU 算法能夠有效地管理內(nèi)存頁(yè)面,使得系統(tǒng)能夠及時(shí)回收不再使用的頁(yè)面,將釋放的內(nèi)存重新分配給其他急需內(nèi)存的進(jìn)程,從而提高內(nèi)存的使用效率,保障系統(tǒng)的穩(wěn)定運(yùn)行。
5.2頁(yè)面交換:內(nèi)存與磁盤的 “互動(dòng)”
頁(yè)面交換是 Linux 內(nèi)存回收機(jī)制應(yīng)對(duì)內(nèi)存不足的重要手段,它建立在虛擬內(nèi)存技術(shù)的基礎(chǔ)之上,實(shí)現(xiàn)了內(nèi)存與磁盤之間的數(shù)據(jù)交換,就像在倉(cāng)庫(kù)與臨時(shí)存儲(chǔ)點(diǎn)之間搬運(yùn)貨物,以解決倉(cāng)庫(kù)空間不足的問(wèn)題。當(dāng)系統(tǒng)內(nèi)存緊張時(shí),那些不活躍的頁(yè)面,也就是長(zhǎng)時(shí)間未被訪問(wèn)的頁(yè)面,會(huì)被操作系統(tǒng)視為 “暫時(shí)不需要的貨物”,從物理內(nèi)存中移出,交換到磁盤上的交換分區(qū)(Swap Partition)中,這個(gè)過(guò)程被稱為 “換出”(Swap Out) 。交換分區(qū)就像是內(nèi)存的 “備份倉(cāng)庫(kù)”,專門用于存儲(chǔ)這些被換出的頁(yè)面。
當(dāng)這些被換出的頁(yè)面在未來(lái)某個(gè)時(shí)刻又需要被訪問(wèn)時(shí),操作系統(tǒng)會(huì)將其從交換分區(qū)重新調(diào)入內(nèi)存,這個(gè)過(guò)程被稱為 “換入”(Swap In) 。在 Linux 系統(tǒng)中,頁(yè)面交換由內(nèi)核的頁(yè)替換算法自動(dòng)執(zhí)行,常見(jiàn)的算法如 LRU 算法在這一過(guò)程中發(fā)揮著重要作用,它幫助系統(tǒng)確定哪些頁(yè)面是最不活躍的,應(yīng)該被優(yōu)先換出 。例如,當(dāng)我們?cè)谑褂?Linux 系統(tǒng)進(jìn)行多任務(wù)處理時(shí),同時(shí)運(yùn)行多個(gè)大型程序,隨著內(nèi)存逐漸被占用,系統(tǒng)會(huì)將一些暫時(shí)不使用的程序頁(yè)面換出到交換分區(qū),如后臺(tái)運(yùn)行的數(shù)據(jù)庫(kù)程序中一些不常用的數(shù)據(jù)頁(yè)面。
當(dāng)這些程序再次需要這些頁(yè)面時(shí),系統(tǒng)又會(huì)及時(shí)將它們從交換分區(qū)換入內(nèi)存,確保程序能夠正常運(yùn)行。雖然頁(yè)面交換機(jī)制有效地增加了系統(tǒng)的可用內(nèi)存,但頻繁的頁(yè)面交換會(huì)導(dǎo)致系統(tǒng)的磁盤 I/O 負(fù)載過(guò)高,因?yàn)榇疟P的讀寫(xiě)速度遠(yuǎn)遠(yuǎn)低于內(nèi)存,這就好比頻繁地在倉(cāng)庫(kù)與臨時(shí)存儲(chǔ)點(diǎn)之間搬運(yùn)貨物,會(huì)耗費(fèi)大量的時(shí)間和精力,進(jìn)而影響系統(tǒng)的響應(yīng)速度。因此,在實(shí)際應(yīng)用中,需要合理地設(shè)置交換分區(qū)的大小和內(nèi)核的頁(yè)面交換算法,以及優(yōu)化系統(tǒng)的內(nèi)存使用方式,以避免過(guò)度使用交換分區(qū),保障系統(tǒng)的性能。
5.3內(nèi)存壓縮:向空間要效率
內(nèi)存壓縮是 Linux 內(nèi)存回收機(jī)制中一項(xiàng)旨在提高內(nèi)存使用效率、減少磁盤 I/O 的創(chuàng)新技術(shù),它通過(guò)運(yùn)用高效的壓縮算法,對(duì)那些不活躍的頁(yè)面進(jìn)行壓縮處理,從而在有限的內(nèi)存空間中存儲(chǔ)更多的數(shù)據(jù),就像將蓬松的物品壓縮成緊湊的狀態(tài),以節(jié)省存儲(chǔ)空間。當(dāng)系統(tǒng)內(nèi)存不足時(shí),傳統(tǒng)的頁(yè)面交換機(jī)制會(huì)將不活躍頁(yè)面寫(xiě)入磁盤交換分區(qū),這一過(guò)程伴隨著大量的磁盤 I/O 操作,嚴(yán)重影響系統(tǒng)性能。而內(nèi)存壓縮機(jī)制則另辟蹊徑,它將不活躍頁(yè)面在內(nèi)存中直接進(jìn)行壓縮,然后存儲(chǔ)在內(nèi)存的特定區(qū)域,避免了頻繁的磁盤 I/O 。
在 Linux 系統(tǒng)中,內(nèi)存壓縮機(jī)制通常借助 zRAM 等技術(shù)來(lái)實(shí)現(xiàn)。zRAM 虛擬出一個(gè)塊設(shè)備,當(dāng)系統(tǒng)觸發(fā)內(nèi)存回收時(shí),會(huì)先從系統(tǒng)中查找不活躍的內(nèi)存頁(yè)面,然后將這些頁(yè)面發(fā)送到 zRAM 設(shè)備。zRAM 設(shè)備接收到頁(yè)面后,會(huì)使用特定的壓縮算法,如 lzo、lz4 等對(duì)頁(yè)面進(jìn)行壓縮,將壓縮后的數(shù)據(jù)存儲(chǔ)在內(nèi)存中 。當(dāng)進(jìn)程需要訪問(wèn)這些被壓縮的頁(yè)面時(shí),系統(tǒng)會(huì)先從 zRAM 設(shè)備中讀取壓縮數(shù)據(jù),然后進(jìn)行解壓縮,將解壓縮后的頁(yè)面重新放置在內(nèi)存中供進(jìn)程使用 。
以手機(jī)系統(tǒng)為例,在運(yùn)行多個(gè)應(yīng)用程序時(shí),內(nèi)存資源容易緊張。采用內(nèi)存壓縮機(jī)制后,系統(tǒng)可以將后臺(tái)應(yīng)用程序中不活躍的頁(yè)面進(jìn)行壓縮,如壓縮圖片處理應(yīng)用在后臺(tái)時(shí)占用的大量圖像數(shù)據(jù)頁(yè)面,將其壓縮后存儲(chǔ)在內(nèi)存中,為前臺(tái)運(yùn)行的應(yīng)用程序騰出更多內(nèi)存空間,同時(shí)避免了將這些頁(yè)面交換到磁盤,減少了磁盤 I/O 操作,提高了系統(tǒng)的整體性能,使得手機(jī)在多任務(wù)處理時(shí)更加流暢。內(nèi)存壓縮機(jī)制在一定程度上緩解了內(nèi)存壓力,提高了系統(tǒng)性能,是 Linux 內(nèi)存回收機(jī)制中一項(xiàng)重要的優(yōu)化技術(shù)。
5.4匿名頁(yè)面丟棄:特殊情況下的內(nèi)存釋放
匿名頁(yè)面丟棄是 Linux 內(nèi)存回收機(jī)制在特定情況下采取的一種內(nèi)存釋放策略,主要針對(duì)那些不屬于文件系統(tǒng)緩存的匿名頁(yè)面,這些頁(yè)面通常由進(jìn)程的堆棧和堆分配產(chǎn)生,就像臨時(shí)搭建的帳篷,在不需要時(shí)可以拆除以騰出空間。當(dāng)系統(tǒng)內(nèi)存極度緊張,且其他內(nèi)存回收機(jī)制無(wú)法滿足內(nèi)存需求時(shí),匿名頁(yè)面丟棄機(jī)制就會(huì)啟動(dòng) 。
在這種情況下,操作系統(tǒng)會(huì)對(duì)匿名頁(yè)面進(jìn)行評(píng)估,選擇那些可以安全丟棄的頁(yè)面。對(duì)于進(jìn)程堆棧和堆分配的匿名頁(yè)面,如果這些頁(yè)面中的數(shù)據(jù)在后續(xù)操作中可以重新生成,或者對(duì)進(jìn)程的正常運(yùn)行沒(méi)有直接影響,那么它們就有可能被丟棄 。例如,在一些計(jì)算密集型的進(jìn)程中,堆棧中可能會(huì)臨時(shí)存儲(chǔ)一些中間計(jì)算結(jié)果,這些結(jié)果在計(jì)算完成后可以通過(guò)重新計(jì)算得到,當(dāng)系統(tǒng)內(nèi)存不足時(shí),這些匿名頁(yè)面就可以被丟棄,釋放出內(nèi)存空間 。
不過(guò),匿名頁(yè)面丟棄機(jī)制的實(shí)施需要謹(jǐn)慎,因?yàn)殄e(cuò)誤地丟棄關(guān)鍵的匿名頁(yè)面可能會(huì)導(dǎo)致進(jìn)程崩潰或數(shù)據(jù)丟失。因此,Linux 系統(tǒng)在執(zhí)行匿名頁(yè)面丟棄操作時(shí),會(huì)嚴(yán)格遵循一定的規(guī)則和條件,確保丟棄的頁(yè)面不會(huì)對(duì)系統(tǒng)和進(jìn)程的正常運(yùn)行造成損害 。匿名頁(yè)面丟棄機(jī)制為 Linux 系統(tǒng)在極端內(nèi)存壓力下提供了一種有效的內(nèi)存釋放手段,保障了系統(tǒng)的基本運(yùn)行和關(guān)鍵進(jìn)程的正常執(zhí)行 。