注:本文分析基於3.10.0-693.el7內核版本,即CentOS 7.4
1、關於內存整理
內存的夥伴系統以頁爲單位進行內存管理,經過系統和應用程序的大量申請釋放,就會造成大量離散的頁面,這就是內存碎片的來源。
而一些應用程序和硬件需要物理地址連續的內存,即使內存總量夠但內存分配依然會失敗,此時就需要進行內存整理,把部分內存遷移整合,從而騰出連續內存供應用使用。
2、觸發內存整理
觸發內存整理分爲兩種方式,主動式和被動式,所謂主動式就是通過/proc/sys/vm/compact_memory接口主動觸發一次內存整理;而被動式就是系統在分配內存時,當前系統內存分佈無法滿足應用需求,此時就會觸發系統被動的進行一次內存整理,整理完再進行內存的分配。
今天我們主要介紹主動式的內存整理。
3、compact_memory和extfrag_threshold
該接口只有在啓用CONFIG_COMPACTION編譯選項時才生效。只要往/proc/sys/vm/compact_memory中寫入1,就會觸發所有的內存域壓縮,使空閒的內存儘可能形成連續的內存塊。而extfrag_threshold則是控制進行內存整理的意向,作用於內存分配路徑中,該值設置得越小,越傾向於進行內存整理。
設置compact_memory在內核中的處理函數是sysctl_compaction_handler,該接口文件只可寫,因此write參數爲1時纔有實際操作,
/* This is the entry point for compacting all nodes via /proc/sys/vm */
int sysctl_compaction_handler(struct ctl_table *table, int write,
void __user *buffer, size_t *length, loff_t *ppos)
{
if (write)
compact_nodes();
return 0;
}
主要的處理邏輯在於compact_nodes,
/* Compact all nodes in the system */
static void compact_nodes(void)
{
int nid;
/* 將每CPU上的pagevec管理的內存頁刷到LRU鏈表中,便於後續在LRU鏈表中進行統一的整理合並*/
lru_add_drain_all();
/* 針對每個內存zone進行整理,在numa架構下會有多個node */
for_each_online_node(nid)
compact_node(nid);
}
static void compact_node(int nid)
{
//注意order這個初始值,後續會用到,用於區分主動觸發還是被動觸發
struct compact_control cc = {
.order = -1,
.sync = true,
};
__compact_pgdat(NODE_DATA(nid), &cc);
}
通過定義一個compact_control變量,記錄後續需要整理的頁面夜框信息,初始化order的值,用於區分主動觸發還是被動觸發。
/* Compact all zones within a node */
static void __compact_pgdat(pg_data_t *pgdat, struct compact_control *cc)
{
int zoneid;
struct zone *zone;
//遍歷zone,整理每個zone的內存,比如ZONE_DMA,ZONE_NORMAL之類
for (zoneid = 0; zoneid < MAX_NR_ZONES; zoneid++) {
zone = &pgdat->node_zones[zoneid];
if (!populated_zone(zone))
continue;
cc->nr_freepages = 0;
cc->nr_migratepages = 0;
cc->zone = zone;
INIT_LIST_HEAD(&cc->freepages);
INIT_LIST_HEAD(&cc->migratepages);
//手動觸發內存整理時order爲-1,因此進入compact_zone路徑
//或者此次內存整理不能跳過,整理失敗會指數退避跳過下一次整理
if (cc->order == -1 || !compaction_deferred(zone, cc->order))
//壓縮對應zone的內存
compact_zone(zone, cc);
//如果是在內存分配路徑上觸發的內存整理
if (cc->order > 0) {
//壓縮完內存後再次檢測剩餘內存是否能滿足此次內存分配需求
//因此不需要進行內存整理,因此水位值直接用的low,沒有加冗餘量
int ok = zone_watermark_ok(zone, cc->order,
low_wmark_pages(zone), 0, 0);
if (ok && cc->order >= zone->compact_order_failed)
zone->compact_order_failed = cc->order + 1;
/* Currently async compaction is never deferred. */
else if (!ok && cc->sync)
defer_compaction(zone, cc->order);
}
VM_BUG_ON(!list_empty(&cc->freepages));
VM_BUG_ON(!list_empty(&cc->migratepages));
}
}
實際的整理動作都在compact_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);
//判斷內存整理狀態,我們知道有主動和被動兩種方式
ret = compaction_suitable(zone, cc->order);
switch (ret) {
case COMPACT_PARTIAL:
case COMPACT_SKIPPED:
//不需要整理內存,或者內存餘量太小無法整理內存,則返回
return ret;
case COMPACT_CONTINUE:
/* Fall through to compaction */
;
}
/*
* Clear pageblock skip if there were failures recently and compaction
* is about to be retried after being deferred. kswapd does not do
* this reset as it'll reset the cached information when going to sleep.
*/
if (compaction_restarting(zone, cc->order) && !current_is_kswapd())
__reset_isolation_suitable(zone);
//開始真正的內存整理,遷移合併之類的操作
//直到空閒內存高於水位值,或者無可遷移的頁面
...
return ret;
}
在整理內存前需要判斷下此次是否有必要整理內存,因爲我們是主動觸發的,所以肯定是要進行這次的整理,但是對於在分配內存路徑上進入到該函數,就不一定非得整理,因爲有可能不需要整理就能滿足此次內存分配,也有可能因爲內存餘量太小,無法進行內存整理,因爲整理需要一定的空閒頁面作爲遷移使用。所以需要通過compaction_suitable函數進一步判斷,
/*
* compaction_suitable: Is this suitable to run compaction on this zone now?
* Returns
* COMPACT_SKIPPED - If there are too few free pages for compaction
* COMPACT_PARTIAL - If the allocation would succeed without compaction
* COMPACT_CONTINUE - If compaction should run now
*/
unsigned long compaction_suitable(struct zone *zone, int order)
{
int fragindex;
unsigned long watermark;
//通過/proc/sys/vm/compact_memory屬於主動觸發,所以肯定是要繼續整理的
if (order == -1)
return COMPACT_CONTINUE;
/*
* Watermarks for order-0 must be met for compaction. Note the 2UL.
* This is because during migration, copies of pages need to be
* allocated and for a short time, the footprint is higher
*/
//獲取對應zone的內存WMARK_LOW水位線,同時考慮對應order內存頁遷移拷貝時需要的冗餘量
watermark = low_wmark_pages(zone) + (2UL << order);
//如果此時內存空閒頁面太少不能都完成內存整理,則跳過這個zone
//這裏傳入order=0,表示此次不進行內存分配,單純檢測內存餘量是否高於水位線
if (!zone_watermark_ok(zone, 0, watermark, 0, 0))
return COMPACT_SKIPPED;
//如果內存餘量夠,那就要考慮剩餘量能否滿足此次分配需求
//計算fragmentation index,確定如果此次分配失敗的原因是由於內存不足還是內存碎片
//index值趨向0表示此次內存分配失敗是由於內存不足導致
//index值趨向1000表示此次內存分配失敗是由於內存碎片導致
//只有當是因爲內存碎片失敗,才需要整理內存,也纔有整理的意義
fragindex = fragmentation_index(zone, order);
//fragindex大於零,且不超過/proc/sys/vm/extfrag_threshold設置的值
//則內存餘量過小不進行內存整理,extfrag_threshold默認值500
if (fragindex >= 0 && fragindex <= sysctl_extfrag_threshold)
return COMPACT_SKIPPED;
//如果內存碎片檢測通過,且此次內存分配不會導致內存餘量低於水位線
//表明此次內存分配沒問題,不需要整理內存
if (fragindex == -1000 && zone_watermark_ok(zone, order, watermark, 0, 0))
return COMPACT_PARTIAL;
//其餘情況需要整理內存
return COMPACT_CONTINUE;
}
如何來判斷此次內存分配是否需要進行內存整理呢,這時就需要考慮此次分配是在什麼情況下進行的,是否緊急;以及系統設置的內存水位線;還有實際內存塊是否滿足等條件,
bool zone_watermark_ok(struct zone *z, int order, unsigned long mark,
int classzone_idx, int alloc_flags)
{
return __zone_watermark_ok(z, order, mark, classzone_idx, alloc_flags,
zone_page_state(z, NR_FREE_PAGES));
}
/*
* Return true if free pages are above 'mark'. This takes into account the order
* of the allocation.
*/
static bool __zone_watermark_ok(struct zone *z, int order, unsigned long mark,
int classzone_idx, int alloc_flags, long free_pages)
{
/* free_pages my go negative - that's OK */
long min = mark;
//獲取當前zone對應目標zone的保留值
long lowmem_reserve = z->lowmem_reserve[classzone_idx];
int o;
long free_cma = 0;
//先扣除這次需要分配的內存大小,但是爲什麼要-1,不是很清楚
free_pages -= (1 << order) - 1;
//內存分配時帶ALLOC_HIGH標誌位,將水位線降一半,也就是此時內存分配可以多用一些
//一般是atomic方式會帶此標誌
if (alloc_flags & ALLOC_HIGH)
min -= min / 2;
//如果帶ALLOC_HARDER標誌位,將水位線再減少1/4,一般是實時進程並且不在中斷中
if (alloc_flags & ALLOC_HARDER)
min -= min / 4;
#ifdef CONFIG_CMA
//如果設置了ALLOC_CMA標誌位,即不使用CMA連續內存管理區的內存,那就要相應扣除
if (!(alloc_flags & ALLOC_CMA))
free_cma = zone_page_state(z, NR_FREE_CMA_PAGES);
#endif
//當前zone扣除此次分配內存及CMA內存後,剩餘內存小於min水位線及保留內存之和
//那就說明內存在水位線之下
if (free_pages - free_cma <= min + lowmem_reserve)
return false;
//剩餘內存量夠此次分配,還要進一步考慮是否有滿足此次分配的內存塊,
//比如,需要一個order爲2的內存頁,即pagesize 4k*2^2 =16k的內存塊,
//這樣就需要扣除小於16k的內存塊,也就是小於order的都要扣除
for (o = 0; o < order; o++) {
//減去當前order的內存餘量,free_area數組中保存每個order的內存信息
/* At the next order, this order's pages become unavailable */
free_pages -= z->free_area[o].nr_free << o;
/* Require fewer higher order pages to be free */
//我們不需要每個order下都要保留min大小的內存,因此沒扣除一個order的內存
//min也對應減小1/2,也就是將min分攤在各個order上
min >>= 1;
//滿足order的內存小於min水位線,無法進行此次分配
if (free_pages <= min)
return false;
}
//進過重重校驗,滿足此次分配需求
return true;
}
內存餘量高於水位線,那麼就要看看能否滿足此次內存分配了,如果不能滿足此次分配需要知道是因爲內存碎片的原因還是因爲內存真的不足了,因爲只有是由於內存碎片導致內存分配失敗纔有必要整理內存。
int fragmentation_index(struct zone *zone, unsigned int order)
{
struct contig_page_info info;
fill_contig_page_info(zone, order, &info);
return __fragmentation_index(order, &info);
}
static void fill_contig_page_info(struct zone *zone,
unsigned int suitable_order,
struct contig_page_info *info)
{
unsigned int order;
info->free_pages = 0;
info->free_blocks_total = 0;
info->free_blocks_suitable = 0;
for (order = 0; order < MAX_ORDER; order++) {
unsigned long blocks;
//統計該zone所有空閒的block數量
blocks = zone->free_area[order].nr_free;
info->free_blocks_total += blocks;
//統計該zone所有空閒的page數量
info->free_pages += blocks << order;
//統計該zone能滿足此次內存分配的block數量
if (order >= suitable_order)
info->free_blocks_suitable += blocks <<
(order - suitable_order);
}
}
static int __fragmentation_index(unsigned int order, struct contig_page_info *info)
{
unsigned long requested = 1UL << order;
//沒有空閒內存,此次內存分配會失敗
if (!info->free_blocks_total)
return 0;
/* Fragmentation index only makes sense when a request would fail */
//free_blocks_suitable大於0說明此次內存分配可以成功
if (info->free_blocks_suitable)
return -1000;
//return值趨向0表示此次內存分配失敗是由於內存不足導致
//return值趨向1000表示此次內存分配失敗是由於內存碎片導致
return 1000 - div_u64( (1000+(div_u64(info->free_pages * 1000ULL, requested))), info->free_blocks_total);
}