1.前言
伙伴系統(tǒng)作為內核最基礎的物理頁內存分配器,具有高效、實現(xiàn)邏輯簡介等優(yōu)點,其原理頁也盡可能降低內存外部碎片產生,但依然無法杜絕碎片問題。外部碎片帶來的最大影響就是內存足夠,但是卻無法滿足內存分配需求,如下圖所示:
內存外部碎片導致實際占用物理頁不多,但是已無法申請>=4個頁連續(xù)內存,理想當中我們希望內存沒有外部碎片,如下圖所示:
內核并未為此目標設計新的內存分配算法(伙伴系統(tǒng)足夠簡單和高效),其選擇在伙伴系統(tǒng)基礎上根據(jù)內存使用需求進行內存分配,將不可移動內存和可移動內存歸類,在內存碎片問題出現(xiàn)時,嘗試進行內存規(guī)整(compact),移動可移動的頁面,騰出更多連續(xù)內存,如下圖簡述:
上圖中將一個頁移動到另一個頁的過程叫頁遷移,這并不是一件輕松的事情,數(shù)據(jù)的拷貝、進程映射信息更改等等都很耗時并且也是個復雜邏輯,這注定內存規(guī)整的過程是一個重負載的過程。事實上,頁遷移是內存管理的獨立邏輯,內核對此單獨封裝接口migrate_pages,內存規(guī)整只是其中一個應用場景,類似場景還有NUMA Balance、Memory hotplug及CMA內存等等。本文聚焦內存規(guī)整,不描述內存遷移邏輯。
站在開發(fā)者角度有了內存遷移基礎能力,那么就有實現(xiàn)內存規(guī)整基礎,但依然有值得思考的問題,比如內存規(guī)整的范圍,何時進行內存規(guī)整等等。
對于內存規(guī)整范圍問題,內核通常選擇以zone為單位進行規(guī)整(實際范圍受到參數(shù)影響可能為zone一部分),并為此封裝compact_zone接口,作為內存規(guī)整核心接口(alloc_contig_range例外)。
對于何時觸發(fā)問題,屬于觸發(fā)策略和場景問題,內核當前引入直接內存規(guī)整、被動內存規(guī)整、預應性內存規(guī)整及主動內存規(guī)整四種策略場景,這些場景最終都會通過compact_zone進行內存規(guī)整,但是他們觸發(fā)的時機不同、目標不同、規(guī)整范圍不同、規(guī)整退出條件不同,規(guī)整強度不同等等。基礎能力和策略分離設計是內核的基礎設計理念。
如上圖所示,內存規(guī)整是基于內存遷移實現(xiàn)的功能,內核根據(jù)策略在不同實際觸發(fā)內存規(guī)整,用于緩解內存外部碎片問題,可以分層分析看待內存規(guī)整。
2.內存規(guī)整場景
前言中已說明內核當前觸發(fā)內存規(guī)整的策略有四種,為便于查看和直觀理解,優(yōu)先羅列四種場景特點,見下表:
上述表格中各規(guī)整策略詳見2.1~2.4節(jié)描述,compact_control各種含義,詳見3.1節(jié)描述。
FAQ:
(1)直接內存規(guī)整較為特殊,內存分配過程中如果觸發(fā)直接內存規(guī)整依然無法分配內存,那么有可能循環(huán)調用并且提高內存規(guī)整的級別,因此出現(xiàn)首次和重試之分。
(2)規(guī)整頁類型中不包含不可回收頁,除非通過sysctl_compact_unevictable_allowed進行設置。
(3)內存規(guī)整中有一個特例就是alloc_contig_range函數(shù),該函數(shù)用于分配指定地址區(qū)域內存,若這部分內存被占用,會嘗試對這段內存進行規(guī)整遷移,其并非針對zone的規(guī)整,而是針對指定內存區(qū)域的規(guī)整,它的規(guī)整類型與主動內存規(guī)整類似,其實現(xiàn)核心是內存規(guī)整機制,本文不對此邏輯進行說明。
2.1 直接內存規(guī)整(direct compaction)
2.1.1 直接內存規(guī)整觸發(fā)條件
伙伴系統(tǒng)分配內存時,會先以low水線為基準調用get_page_from_freelist函數(shù)嘗試進行內存分配,如果失敗則會進入慢速內存分配流程,即__alloc_pages_slowpath函數(shù),我們對此函數(shù)邏輯稍作刪減,內容如下:
慢速內存分配,會嘗試喚醒kswapd進行內存回收,但并不會等待內存回收的結果,而是直接先調用get_page_from_freelist函數(shù)嘗試內存分配,但這次不同的是使用min水線進行嘗試,如果依然失敗,那么將會根據(jù)gfp標識確認當前分配是否支持直接內存回收,若支持,將會調用__alloc_pages_direct_compact嘗試第一次直接內存規(guī)整以及內存分配。如果依然失敗,則進入喚醒kswapd、get_page_from_freelist、__alloc_pages_direct_reclaim及__alloc_pages_direct_compact循環(huán)調用流程里面來,當然這之中存在眾多條件判斷隨時可能返回頁分配失敗、頁分配成功、重試甚至是觸發(fā)OOM。值得注意的是在慢速內存分配邏輯中,首次調用直接內存規(guī)整時其優(yōu)先級設置為INIT_COMPACT_PRIORITY,這將影響內存規(guī)整觸發(fā)頁遷移的類型,比如INIT_COMPACT_PRIORITY對應的就是MIGRATE_ASYNC即異步遷移類型代表頁遷移時不會阻塞,當然這樣帶來的效果就是規(guī)整或遷移的能力較弱。慢速內存分配邏輯中后續(xù)直接內存規(guī)整調用其規(guī)整優(yōu)先級可能會逐步降低(越低對應規(guī)整強度越高)從而提升內存規(guī)整效用,但是內存規(guī)整可能變?yōu)樽枞?guī)整,這是相互對應邏輯。
通過上述描述,可以初步了解直接內存規(guī)整起到的作用,也可以感受到內核內存分配進入到慢速分配邏輯后性能的代價。
另一方面,直接內存規(guī)整實際是由于伙伴系統(tǒng)無法分配內存時觸發(fā),因此直接內存規(guī)整目標并也并非消除整個zone的外部碎片,而只是通過內存規(guī)整遷移出目標階連續(xù)內存。
2.1.2 直接內存規(guī)整邏輯說明
__alloc_pages_direct_compact函數(shù)是直接內存規(guī)整運行入口,該函數(shù)核心內容如下:
try_to_compact_pages函數(shù)將會進一步調用compact相關流程進行規(guī)整,規(guī)整完成后調用get_page_from_freelist進行內存分配。try_to_compact_pages核心代碼邏輯如下:
try_to_compact_pages函數(shù)核心,遍歷規(guī)定范圍內zone,針對每個zone調用compaction_deferred確認其是否合適進行規(guī)整,若合適進一步調用compact_zone_order函數(shù)進行規(guī)整,規(guī)整成功則直接內存規(guī)整將會直接返回。在try_to_compact_pages函數(shù)中我們重點說明一下zone延遲判斷邏輯,這部分邏輯同樣適用于后續(xù)kcompactd對于zone的判斷。
2.1.2.1 延遲規(guī)整
compaction_deferred函數(shù)用于判斷當前zone是否需要進行延遲處理,延遲的目的是避免頻繁或無效的內存規(guī)整,其引入兩個機制用于延遲,一個是內存規(guī)整失敗階判斷,另一個是內存規(guī)整延遲次數(shù)判斷(這更像一種計時器)。
A) 規(guī)整失敗階的判斷(compact_order_failed)
如果當前規(guī)整階大于等于zone的最大規(guī)整失敗階,那么代表當前再去規(guī)整失敗的可能性很高,建議延遲對當前zone規(guī)整。
B) 內存規(guī)整延遲次數(shù)超閾值判斷
如果失敗階判斷滿足,那么會對延遲次數(shù)進行判斷,compact_considered記錄了當前zone延遲次數(shù),compaction_deferred每次調用時compact_considered都會累加,如果其小于閾值,那么建議zone不進行規(guī)整,標識近期可能已經進行過規(guī)整。
可以想象到,延遲判斷的這些參數(shù)會動態(tài)變化,實際如上圖所示。
A) 當內存規(guī)整成功時,調用compaction_defer_reset函數(shù)清空compact_considered延遲計數(shù),清空compact_defer_shift延遲計數(shù)閾值(defer_limit = compact_defer_shift << 1),同時如果當前order大于等于compact_defer_shift ,則更新compact_order_failed最大規(guī)整失敗階。
總結,當規(guī)整成功時,會降低此zone延遲標準,讓后續(xù)對zone規(guī)整判斷變得更為容易。
B) 當內存規(guī)整失敗時,依然會將compact_considered清零,若order大于更新更新compact_order_failed最大規(guī)整失敗階,增大compact_defer_shift延遲計數(shù)閾值。
總結,當規(guī)整失敗時,會增大此zone延遲標準,讓后續(xù)對zone規(guī)整將會延遲更多次。
通過上述延遲方案,確保對于某個zone不做重復規(guī)整、不做成功率低的規(guī)整,當一次對zone規(guī)整失敗時,內核將會盡量給與zone足夠時間然后再進行嘗試。zone延遲判斷機制適用于直接內存規(guī)整以及kcompactd內存規(guī)整機制,這兩種機制對于耗時較為敏感,其它場景內存規(guī)整通常不需要此機制。
2.1.2.2 capture_control說明
再次回到try_to_compact_pages函數(shù),一個zone在通過延遲判斷后,將會調用compact_zone_order函數(shù),該函數(shù)核心是定義compact_control并調用compact_zone完成規(guī)整。但是這里引入了一個很有意思的機制capture_control,因此需要額外進行說明,這筆修改可見如下內容:
通常的邏輯通過內存規(guī)整遷移出目標階內存塊,再進行內存分配,而為了提效capture_control的思路則是在內存規(guī)整的過程中就將內存分配出來,只不過這個分配更像是截胡,在直接內存規(guī)整的過程中,若發(fā)生內存釋放,則在伙伴系統(tǒng)內存釋放邏輯中截胡合適的內存,下面詳細說明這個過程。
在compact_zone_order函數(shù)會填充capture_control變量,并將其賦值給當前進程上下文,標志著當前進程進入到直接規(guī)整邏輯里面。可以想象在內存規(guī)整過程中涉及內存釋放,此時capture開始行動,代碼如下:
內存釋放流程中通過compaction_capture嘗試捕獲已釋放的內存,compaction_capture函數(shù)代碼實現(xiàn)如下:
釋放內存階必須與直接內存規(guī)整階相等才有可能捕獲,同時需要強調如果當前釋放的是MIGRATE_MOVABLE類型頁盡量不去捕獲,避免污染可移動頁面,因為觸發(fā)直接規(guī)整的有可能是不可移動的內存請求。
2.1.3 直接內存規(guī)整特點
(1)指定了規(guī)整目標階,降低規(guī)整范圍和難度;
(2)遷移頁掃描器和空閑頁掃描器,使用快速掃描能力;
(3)其指定highest_zoneidx和目標階,因此存在水線判斷。
(4)direct_compaction設置直接規(guī)整標識;
直接內存規(guī)整優(yōu)先級COMPACT_PRIO_ASYNC逐步升高,內存規(guī)整強度將會增強,內容如下:
(5)隨著規(guī)整失敗,規(guī)整模式MIGRATE_ASYNC變?yōu)镸IGRATE_SYNC_LIGHT,即直接內存規(guī)整可能是不阻塞也可能是阻塞模式;
(6)隨著規(guī)整失敗,規(guī)整范圍從根據(jù)上次規(guī)整結果制定范圍變?yōu)橥暾鹺one地址范圍;
(7)隨著規(guī)整失敗,pageblock將會被重新掃描,不會根據(jù)標記skip,逐步加強規(guī)整強度;
(8)隨著規(guī)整失敗,空閑頁掃描器將變得嚴格,空閑頁必須來自于MIGRATE_MOVABLE和MIGRATE_CMA可移動的頁面;
上述compact_control結構體參數(shù)含義見3.1節(jié);
2.2 被動內存規(guī)整(kcompactd)
內核在啟動過程中會調kcompactd_init函數(shù),為每個node啟動一個kcompactd內核線程,并且kcompactd線程會運行在與node相對應的CPU核上,在合適的時機kcompactd將會被喚醒進行內存規(guī)整,這就是被動內存規(guī)整邏輯。一個特殊場景是若開啟proactive compaction功能,那么kcompactd會被周期性喚醒。
本節(jié)主要從三個方面說明,分別是kcompactd喚醒條件、kcompactd運行條件、以及kcompactd內存規(guī)整特點(kcompactd被喚醒不一定會進行內存規(guī)整)。
2.2.1 kcompactd喚醒條件
內存規(guī)整模塊向內核提供wakeup_kcompactd口用于喚醒node對應的kcompactd線程,內核中kcompactd喚醒與kswapd強相關,總結如下場景會被被動喚醒:
FAQ:這里指的觸發(fā)內存規(guī)整,指的是調用wakeup_kcompactd函數(shù),未必真的進行內存規(guī)整,wakeup_kcompactd還存在諸多判斷;
2.2.1.1 kswapd運行前觸發(fā)內存規(guī)整
當內存分配失敗時經各種判斷后,會進?內存慢速分配過程,此時伙伴系統(tǒng)將嘗試喚醒內存回收,在這個過程中,有如下關鍵代碼:
上述代碼為未喚醒kswapd前進行內存規(guī)整的條件判斷,其意圖如下:
(1)kswapd內存回收失敗多次;
(2)根據(jù)pgdat_balanced函數(shù)判斷當前水位安全,即存在?夠可?內存并且未出現(xiàn)”偷“內存情況;本質在于,當前內存無法分配的原因并非低內存,此時內存回收可能已經無法解決此問題時,wakeup_kswapd函數(shù)將會提前進行內存規(guī)整。這里還需要說明的是,內存分配指定不支持直接內存回收時上述邏輯才能生效,這是因為若支持直接內存規(guī)整,則可以借助直接內存回收來進行改善并且通常直接內存規(guī)整有更好的性能表現(xiàn)。
2.2.1.2 kswapd運行中觸發(fā)內存規(guī)整
watermark_boost_factor導致的內存規(guī)整,歸類為kswapd運行中觸發(fā)內存規(guī)整稍有牽強,不過其確實是在kswapd內存規(guī)整核心邏輯中觸發(fā)。這里簡單介紹?下watermark_boost_factor特性,當分配內存時如果在對應migrate type上沒有分配到內存,那么系統(tǒng)將會從fall_back的migrate type進行內存分配,有時將其叫做”偷“,由于分配了不匹配遷移類型的內存,內核會認為這可能存在外部碎片的風險,所以當出現(xiàn)這種”偷“時內核會提前進行內存回收及規(guī)整,從而降低后續(xù)”偷“行為的發(fā)生,避免內存碎片問題,提升內存分配的效率,這就是watermark_boost_factor特性。
steal_suitable_fallback函數(shù)是從其它遷移類型上分配內存的核心邏輯,此函數(shù)中會設置?個ZONE_BOOSTED_WATERMARK標志位,這個標志位只能被kswapd清除,伙伴系統(tǒng)在內存分配成功后,如果發(fā)現(xiàn)ZONE_BOOSTED_WATERMARK被置位,將會喚醒kswapd線程。
kswapd函數(shù)通過調用balance_pgdat進行內存回收,而balance_pgdat函數(shù)會在整體內存回收結束后,嘗試喚醒kcompactd線程,當然前提是本次內存回收是boosted屬性的內存回收。
boost特性引起內存規(guī)整,其規(guī)整階為pageblock對應階,在后續(xù)compact_zone函數(shù)介紹中將會說明其影響。上述僅僅簡單描述watermark_boost_factor特性,其還涉及到內存回收時boost導致水線提升等特性,由于其與內存回收關系較大,本文不深入分析,大家可通過如下合入記錄進?步了解:
2.2.1.3 kswapd運行后觸發(fā)內存規(guī)整
kswapd內核線程,每次在內存回收完畢后將會調?kswapd_try_to_sleep嘗試休眠,此函數(shù)會在休眠前調用reset_isolation_suitable函數(shù)清空遷移掃描器和空閑頁掃描器掃描過程中產生的緩存數(shù)據(jù)(比如某個pageblock是否需要跳過信息等等),隨后調用wakeup_kcompactd函數(shù)嘗試進行內存規(guī)整。
簡單說,每次kswapd運行完成后都會嘗試喚醒內存規(guī)整線程,但是內存規(guī)整線程是否真的需要運行,其有復雜的判斷邏輯。
2.2.2 kcompactd運行條件
當kcompactd線程運行時,若當前是非預應性規(guī)整(詳見2.3節(jié)),那么將會調用
kcompactd_do_work函數(shù)進行被動內存規(guī)整。kcompactd_do_work函數(shù)會遍歷當前node的所有zone進行合法性判斷,若符合條件,則調用compact_zone針對zone進?內存規(guī)整,其對zone的判斷邏輯如下:
根據(jù)上述代碼,可以總結如下判斷邏輯:
A. zone是否包含有效物理頁,若不包含,不需要規(guī)整;
B. 當前規(guī)整目標階是否大于等于之前規(guī)整失敗的最小階,若大于不需要規(guī)整;
C. 是否需要延遲規(guī)整,若延遲次數(shù)超過閾值則須規(guī)整,否則不規(guī)整,避免無效規(guī)整;
D. 當前內存水線是否滿足內存申請,若滿足則不規(guī)整;
E. 若當前order大于3同時不滿足內存分配,則評估其內存碎片程度,若小于閾值,則不規(guī)整;其中,populated_zone函數(shù)僅僅判斷當前zone是否存在有效物理頁,其它邏輯在compaction_deferred和compaction_suitable函數(shù)中實現(xiàn)。
2.2.2.1 延遲規(guī)整
在直接內存規(guī)整中已經介紹了延遲判斷的邏輯,對于kcompactd同樣使用,下述代碼可以看到其對延遲參數(shù)的更新邏輯。
如果comapct_zone返回值是由于掃描器相遇導致(一輪掃描結束),而非規(guī)整目標達成導致,代表規(guī)整并未達成目標,同樣按照失敗處理。
2.2.2.2 水線判斷
compaction_suitable函數(shù)一方面通過__compaction_suitable判斷水線是否滿足當前order階內存分配要求,若滿足則當前zone不需要規(guī)整;另一方面,通過fragmentation_index函數(shù)獲取當前zone對于order階內存塊碎片程度評估,如果認為碎片程度不高,則不進行規(guī)整。先來分析__compaction_suitable函數(shù),此函數(shù)用于評估當前水線是否滿足內存分配,若不滿足,則評估水線是否滿足內存規(guī)整過程中內存占用需求,關鍵實現(xiàn)如下:
此函數(shù)中,調用兩次__zone_watermark_ok進行水線判斷,__zone_watermark_ok函數(shù)功能是判斷當前zone在分配order階內存塊后水線是否達標。第一次水線檢查成功,代表當前zone可以滿足內存分配訴求,因此當前zone不用規(guī)整。若第一次調用失敗,則嘗試第二次判斷,第二次水線判斷的目的是確認內存規(guī)整過程中是否可能存在水線問題,因為內存規(guī)整過程中需要order階空閑頁用于內存遷移。因此第二次水線的判斷,僅對0階內存判斷,因為遷移過程中申請空閑頁都是單頁,另一方面watermark增加order階內存塊代表遷移內存占用,但是需要注意的是理論上遷移只需要order階大小的內存,但是實際watermark增加了2倍order階大小的內存(compact_gap函數(shù)),這是由于實際內存規(guī)整過程中由于遷移頁掃描器可能掃描出大于order階待遷移內存,因此空閑頁掃描器也會占用大于order階空閑頁,為了確保評估的安全性,改為2倍order階內存,這部分說明在compact_gap函數(shù)內部注釋中有所描述。
2.2.2.3 各階碎片評分判斷
fragmentation_index函數(shù)用于獲取目標階碎片程度評價,從而評估當前內存無法分配的原因是由于低內存還是外部碎片導致。
其中fill_contig_page_info會遍歷zone內存,統(tǒng)計當前空閑頁以及對應order階空閑區(qū)域數(shù)量,此函數(shù)下文將會詳細介紹,最后通過__fragmentation_index函數(shù)和上述遍歷空閑頁獲得的相關信息進行計算評估。代碼中計算公式如下:
先明確info->free_blocks_total代表當前zone中各個order階內存區(qū)域數(shù)量,info->free_pages代表當前zone中空閑頁的數(shù)量,requested代表order階對應頁數(shù)量。我們命名info->free_pages/requested為target_order_blocks,在當前空閑頁狀態(tài)下若不存在內存碎片問題這種理想狀態(tài)下?lián)碛卸嗌賯€order階內存塊,現(xiàn)在重新簡化公式,即可獲得如下描述:
極限狀態(tài)info->free_blocks_total非常大時,意味著嚴重的內存碎片問題,上述值趨近于1,反之0代表內存不足問題。__fragmentation_index函數(shù)引入1000這個數(shù)值參與運算是為了避免小數(shù),導致返回值不易判斷,現(xiàn)在引入此值后,使函數(shù)返回值落入-1000,0~1000范圍中,其中-1000場景較為特殊,其代表當前zone滿足內存分配需求,但是在此之前卻通過了上文中__compaction_suitable函數(shù)的判斷,當前-1000返回值實際在代碼中似乎并未被使用。重點還是回到0~1000返回值,那么數(shù)值越大代表對于當前order階內存塊而言碎片程度越高,難以分配。
再回到compaction_suitable函數(shù),有兩個關注點,一個是對于內存分配階大于3的申請才會利用fragmentation_index進行額外判斷(對于較小內存需求評估其內存碎片程度意義降低,例如單頁內存分配,伙伴系統(tǒng)基本分配單元,就不涉及內存外部碎片問題);另一個是,fragmentation_index返回值需要和sysctl_extfrag_threshold閾值進行比較,如果小于閾值,則不進行規(guī)整,此值通過/proc/sys/vm/extfrag_threshold進行設置。
FAQ:基于上述邏輯,內核中可以通過/sys/kernel/debug/extfrag/extfrag_index文件節(jié)點查看各個階外部碎片評估數(shù)據(jù),其數(shù)值對1000取余為小數(shù),越趨近于1,代表當前order階內存碎片程度高,相關代碼如下:
2.2.3 kcompactd規(guī)整特點
內存規(guī)整的特點還是需要從其配置compact_control來進行說明,下述代碼為kcompactd規(guī)整CC設置。
(1)指定了規(guī)整目標階,降低規(guī)整范圍和難度;
(2)遷移頁掃描器和空閑頁掃描器,使用快速掃描能力;
(3)其指定highest_zoneidx和目標階,因此存在水線判斷。
(4)規(guī)整模式為MIGRATE_SYNC_LIGHT,輕度同步模式,會阻塞;
(5)規(guī)整范圍從根據(jù)上次規(guī)整結果繼續(xù)規(guī)整;
(6)pageblock將會根據(jù)標記選擇跳過,避免頻繁掃描;
上述compact_control結構體參數(shù)含義見3.1節(jié);
2.3 預應性內存規(guī)整(proactive compaction)
這屬于新增內存規(guī)整特性,其最初目的是降低大頁分配延遲,通過大頁內存塊碎片程度決策當前是否啟動內存規(guī)整,提前減少內存碎片,提升大頁分配性能。以下鏈接對此特性做了原理性說明:
https://lwn.net/Articles/817905/
實際最終代碼與上文的最初設想已不相同,下文將依據(jù)代碼說明,代碼合?記錄如下:
https://lore.kernel.org/all/[email protected]/T/#u
2.3.1 觸發(fā)預應行規(guī)整
預應性規(guī)整并非獨立存在,其融?kcompactd內核線程,但又與kcompactd原有功能互斥。關鍵代碼如下:
上述代碼總結如下:
(1)若設置預應性規(guī)整,kcompactd將會每500ms(HPAGE_FRAG_CHECK_INTERVAL_MSEC宏設置)喚醒判斷當前是否需要進行規(guī)整;
(2)若設置預應性規(guī)整,則會跳過kcompactd原有邏輯,調用預應性規(guī)整判斷及規(guī)整邏輯;
(3)預應性規(guī)整觸發(fā)的依據(jù)是根據(jù)整個node的碎片化程度決策;
(4)預應性規(guī)整會在規(guī)整前后統(tǒng)計碎片得分,若得分增加,達標碎片問題未解決,那么會增加kcompactd喚醒周期,避免頻繁無效的預應性規(guī)整;
(5)每輪循環(huán)后,均會關閉預應性規(guī)整,這是考慮到在某些嵌入式場景下并不需要頻繁的喚醒并判斷,將啟動預應性規(guī)整策略交給用戶層,如下合入增加了這個功能:
https://lore.kernel.org/all/1627653207-12317-1-git-send-emailcharante@
codeaurora.org/T/#u
預應性規(guī)整在內核中引入vm.compaction_proactiveness文件節(jié)點,可以向其寫入0~100數(shù)值,其用于指定預應性規(guī)整的積極性,數(shù)值越大積極性越高(見2.3節(jié)),如果設置為0相當于關閉,設置此數(shù)值時,pgdat->proactive_compact_trigger將會被設置為true,預應性內存規(guī)整功能打開,此節(jié)點實現(xiàn)不過多描述。
2.3.2 碎片程度評估
預應性內存規(guī)整的碎片化評價,實際是對大頁碎片程度的評價,其本身也是為了解決大頁分配延遲而產生,與上文各order階內存碎片評估方式不同,目前也并不針對其它order階內存,通過如下代碼進?定義:
即便未開啟大頁功能,COMPACTION_HPAGE_ORDER通常也會被設置為9,代表要預應性內存規(guī)整主要是針對2MB內存碎片程度進行評估,下面從最頂層代碼進行說明。
2.3.2.1 should_proactive_compact_node
should_proactive_compact_node 函數(shù)計算當前node碎片程度,當然是針對
COMPACTION_HPAGE_ORDER階內存塊,其將會對每個zone進行評估得分,并將所有zone所得分數(shù)累加,獲得最后得分。此得分如果高于預應性規(guī)整水線線,代表碎片化程度較高,需要進行預應性規(guī)整。之前通過vm.compaction_proactiveness節(jié)點設置積極性,將會影響預應性規(guī)水線,其值越高,預應性水線值越低,將越容易觸發(fā)規(guī)整。預應性水線判斷邏輯如下:
注意,預應性規(guī)整是?種預操作,應盡可能降低對系統(tǒng)性能影響,因此當kswapd運行時,預應性規(guī)整不會啟動。
2.3.2.2 fragmentation_score_wmark
fragmentation_score_wmark 函數(shù)將會獲取當前水線值,當low入?yún)⒃O置為true時,獲取的是低水線,否則獲取高水線。
預應性水線的計算不復雜,compaction_proactiveness設置的越低,低水線值越高,高水線通常比低水線高10,但是高低水線都在100數(shù)值以內。
2.3.2.3 fragmentation_score_node
fragmentation_score_node函數(shù)實現(xiàn)清晰,即針對每?個zone計算一個得分,并進行累加,即為node的最后得分。
由于每個zone大小不同,因此得分應有對應比重,fragmentation_score_zone_weighted函數(shù)完成這個計算,實現(xiàn)方式較為直接。
zone碎片評分乘以zone的有效內存頁數(shù)量,再除以整個node有效內存數(shù)量,即為zone碎片程度得分的比重值。(zone有效頁數(shù)量 / node有效頁數(shù)量) * zone當前得分簡單來說就是按照內存大小重進?計算。
fragmentation_score_zone函數(shù)用于計算每個zone碎片得分,其實現(xiàn)原理是統(tǒng)計當前zone中?于等于COMPACTION_HPAGE_ORDER階空閑區(qū)域的個數(shù),并計算除此之外空閑內存內存與當前zone空閑內存百分比,這個占比可以說明碎片程度,此值越高,說明符合COMPACTION_HPAGE_ORDER階內存塊越少。
fill_contig_page_info函數(shù)用于獲取當前zone空閑頁、多少個COMPACTION_HPAGE_ORDER階內存塊等等,此函數(shù)將會遍歷當前zone上所有order的空閑鏈表進行累加計算,對于order等于COMPACTION_HPAGE_ORDER的階內存塊個數(shù)累加,對于order大于COMPACTION_HPAGE_ORDER階內存塊會拆分累加(因為包含多個COMPACTION_HPAGE_ORDER階空閑內存區(qū)),此函數(shù)實現(xiàn)并不復雜,不展開描述。
2.3.3 預應性規(guī)整特點
經過上述的判斷,終于可以開始預應性內存規(guī)整,proactive_compact_node函數(shù)實現(xiàn)此功能,代碼如下:
從compact_control結構體的設置可以看出,預應性規(guī)整與下文主動規(guī)整類似,其指定參數(shù)有如下特點:
(1)不指定目標階,規(guī)整持續(xù)進行,待掃描結束;
(2)遷移頁掃描器和空閑頁掃描器,不使用快速掃描能力;
(3)規(guī)整模式為MIGRATE_SYNC_LIGHT,輕度同步模式,會阻塞;
(4)規(guī)整范圍為完整zone地址空間;
(5)pageblock將會不會根據(jù)標記選擇跳過;
上述compact_control結構體參數(shù)含義見3.1節(jié);
2.4 主動內存規(guī)整
主動內存規(guī)整主要是指用戶通過設備節(jié)點觸發(fā)完整內存規(guī)整或針對node內存規(guī)整。內核提供兩個設備節(jié)點用于觸發(fā)內存規(guī)整。
通過compact_memory節(jié)點可以觸發(fā)當前所有node以及下屬所有zone的內存規(guī)整,這個操作的成本非常高。另一個compact節(jié)點只有在NUMA系統(tǒng)上存在,可以僅觸發(fā)某一個node進行規(guī)整。無論如何主動內存規(guī)整觸發(fā)邏輯是簡單明了。
2.4.1 主動內存規(guī)整特點
主動內存規(guī)整compact_control設置較為簡單,解釋如下:
(1)不指定目標階,規(guī)整持續(xù)進行,待掃描結束;
(2)遷移頁掃描器和空閑頁掃描器,不使用快速掃描能力;
(3)規(guī)整模式為MIGRATE_SYNC,同步模式,會阻塞;
(4)規(guī)整范圍為完整zone地址空間;
(5)pageblock將會不會根據(jù)標記選擇跳過;
上述compact_control結構體參數(shù)含義見3.1節(jié);
主動內存規(guī)整,是內存規(guī)整最全面的方法,當然其帶來的性能影響也會最大,默認情況下內核并不會觸發(fā)這類內存規(guī)整。
3.內存規(guī)整
前言中已經簡述規(guī)整的大致思路,將一些頁遷移聚攏,騰出更多連續(xù)空間。那么在真正實現(xiàn)時實際需要解決的問題是內存規(guī)整范圍是什么,如何找到需要遷移的頁,什么頁適合遷移,規(guī)整何時結束等問題。上述的問題最終都在compact_zone函數(shù)中被解決。
compact_zone函數(shù)針對單個zone內存區(qū)進行內存規(guī)整,這是內存規(guī)整的最小單元。其通過遷移頁掃描器從低地址到高地址尋找遷移頁,通過空閑頁掃描器從高地址到低地址尋找空閑頁,最終將掃描出的遷移頁遷移至掃描出的空閑頁,完成內存規(guī)整,如下圖所示:
更細節(jié)一些來說,內存規(guī)整開始后,先通過遷移頁掃描器掃描,并且掃描的單位為一個pageblock,將當前pageblock中可遷移的頁隔離后放入到待遷移的鏈表。隨后調用空閑頁掃描器掃描,空閑頁掃描器依然以pageblock為步長,但不再限制掃描一個pageblock,其掃描的目標是找到大于等于當前遷移頁數(shù)量的空閑頁,上述中綠色和黃色箭頭長度不同即想表達這個邏輯。上述掃描過程將會產生遷移頁和空閑頁,用于后續(xù)內存遷移,這樣就完成了內存規(guī)整的一輪操作,可能與大家理解不同,內存規(guī)整并非一次性掃描zone然后再遷移,而是以這種一步一步的方式進行遷移,這能平攤內存規(guī)整對性能帶來的風險,并且每輪處理后都有機會判斷當前內存規(guī)整是否可以退出。上文描述已經非常清晰勾畫了compact_zone函數(shù)的核心邏輯,但是實際上實現(xiàn)可能更加復雜,比如什么條件下規(guī)整可以提前結束、遷移頁掃描器和空閑頁掃描器是否可以加速等等,這些都屬于更高層面優(yōu)化的話題,下文將會簡述。
再次回到compact_zone函數(shù),將復雜的代碼剝離可以很容易得到如下核心代碼邏輯:
(1)compact_finished函數(shù)用于判斷當前規(guī)整是否結束
(2)isolate_migratepages是遷移頁掃描器實現(xiàn),用于查找需要移動的頁;
(3)isolate_freepages是空閑頁掃描器實現(xiàn),用于查找用于頁遷移的空閑頁;
(4)migrate_pages是頁遷移函數(shù),將上述兩個掃描器掃描結果進行頁遷移處理,完成規(guī)整;
至此,compact_zone大體邏輯已經完成說明,下文將會對(1)~(3)函數(shù)進行細致描述。
3.1 內存規(guī)整參數(shù)說明
compact_control控制了compact_zone的諸多行為,不同場景觸發(fā)內存規(guī)整訴求不同,因此參數(shù)也不同,這里初步羅列常用參數(shù)含義,有助于理解不同場景下內存規(guī)整的差異。
3.2 遷移頁掃描器(migrate scanner)
isolate_migratepages函數(shù)實際就是遷移頁掃描器的代碼實現(xiàn),其通常會從低地址到高地址完整或部分掃描zone區(qū)域,以pageblock為步長選擇一個合適pageblock調用isolate_migratepages_block函數(shù)進行內存隔離,需要注意的是isolate_migratepages函數(shù)在處理完一個pageblock后就會退出,換句話說此函數(shù)一次調用只處理一個合適的pageblock。核心代碼如下所示:
總結一下,isolate_migratepages函數(shù)主要通過如下三個函數(shù)調用完成整個isolate工作:
A)快速尋找合適pageblock或返回而遷移掃描起始位置(fast_find_migrateblock)
B)判斷pageblock是否合適進行隔離(suitable_migration_source)
C)實際對一個pageblock進行隔離頁搜索操作(isolate_migratepages_block)
FAQ:注意若未找到合適pageblock,那么會持續(xù)進行線性遍歷查找,直到地址超過cc->free_pfn(最開始時此值應為zone地址區(qū)域結尾處)。
下面將會對上述三個函數(shù)進行解析,完整描述isolate_migratepages函數(shù)功能細節(jié)。
isolate_migratepages函數(shù)會返回三個返回值,內容如下:
這些返回值將會影響compact_zone函數(shù)的返回值。
3.2.1 遷移頁掃描器快速掃描
(fast_find_migrateblock)
fast_find_migrateblock函數(shù)會嘗試快速尋找一個pageblock來進行規(guī)整,如果無法找到則返回cc->migrate_pfn(注:此值初始應為zone的起始地址,后續(xù)應記錄上一次遷移掃描器掃描結束位置避免后續(xù)重復掃描)作為起始地址開始遍歷,通常考慮不就是從zone內存區(qū)域的起始地址尋找一個么,但這樣可能并不高效,在之前版本中,遷移頁掃描器每次循環(huán)確實是基于zone的某個地址開始進行線性遍歷,這是一種線性搜索的過程,但是隨后引入了如下補丁,改變了這一現(xiàn)狀:
https://patchwork.kernel.org/project/linux-mm/patch/[email protected]/
其目的是盡量選擇一個充斥著可移動空閑頁pageblock塊,這樣通過較少頁遷移,就可以滿足高階order內存申請需求。通過查找freelist空閑內存塊反向查找對應pageblock,這樣效率非常高,另一方面,由于pageblock選擇不是簡單的順序查找,為了避免后續(xù)掃描重復pageblock還需要將其進行單獨標識,通過set_pageblock_skip函數(shù)完成設置,確保再次進行掃描時會跳過這個pageblock內存塊。以上就是主要思路,但是具體在實現(xiàn)上有很多細節(jié)比如:
(a)如果規(guī)整目標order太小,那么完全沒必要去尋找,依然使用cc->migrate_pfn作為起始地址;
(b)由于對于cc->order有要求,因此僅適用于直接內存回收和異步內存回收(僅這兩種場景會指定cc->order,其它場景cc->order為-1);
(c)尋找空閑頁所在pageblock必須是在內存搜索范圍的前1/2或1/8,這部分是最有可能被遷移頁掃描到得區(qū)域,避免影響到空閑頁掃描;
(d)空閑頁掃描會改變free_list布局,盡量保證下次掃描free_list不重復掃描空閑內存塊;
(e)若這只cc->ignore_skip_hint,則遷移頁掃描器不采用遷移頁的fast機制;
(f)僅搜索可移動空閑頁,并且搜索范圍從order - 1階開始;
fast_find_migrateblock函數(shù)若無法找到合適pageblock,那么將會返回cc->migrate_pages退化為正常線性掃描,請注意的是fast_find_migrateblock函數(shù)每次也只找到一個pageblock并設置其為skip,通過上述方式確實在一定程度上能夠加速針對目標階內存規(guī)整,能夠更快整理出目標階內存需求,但是對于完整內存規(guī)整并無實質效果,因此fast加速查找僅適用于直接內存規(guī)整和kcompactd被動內存規(guī)整,因為這些場景下通常會指定規(guī)整目標階。核心代碼如下:
無論如何,通過fast_find_migrateblock函數(shù)我們可以找到一個待遷移pageblock或者返回一個遷移頁掃描起始地址(cc->migrate_pfn),用于后續(xù)針對pageblock內存遷移頁掃描使用。
3.2.2 suitable_migration_source函數(shù)解析
suitable_migration_source函數(shù)用于判斷當前pageblock是否可以進行隔離及遷移,這部分邏輯相對簡單。
這里需要關注的是,如果非直接內存規(guī)整或非遷移類型非ASYNC模式,則不需要判斷pageblock遷移類型與compact_control遷移類型是否匹配,盡可能進行內存規(guī)整。
3.2.3 pageblock內存隔離(isolate_migratepages_block)
isolate_migratepages_block函數(shù)會在單個pageblock內進行遍歷,嘗試將符合規(guī)整要求的頁放入對應compact_control所指向的migratepages鏈表,進行隔離,用于后續(xù)頁遷移操作。函數(shù)整體結構大致如下:
起始會通過too_many_isolated函數(shù)檢查當前內存節(jié)點上是否存在過多isolated頁,如果數(shù)量過大(isolated > (inactive + active) / 2)那么根據(jù)MIGRATE模式選擇處理方法,比如對于異步模式就是直接退出,對于同步模式函數(shù)將會在這里進行等待一段時間,再循環(huán)檢查是否合適繼續(xù)向下執(zhí)行。隨后就是通過for循環(huán)開始遍歷這個pageblock里面所有頁,并對每一個頁進行判斷,決定其處理方法,這是一個復雜的過程,下文拆分代碼進行說明。
3.2.3.1 大頁處理
對于大頁處理,一般情況下內存規(guī)整是會選擇略過,不進行整理。處理代碼如下:
從代碼看當頁為復合頁并且cc->alloc_contig為false時,此頁將不會被規(guī)整。無論是hugetlbfs和THP大頁都屬于復合頁,那么問題的關鍵來到cc->alloc_contig是什么?
從代碼進一步推進可以看到,通過alloc_contig_range函數(shù)進行內存分配時,此函數(shù)會指定申請內存地址范圍并盡力實現(xiàn),如果指定范圍已經被占用,會嘗試觸發(fā)直接規(guī)整進行頁遷移,如下代碼所示:
可以看到,這種情況下會將alloc_contig參數(shù)設置為true,在此邏輯中當調用到isolate_migratepages_block函數(shù)是會嘗試規(guī)整大頁,其實際的做法是通過isolate_or_dissolve_huge_page函數(shù)實現(xiàn)大頁溶解,這部分涉及大頁邏輯不再發(fā)散。
總結,只有當頁是hugetlbfs大頁并且通過alloc_contig_range函數(shù)調用下來觸發(fā)內存規(guī)整時才會進行處理,其它場景下大頁處理策略都是略過。
3.2.3.2 空閑物理頁處理
空閑頁的處理為直接跳過,這無可厚非,唯一需要注意的時,空閑頁跳過時并非單頁跳過,而是根據(jù)頁的order階進行跳過,代碼如下:
3.2.3.3 non-LRU物理頁
這個很有意思,上文談到大部分可移動頁應該都是用戶態(tài)的匿名頁,這里怎么還會有不再LRU上的物理頁呢,實際這涉及到頁遷移特性的一種功能,有興趣的朋友可以閱讀一下"Documentation/vm/page_migration.rst"文章中"Non-LRU page migration"這一小節(jié)。內核中申請的內存通常都是non-LRU上并且不可移動,但是內核提供了定制能力,開發(fā)者可以在內核驅動中將自己申請的內存標記為可移動,為此內核為page添加了兩個新的flag即PG_movable和PG_isolated用于標識這種non-LRU并且可遷移的頁。開發(fā)者通常使用__SetPageMovable接口主動設置這些內存頁PG_movable標記,而PG_isolated標識此頁已經被隔離,開發(fā)者不需要主動設置此標記。
現(xiàn)在我們應該可以理解上述代碼中對于__PageMovable(page)判斷,如果一個non-LRU頁被設置了PG_movable并且PG_isolated還未被設置,那么代表這個頁也是可以進行遷移,隨后將會調用isolate_movable_page函數(shù)進行隔離操作。
問題還沒有結束,實際想要讓這些在內核中直接申請的頁變?yōu)榭蛇w移,光設置標記還不行,開發(fā)人員需要自定義這些頁如何隔離以及如何遷移,因此內核要求,開發(fā)者需要在address_space_operations結構體里面實現(xiàn)isolate_page、migratepage及putback_page函數(shù)。現(xiàn)在回到isolate_movable_page函數(shù),此函數(shù)將會調用開發(fā)人員注冊的isolate_page函數(shù)完成這些頁隔離操作,代碼如下:
3.2.3.4 pinned匿名頁
如果匿名頁已經被mlock等接口pin住,那么將會略過。
一方面,通過page_mapping判斷當前頁是否為文件頁;另一方面,通過page_count(page) > page_mapcount(page)判斷是否被pin住,匿名頁被pin住時會增加_refcount數(shù)值。
3.2.3.5 GFP_NOFS配置下僅處理匿名頁
__GFP_FS表示內存分配過程中可以觸發(fā)文件操作,如果compact_control中gfp_mask不帶__GFP_FS則結果依賴page_mapping返回值,對于匿名頁而言page_mapping返回NULL,因此上述代碼判斷實際的含義是當分配上下文為無__GFP_FS并且是文件頁時將會略過,另一方面也可以解釋為GFP_NOFS時僅處理匿名頁。
那么什么時候compact_control中gfp_mask不帶__GFP_FS,前文說明了觸發(fā)內存回收的場景,在內存分配失敗時有可能導致直接觸發(fā)內存規(guī)整,此時內存分配GFP標記將會被賦值到compact_control中用于配置內存規(guī)整行為。
可以想象這么做的原因,實際在內存分配的過程中直接觸發(fā)內存規(guī)整其系統(tǒng)并不希望耗費過多時間,做有限度規(guī)整更為合適。
其它觸發(fā)內存規(guī)整的場景,通常compact_control中gfp_mask為GFP_KERNEL,這是包含__GFP_FS,因此規(guī)整涉及的內存范圍通常更廣。
3.2.3.6 不同isolate模式會影響頁的處理策略
__isolate_lru_page_prepare完成此任務,關鍵代碼如下:
此處邏輯是根據(jù)隔離類型篩選可遷移的物理頁,隔離類型來源于cc-mode也就是遷移類型,代碼如下:
通常,僅當MIGRATE_ASYNC和MIGRATE_SYNC_LIGHT模式時,其隔離模式為ISOLATE_ASYNC_MIGRATE異步模式,在這種模式下其會盡可能避免隔離可能會阻塞頁,比如代碼中正在回寫的頁或者是臟頁,這里注意如果頁為臟頁,但是其并非文件頁(swap匿名頁)或擁有自己migratepage函數(shù)那么頁被認為遷移過程不會被阻塞,否則都無法隔離。如果isolate_mode為ISOLATE_UNEVICTABLE,代表本次隔離可以處理不可回收頁,這個主要是針對那些被lock住的unevictable頁,這些頁不能夠被回收但是支持遷移。
3.2.3.7 修改LRU即真正意義isolate(隔離)
在完成(1)~(6)的判斷后,剩下頁將能夠被隔離,隔離的含義就是將其從LRU鏈表去除(這個LRU有可能是來自于pglist_data也可能來自于頁對應memcg),隨后將這些頁添加至cc->migratepages,用于后續(xù)頁遷移。
3.2.3.8 總結
isolate_migratepages_block函數(shù)是內存規(guī)整過程中頁隔離的重要函數(shù),其確定哪些頁應該被隔離,哪些頁應該被略過,其基本策略如下:
(1)大頁不應被隔離,但是alloc_contig_range場景下有可能觸發(fā)hugetlbfs大頁溶解,但這已不屬于內存規(guī)整場景;
(2)空閑物理頁,不會被隔離;
(3)non-LRU物理頁,作為在內核分配內存,如果開發(fā)者為其實現(xiàn)isolate_page、migratepage及putback_page函數(shù),則可以被隔離或遷移;
(4)被Pin住的匿名頁,不會被隔離;
(5)GFP_NOFS分配上下文僅隔離匿名頁;
(6)不同isolate模式會影響頁的處理策略,比如ISOLATE_ASYNC_MIGRATE不會隔離正在回寫的頁或臟頁;
3.3 空閑頁掃描器(free scanner)
isolate_freepages函數(shù)是空閑頁掃描器的核心邏輯,但是compact_zone中對于空閑頁掃描器調用并不直接,而是通過migrate_pages間接調用。
migrate_pages是內存遷移的基礎接口,其核心功能是將from鏈表中的頁遷移至空閑頁,空閑頁如何獲取則在get_new_page中實現(xiàn),這是一個函數(shù)指針,在compact_zone函數(shù)中,此接口的實際調用形態(tài)如下:
cc->migratepages就是之前通過isolate_migratepages隔離出來頁,compaction_alloc則實現(xiàn)如何獲取空閑頁,可以想象isolate_freepages函數(shù)會在此調用,也是本節(jié)分析的重點。compaction_alloc函數(shù)并不復雜,其用于為內存規(guī)整頁遷移時申請遷移的目的內存,代碼如下:
cc->migratepages保存了需要進行規(guī)整遷移的頁,也就是遷移掃描器掃描的結果。
cc->freepages保存了頁遷移的目的空閑頁,也就是空閑頁掃描器掃描的結果。
當cc->freepages為空時,嘗試調用空閑頁掃描器isolate_freepages函數(shù)嘗試掃描隔離更多空閑頁用于頁遷移,為什么要隔離呢?隔離的本質是將其從伙伴分配系統(tǒng)中取出不再參與系統(tǒng)內存分配,僅用于內存規(guī)整遷移使用。
isolate_freepages與上文isolate_migratepages函數(shù)相對應,用于隔離空閑頁,用于頁遷移。此函數(shù)也是free scanner(空閑頁掃描器的核心邏輯)。其實現(xiàn)邏輯也與isolate_migratepages函數(shù)相似,核心邏輯大致如下:
isolate_freepages函數(shù),會根據(jù)cc->free_pfn開始反向以pageblock為單位進行遍歷,如果遇到合適pageblock,則會進一步對pageblock中的頁進行遍歷,將其中合適的空閑頁進行隔離,放入cc->freepages鏈表中用于后續(xù)頁遷移使用,當收集的空閑頁足夠遷移時將會退出。上圖僅描述核心邏輯與實際實現(xiàn)有一些出入,比如fast_isolate_freepages機制引入,就會導致上述反向線性遍歷的過程改變,但是上圖已經比較簡要說明了空閑頁掃描器的工作原理。
上文從函數(shù)調用角度描述isolate_freepages函數(shù)邏輯,下文對部分重要函數(shù)調用做詳細分析。
3.3.1 空閑頁掃描器快速掃描(fast_isolate_freepages)
常規(guī)情況下,系統(tǒng)通過從cc->free_pfn開始反向遍歷尋找一個合適pageblock,隨后針對這個pageblock隔離其空閑頁。但是這有可能低效,比如第一個pageblock里面并沒有多少空閑頁,那么針對這個pageblock進行大部分操作都是無效,fast_isolate_freepages就是為改善這個問題,其并不從cc->free_pfn開始進行線性查找,而是借助伙伴系統(tǒng)中free_list快速找到一塊合適空閑區(qū)域進行隔離,從某種角度看這已經不是基于pageblock的處理了。這與fast_find_migrateblock函數(shù)目標類似均為提升掃描器效率。詳細代碼功能描述如下:
(1)首先選取合適order即cc->search_order,通常開始此值為cc->order - 1;
FAQ:此功能也是應用在直接內存回收和kcompactd場景下,因為其快速搜索的前提是cc->order和cc->search_order。其它場景下觸發(fā)的內存規(guī)整,cc->order為-1,其直接返回cc->free_pfn,也就蛻變?yōu)榫€性搜索的模式;
(2)在order的free_list中進行空閑頁的遍歷查找;
(3)查找到合適空閑頁后,如果空閑頁落入圖中下圖中綠色區(qū)域,那么此空閑區(qū)域就會被優(yōu)先隔離,這里注意并非以pageblock為單位進行了(min_pfn為1/2處,low_pfn為3/4處);
這個邏輯并不復雜,內存規(guī)整期望內存向后方遷移,如果空閑頁太靠前,極端點,如果空閑頁落入紅色區(qū)域,內存規(guī)整掃描器容易快速相遇導致無法解決內存碎片的問題;
那么,如果空閑頁落入min_pfn和low_pfn之間,那么系統(tǒng)會降低在當前階freelist遍歷機會,傾向于降階在新search_order階的freelist中尋找綠色區(qū)域的空閑頁;除此以外這種場景下,系統(tǒng)還會記錄當前search_order對應freelist搜索記錄(這會改變freelist內存塊順序),后續(xù)可避免額外搜索,代碼如下:
(4)對于(3)已經找到search_order次的空閑區(qū)域,將直接調用__isolate_free_page(接口分析詳見3.3.2.1節(jié))函數(shù)完成隔離,隨后將這些頁放入cc->freepages鏈表,完成整個操作;
(5)到這里,已經成功隔離了search_order階空閑頁,這并不針對pageblock,并且對于是否已經滿足遷移需求數(shù)量也并沒有約束,所以在函數(shù)末尾調用了fast_isolate_around函數(shù),此函數(shù)本質是根據(jù)當前需求,確認是否需要針對當前空閑區(qū)域所在pageblock再額外進行隔離,代碼如下:
簡單總結,代碼邏輯如下:
如果當前已隔離的綠色區(qū)域已經滿足訴求,那么此函數(shù)將會直接退出,如果不滿足,將會嘗試將這個空閑區(qū)域對應pageblock左側和右側區(qū)域通過isolate_freepages_block函數(shù)進行空閑頁隔離。
無論如何,fast_isolate_freepages接口都嘗試以更快速的方式獲取空閑頁,有可能這個空閑頁并不是緊貼著cc->free_pfn,但是它一定在后1/4范圍內,并且它會改變freelist的結構避免重復判斷相同空閑頁,這是一個優(yōu)化功能。對于內存規(guī)整若想簡單理解,可以忽略此處細節(jié)直接理解為從zone末尾開始線性查找。
3.3.2 空閑頁隔離(isolate_freepages_block)
isolate_freepages_block函數(shù),即在指定內存范圍內正向遍歷,將合適的空閑頁進行隔離加入到cc->freepages鏈表,用于后續(xù)頁遷移,這是isolate_freepages函數(shù)得核心函數(shù)調用,通常isolate_freepages每次調用會傳遞一個pageblock范圍進行空閑頁隔離。關鍵代碼如下:
函數(shù)關鍵邏輯說明:
(1)函數(shù)在pageblock內遍歷并不一定按照頁為單位,參數(shù)stride作為步長存在;
(2)復合頁將會略過;
(3)非空閑頁將會略過;
(4)符合上述要求空閑頁通過__isolate_free_page進行隔離操作,隨后將這些加入到cc->freepages鏈表;
(5)如果當前收集空閑頁已經大于當前已經收集遷移頁則退出循環(huán);
上述即isolate_freepages_block函數(shù)的邏輯,這里面需要關注一個strict參數(shù),如果該參數(shù)為true,那么isolate_freepages_block函數(shù)將會以頁為遍歷單位進行遍歷及隔離,并且不會再根據(jù)上述(5)條件提前退出,而是完整隔離整個pageblock中合適的空閑頁。
3.3.2.1 伙伴系統(tǒng)處理(__isolate_free_page)
空閑頁隔離與遷移頁的隔離不太相同,由于空閑頁還屬于伙伴系統(tǒng)管轄范圍內,伙伴體統(tǒng)提供專用隔離接口,即__isolate_free_page函數(shù)。
此結構邏輯清晰,不過多贅述。
3.4 內存規(guī)整退出判斷(compact_finished)
compact_finished用于判斷當前規(guī)整是否結束,有多種不同條件導致規(guī)整結束,并且返回值不同,由于compact_finished函數(shù)較長并且對于理解內存規(guī)整較為重要,因此代碼拆分說明。
3.4.1 掃描器相遇
上文說明migrate scanner從正向掃描,free scanner反向掃描,當兩者相遇,代表掃描和遷移操作結束,因此規(guī)整結束,這是最為正常的一種退出方式,代碼如下:
掃描器相遇場景退出,上述代碼注釋完整標記其邏輯,對于如何判斷掃描器相遇,實際根據(jù)cc->free_pfn和cc->migrate_pfn的大小容易判斷,不進行函數(shù)代碼說明。
3.4.2 預應性規(guī)整退出條件
這里預應性規(guī)整除了掃描器相遇退出條件外,擁有額外退出條件。
fragmentation_score_zone和fragmentation_score_wmark均為預應性規(guī)整碎片評估函數(shù),簡單說當前如果對于大頁階碎片評估分數(shù)低于預應性碎片水線時,則停止規(guī)整,返回成功。關于預應性規(guī)整碎片評估邏輯詳見2.3.2節(jié)。
3.4.3 direct規(guī)整模式額外退出條件
直接內存規(guī)整在識別是否成功時,如果判斷當前申請需求已滿足,并且分配遷移類型也滿足一定要求即可退出直接規(guī)整邏輯。為何在申請可以滿足的情況下還要滿足一定要求才能退出呢,主要考慮是即便滿足分配,但也不能引入潛在擴大內存碎片化的情況,否則將會頻繁進入直接內存規(guī)整。
3.4.4 返回值總結
1.COMPACT_CONTINUE:代表內存規(guī)整未結束,繼續(xù)規(guī)整;
2.COMPACT_COMPLETE:在cc->whole_zone為true場景下,完成全區(qū)域掃描和規(guī)整,將返回此值;
3.COMPACT_PARTIAL_SKIPPED:多種場景下均會返回此值,例如:
(1)cc->whole_zone為false場景下,掃描和規(guī)整完成,將會范圍此值;
(2)在proactive_compaction模式下,如果此時kswapd運行,規(guī)整也將會停止,返回此值;
4.COMPACT_SUCCESS:代表規(guī)整成功,此值也是多種場景下均會返回:
(1)proactive規(guī)整模式下,碎片化得分達標,主動退出規(guī)整,即返回成功;
(2)direct規(guī)整模式下,需求order階及遷移類型鏈表上,已有足量內存,即返回成功;
(3)direct規(guī)整模式下,需求order階上如果有足量CMA內存,前提是本身需求也是可遷移頁(否則CMA內存申請時無法遷移),即返回成功;
(4)direct規(guī)整模式下,可以從其它遷移類型偷到內存,在滿足一定條件下也會范圍成功;
5.COMPACT_CONTENDED:若當前進程被強制退出或依然持有zone lock,則規(guī)整邏輯返回此值,屬于一種異常退出狀態(tài);
4.內存規(guī)整總結
代碼分析基本完成,再對內存規(guī)整統(tǒng)計信息及可調文件節(jié)點進行簡要說明。
4.1 內存規(guī)整統(tǒng)計信息
在上文的代碼描述中忽略內存規(guī)整相關信息統(tǒng)計邏輯,統(tǒng)計信息可以通過/proc/vmstat文件節(jié)點進查詢,相關信息含義說明如下:
4.2 內存規(guī)整文件節(jié)點
4.3 總結
內存規(guī)整是一個較重內存碎片優(yōu)化措施,在使用時內核較為謹慎,當前有直接內存規(guī)整、kcompactd內存規(guī)整、預應性內存規(guī)整及主動內存規(guī)整四種場景,這些場景涵蓋在內存分配、內存回收等上下文,由于規(guī)整的訴求和緊迫程度不同,其通過compact_control結構體參數(shù)控制compact_zone內存規(guī)整行為包括但不限于內存掃描范圍、頁遷移的能力、遷移頁是否適合規(guī)整及是否可以阻塞等等。
另一方面,內存規(guī)整的核心邏輯在于遷移頁掃描器(migrate scanner)和空閑頁掃描器(free scanner)運作原理,包括哪些頁可以作為遷移頁或空閑頁,何時內存規(guī)整結束等等這些直接影響對內存規(guī)整理解。
審核編輯:彭菁
-
接口
+關注
關注
33文章
8932瀏覽量
153182 -
數(shù)據(jù)
+關注
關注
8文章
7239瀏覽量
90973 -
Linux
+關注
關注
87文章
11456瀏覽量
212745 -
內存
+關注
關注
8文章
3108瀏覽量
74976 -
分配器
+關注
關注
0文章
203瀏覽量
26134
原文標題:超詳細!Linux內核內存規(guī)整詳解
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
走進Linux內存系統(tǒng)探尋內存管理的機制和奧秘
Linux內核的作用

Linux內核內存管理架構解析

Linux內核地址映射模型與Linux內核高端內存詳解

高端內存的詳解:linux用戶空間與內核空間

知識總結:一篇就讓你入Linux內核的大門

Linux內核引導內存分配器的原理
Linux內核實現(xiàn)內存管理的基本概念

內存內核中發(fā)生頁面遷移的典型場景

評論