注:本文分析基于3.10.0-693.el7内核版本,即CentOS 7.4
内存的伙伴系统以页为单位进行内存管理,经过系统和应用程序的大量申请释放,就会造成大量离散的页面,这就是内存碎片的来源。
而一些应用程序和硬件需要物理地址连续的内存,即使内存总量够但内存分配依然会失败,此时就需要进行内存整理,把部分内存迁移整合,从而腾出连续内存供应用使用。
触发内存整理分为两种方式,主动式和被动式,所谓主动式就是通过/proc/sys/vm/compact_memory接口主动触发一次内存整理;而被动式就是系统在分配内存时,当前系统内存分布无法满足应用需求,此时就会触发系统被动的进行一次内存整理,整理完再进行内存的分配。
今天我们主要介绍主动式的内存整理。
该接口只有在启用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);
}