内存规整部分源码学习笔记

一、内存页面水位线

页面分配器按照zone的水位线来管理,zone的水位分为3个等级,分别是High、low、min。最低水位下的内存是系统预留的内存,通常情况下,普通优先级的请求是不能访问系统预留内存的,只有在特殊情况下,这些系统预留的内存才能被使用。如下表所示:

在这里插入图片描述

下面是zone水位管理的流程图:

在这里插入图片描述

二、从内存分配到内存规整

在前面提到过点击,在慢速路径当中分配页面不成功(此时分配水位线为min)的情况下,会唤醒kswapd进行内存回收,但如果通过回收之后依然不能分配成功,会判断是否同时满足以下三个条件,考虑尝试先调用 __alloc_pages_direct_compact() 进行内存规整,以解决页面分配失败的问题。

  • 允许调用直接页面回收机制
  • 高成本的分配需求 costly_order,此时,系统可能有足够的空闲内存,但没有分配满足分配需求的连续页面,调用内存规整机制可能解决这个问题。或者对于请求,分配不可迁移的多个连续物理页面
  • 不能访问系统预留内存,gfp_pfmemalloc_allowed()表示是否允许访问系统预留内存。返回 0 则表示不允许访问预留内存,返回ALLOC_NO_WATERMARKS 表示不用考虑水位,访问全部的预留内存。

如下图所示,展示了从内存分配函数到直接内存规整的函数调用流程:
在这里插入图片描述

接下来分析__alloc_pages_direct_compact()函数

2.1 __alloc_pages_direct_compact()

该函数主要调用try_to_compact_pages()函数,进行内存规整,完成之后调用get_page_from_freelist()函数来尝试分配内存。该函数的主要工作如下:

  • 通过调用try_to_compact_pages(),遍历内存节点中的所有区域,并对每个区域进行内存规整,并使用compact_result 记录规整的结果
  • 如果内存规整成功,捕获到了可用的页面,则使用prep_new_page()函数来准备使用该页面
  • 如果通过内存规整没有成功获取页面,则调用get_page_from_freelist()从空闲链表获取页面

以下是该函数的源码注释:

/**
 * @brief 在低水位之下,alloc_pages() 进入慢速路径分配页面
 *
 * @param gfp_mask 调用页面分配器传递的分配掩码,描述页面分配方法的标志
 * @param order 请求分配页面的大小,order必须小于MAX_ORDER
 * @param alloc_flags 表示页面分配器内部使用的分配标志位
 * @param ac 表示页面分配器内部使用的分配上下文描述符
 * @param prio 内存规整优先级
 * @param compact_result 内存规整后返回的结果
 *
 * @return 成功返回分配的page,失败返回NULL
*/
static struct page *
__alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
		unsigned int alloc_flags, const struct alloc_context *ac,
		enum compact_priority prio, enum compact_result *compact_result)
{
	struct page *page = NULL;
	unsigned long pflags;
	unsigned int noreclaim_flag;

	if (!order)
		return NULL;

	psi_memstall_enter(&pflags);
	noreclaim_flag = memalloc_noreclaim_save();
    
	//调用try_to_compact_pages(),遍历内存节点当中的所有zone,并对于每个 zone 进行内存规整
	*compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac,
								prio, &page);

	memalloc_noreclaim_restore(noreclaim_flag);
	psi_memstall_leave(&pflags);

	count_vm_event(COMPACTSTALL);

	/* 如果内存规整成功并且有可用的页面,准备使用此页面 */
	if (page)
		prep_new_page(page, order, gfp_mask, alloc_flags);

	/* 否则,调用get_page_from_freelist()来进行分配内存 */
	if (!page)
		page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

	if (page) {
		struct zone *zone = page_zone(page);

		zone->compact_blockskip_flush = false;
		compaction_defer_reset(zone, order, true);
		count_vm_event(COMPACTSUCCESS);
		return page;
	}
	count_vm_event(COMPACTFAIL);

	cond_resched();

	return NULL;
}

2.2 try_to_compact_pages()

接下来分析内存规整函数中的关键函数try_to_compact_pages()函数,该函数是遍历内存节点中的所有zone,并对每个zone进行内存规整,并执行compact_zone_order()函数, 保存捕获的页面,返回内存规整的结果。

该函数的主要工作如下:

  • 通过调用for_each_zone_zonelist_nodemask()函数遍历内存节点中的各个 zone,尝试对每个 zone 进行内存规整。
  • 对每个zone,都会进行推迟规整检查,如果某个 zone 之前的内存规整被推迟,并且当前的优先级高于最小优先级,则跳过该 zone
  • 对当前 zone 调用 compact_zone_order(),尝试分配连续内存.
  • 对于规整的结果以及规整的类型,进行不同处理。

该函数的源码注释如下:


/**
 * try_to_compact_pages - 直接压缩以满足高阶分配
 * @gfp_mask: 传递给页面分配器的分配掩码
 * @order: 当前请求分配页面的大小
 * @alloc_flags: 页面分配器内部使用的分配标志位
 * @ac: 页面分配器内部使用的分配上下文描述符
 * @prio: 内存规整的优先级,决定了规整的程度
 * @capture: 成功规整后返回捕获的页面指针
 * @return 返回内存规整的结果,枚举值 compact_result 表示操作状态
 * 直接规整的关键点
 */
enum compact_result try_to_compact_pages(gfp_t gfp_mask, unsigned int order,
		unsigned int alloc_flags, const struct alloc_context *ac,
		enum compact_priority prio, struct page **capture)
{
	int may_perform_io = gfp_mask & __GFP_IO;
	struct zoneref *z;
	struct zone *zone;
	enum compact_result rc = COMPACT_SKIPPED;

	/*
	 * 若为GFP_NOIO,直接内存规整会跳过
	 */
	if (!may_perform_io)
		return COMPACT_SKIPPED;
	// 记录规整操作的跟踪信息
	trace_mm_compaction_try_to_compact_pages(order, gfp_mask, prio);

	/* 遍历指定的zonelist中的所有zone */
	for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx,
								ac->nodemask) {
		enum compact_result status;
		// 检查是否应该推迟压缩。如果区域之前的压缩被推迟且当前优先级不是最低,则跳过该区域,继续检查下一个区域。
		if (prio > MIN_COMPACT_PRIORITY
					&& compaction_deferred(zone, order)) {
			rc = max_t(enum compact_result, COMPACT_DEFERRED, rc);
			continue;
		}
		//在每个zone 中调用 compact_zone_order() 来进行内存规整,尝试获取高阶页面
		status = compact_zone_order(zone, order, gfp_mask, prio,
				alloc_flags, ac_classzone_idx(ac), capture);
		rc = max(status, rc);

		/* 如果规整成功,重置该区域的规整推迟标志并退出循环 */
		if (status == COMPACT_SUCCESS) {
			compaction_defer_reset(zone, order, false);

			break;
		}
		// 如果规整是同步的,并且状态是完全完成或部分跳过,推迟该区域的进一步规整
		if (prio != COMPACT_PRIO_ASYNC && (status == COMPACT_COMPLETE ||
					status == COMPACT_PARTIAL_SKIPPED))

			defer_compaction(zone, order);

		/*
		 * 对于异步规整,如果需要重新调度或者有致命信号挂起,则终止规整尝试。
		 */
		if ((prio == COMPACT_PRIO_ASYNC && need_resched())
					|| fatal_signal_pending(current))
			break;
	}

	return rc;
}

2.3 compact_zone_order()

try_to_compact_pages()函数中的关键规整操作由compact_zone_order()函数来完成,该函数是在一个指定的zone中进行内存规整,保存内存规整中捕获的页面,并将内存规整的结果返回。该结果是一个枚举类型,具体在include/linux/compaction.h中定义。

该函数进行的主要工作如下:

  • 初始化 compact_control 结构体,明确了所需的连续页面大小,当前所要规整的区域等信息
  • 初始化 capture_control 结构体,用于在规整该区域的时候捕获所需的页面
  • 根据 compact_control结构体,去调用 compact_zone 函数,在当前区域内按照相关设置执行实际内存规整,并将捕获的页面保存到capture_control 结构体中
  • 返回捕获的页面以及规整的结果
/**
 * compact_zone_order - 初始化内部使用的compact_control数据结构,在调用compact_zone进行内存规整
 * @zone: 指向需要进行内存规整的zone的指针
 * @order: 当前请求分配页面的大小
 * @gfp_mask: 传递给页面分配器的分配掩码
 * @prio: 内存规整的优先级
 * @alloc_flags: 页面分配器内部使用的分配标志位
 * @classzone_idx: 页面分配器根据分配掩码计算出的首选zone的编号
 * @capture: 用于在内存规整过程中捕获一个页面,如果成功,它将指向被分配的页面
 * @return 返回规整的结果
 * 直接规整的关键点
 */
static enum compact_result compact_zone_order(struct zone *zone, int order,
		gfp_t gfp_mask, enum compact_priority prio,
		unsigned int alloc_flags, int classzone_idx,
		struct page **capture)
{
	enum compact_result ret;

	// 初始化 compact_control 结构,用于控制规整的参数
	struct compact_control cc = {
		.order = order,
		.search_order = order,
		.gfp_mask = gfp_mask,
		.zone = zone,
		.mode = (prio == COMPACT_PRIO_ASYNC) ?
					MIGRATE_ASYNC :	MIGRATE_SYNC_LIGHT,
		.alloc_flags = alloc_flags,
		.classzone_idx = classzone_idx,
		//设置并检查有关区域内规整的选项
		.direct_compaction = true,
		.whole_zone = (prio == MIN_COMPACT_PRIORITY),
		.ignore_skip_hint = (prio == MIN_COMPACT_PRIORITY),
		.ignore_block_suitable = (prio == MIN_COMPACT_PRIORITY)
	};
	//填充capture_control变量,用于在内存规整中捕获页面
	struct capture_control capc = {
		.cc = &cc,
		.page = NULL,// 初始化时没有捕获页面
	};
	//将 capture_control 变量赋值给当前进程上下文
	if (capture)
		current->capture_control = &capc;

	//接着调用 compact_zone() 进行内存规整
	ret = compact_zone(&cc, &capc);

	// 使用断言来确保释放所有链表中的页面
	VM_BUG_ON(!list_empty(&cc.freepages));
	VM_BUG_ON(!list_empty(&cc.migratepages));

	// 如果有捕获的页面,通过 capture 返回
	*capture = capc.page;

	// 清除当前线程的 capture_control 设置
	current->capture_control = NULL;

	return ret;
}

三、内存碎片

内存碎片是由大量离散且不连续的空闲页面组成,在系统运行时间越长的情况下,内存碎片会越来越多,内存碎片化越严重,最直接的影响就是导致分配大块内存失败。而在内核中,内存页面可分为可回收、可移动和不可移动等类型,其中可移动页面:仅需要修改页面的映射关系,代价低,可回收页面指的是不可移动但可以释放的页面。

这两种类型的页面会通过改变页面的映射关系以及释放页面空间对内存碎片进行规整。经过上述分析可知,内存规整的核心函数为compact_zone,接下来分析该函数。

四、内存规整源码分析

4.1 compact_zone

compact_zone是内存规整的核心函数,主要用于“兵分两路”扫描一个 zone,一个从zone头部向尾部开始扫描,找出可以迁移的页面。另一个从zone的尾部向头部开始扫描,查找空闲页面。这两路“兵”会在 zone 的中间汇合,或者已经满足分配大块内存的需求(能满足分配的大块内存并且满足最低水位要求),就退出扫描,接着调用内存迁移的接口进行页面迁移,整理大块内存

通过分析源码,总结 compact_zone()函数的核心工作如下:

  • 初始化链表 cc->freepagescc->migratepages,它们分别用于存储扫描到的空闲页面和待迁移的页面。
  • 调用compaction_suitable()函数,根据zone水位线判断当前zone是否需要进行内存规整。如果无需规整,则直接返回,否则执行下一步。
  • 根据cc->whole_zone来确定扫描的范围,如果设置该标志,则扫描整个区域,否则根据compact_cached_migrate_pfn[] zone->compact_cached_free_pfn[]成员记录的位置,确定扫描的范围
  • 在while循环当中,通过调用 compact_finished() 函数检查当前规整是否结束,在while循环中执行以下操作:
    • 通过调用isolate_migratepages() 执行迁移页面的隔离操作,并将可迁移页面加入 cc->migratepages 链表中。
      • 如果成功隔离到页面 (ISOLATE_SUCCESS),继续迁移。
      • 如果隔离被中止 (ISOLATE_ABORT),释放已经隔离的页面并退出。
    • 调用migrate_pages()来迁移页面,从cc->migratepages链表中获取已经隔离的页面,然后尝试迁移
    • 迁移成功之后,释放cc->freepages中的空闲页面并记录迁移事件

有关具体的迁移页面代码详见第五节,页面迁移源码分析。

/**
 * @brief 内存规整的核心函数,主要用于“兵分两路”扫描一个 zone
 * 一个从zone头部向尾部开始扫描,找出可以迁移的页面。另一个从zone的尾部向头部开始扫描,查找空闲页面。这两路“兵”会在 zone 的中间汇合,或者已经满足分配大块内存的需求,退出扫描
 * 然后调用页面迁移的接口函数进行页面迁移,最终整理出大块空闲页面
 *
 * @param cc 表示内存规整中内部使用的控制参数
 * @param capc  表示在内存规整过程中捕获的页面 
*/
static enum compact_result
compact_zone(struct compact_control *cc, struct capture_control *capc)
{
	enum compact_result ret;

	//zone的起始页帧和终止页帧
	unsigned long start_pfn = cc->zone->zone_start_pfn;
	unsigned long end_pfn = zone_end_pfn(cc->zone);

	unsigned long last_migrated_pfn;
	// 表示是否支持同步的迁移模式
	const bool sync = cc->mode != MIGRATE_ASYNC;
	bool update_cached;

	/*
	 * These counters track activities during zone compaction.  Initialize
	 * them before compacting a new zone.
	 */
	cc->total_migrate_scanned = 0;
	cc->total_free_scanned = 0;
	cc->nr_migratepages = 0;
	cc->nr_freepages = 0;
    //初始化空闲页面和待迁移的页面链表
	INIT_LIST_HEAD(&cc->freepages);
	INIT_LIST_HEAD(&cc->migratepages);

	//gfpflags_to_migratetype() 从分配掩码当中来获取页面的迁移类型
	cc->migratetype = gfpflags_to_migratetype(cc->gfp_mask);

	//compaction_suitable() 函数根据当前zone的水位来判断是否要进行内存规整
	ret = compaction_suitable(cc->zone, cc->order, cc->alloc_flags,
							cc->classzone_idx);
	/* 无需规整 */
	if (ret == COMPACT_SUCCESS || ret == COMPACT_SKIPPED)
		return ret;

	/* huh, compaction_suitable is returning something unexpected */
	VM_BUG_ON(ret != COMPACT_CONTINUE);

	/*
	 * Clear pageblock skip if there were failures recently and compaction
	 * is about to be retried after being deferred.
	 */
	if (compaction_restarting(cc->zone, cc->order))
		__reset_isolation_suitable(cc->zone);

	/*
	 * Setup to move all movable pages to the end of the zone. Used cached
	 * information on where the scanners should start (unless we explicitly
	 * want to compact the whole zone), but check that it is initialised
	 * by ensuring the values are within zone boundaries.
	 */
	cc->fast_start_pfn = 0;
	
	// whole_zone表示要扫描整个zone,其中 free_pfn指的是 zone 最后一个页块的起始地址
	if (cc->whole_zone) {
		cc->migrate_pfn = start_pfn;
		cc->free_pfn = pageblock_start_pfn(end_pfn - 1);
	} else {

		//compact_cached_migrate_pfn[] 成员记录了上一次扫描中可迁移页面的位置,数组分别记录同步和异步模式。zone->compact_cached_free_pfn成员记录了上一次扫描中空闲页面的位置
		cc->migrate_pfn = cc->zone->compact_cached_migrate_pfn[sync];
		cc->free_pfn = cc->zone->compact_cached_free_pfn;

		if (cc->free_pfn < start_pfn || cc->free_pfn >= end_pfn) {
			cc->free_pfn = pageblock_start_pfn(end_pfn - 1);
			cc->zone->compact_cached_free_pfn = cc->free_pfn;
		}
		if (cc->migrate_pfn < start_pfn || cc->migrate_pfn >= end_pfn) {
			cc->migrate_pfn = start_pfn;
			cc->zone->compact_cached_migrate_pfn[0] = cc->migrate_pfn;
			cc->zone->compact_cached_migrate_pfn[1] = cc->migrate_pfn;
		}

		if (cc->migrate_pfn <= cc->zone->compact_init_migrate_pfn)
			cc->whole_zone = true;
	}

	last_migrated_pfn = 0;

	/*
	 * Migrate has separate cached PFNs for ASYNC and SYNC* migration on
	 * the basis that some migrations will fail in ASYNC mode. However,
	 * if the cached PFNs match and pageblocks are skipped due to having
	 * no isolation candidates, then the sync state does not matter.
	 * Until a pageblock with isolation candidates is found, keep the
	 * cached PFNs in sync to avoid revisiting the same blocks.
	 */
	update_cached = !sync &&
		cc->zone->compact_cached_migrate_pfn[0] == cc->zone->compact_cached_migrate_pfn[1];
	
	trace_mm_compaction_begin(start_pfn, cc->migrate_pfn,
				cc->free_pfn, end_pfn, sync); //记录开始扫描的事件

	migrate_prep_local();
	//compact_finished() 函数会判断当前规整是否结束,即是否需要扫描,COMPACT_CONTINUE表示需要继续扫描下一个页块
	while ((ret = compact_finished(cc)) == COMPACT_CONTINUE) {
		int err;
		unsigned long start_pfn = cc->migrate_pfn;

		/*
		 * Avoid multiple rescans which can happen if a page cannot be
		 * isolated (dirty/writeback in async mode) or if the migrated
		 * pages are being allocated before the pageblock is cleared.
		 * The first rescan will capture the entire pageblock for
		 * migration. If it fails, it'll be marked skip and scanning
		 * will proceed as normal.
		 */
		cc->rescan = false;
		if (pageblock_start_pfn(last_migrated_pfn) ==
		    pageblock_start_pfn(start_pfn)) {
			cc->rescan = true;
		}

		//isolate_migratepages 是迁移页扫描器实现,用于查找需要移动的页,并将可移动的页面加入到cc->migratepages链表中

		switch (isolate_migratepages(cc)) {
		case ISOLATE_ABORT:
		//如果隔离过程被中止,通过 putback_movable_pages 将已经隔离的页面重新放回内存,再退出
			ret = COMPACT_CONTENDED;
			putback_movable_pages(&cc->migratepages);
			cc->nr_migratepages = 0;
			last_migrated_pfn = 0;
			goto out;
		case ISOLATE_NONE:
		//如果没有找到任何可以隔离的页面,则检查是否需要刷新之前的迁移操作,并跳转到 check_drain 来清理状态
			if (update_cached) {
				cc->zone->compact_cached_migrate_pfn[1] =
					cc->zone->compact_cached_migrate_pfn[0];
			}

			/*
			 * We haven't isolated and migrated anything, but
			 * there might still be unflushed migrations from
			 * previous cc->order aligned block.
			 */
			goto check_drain;
		case ISOLATE_SUCCESS:
		//如果隔离成功,更新缓存信息并继续页面迁移,在这里记录此次迁移的起始地址
			update_cached = false;
			last_migrated_pfn = start_pfn;
			;
		}
		//migrate_pages()迁移页面,从cc->migratepages链表中获取页面,然后尝试迁移页面
		err = migrate_pages(&cc->migratepages, compaction_alloc,
				compaction_free, (unsigned long)cc, cc->mode,
				MR_COMPACTION);
		//记录迁移的事件
		trace_mm_compaction_migratepages(cc->nr_migratepages, err,
							&cc->migratepages);

		/* 所有的页要么被迁移要么被释放*/
		cc->nr_migratepages = 0;
		if (err) {
			//putback_movable_pages()把以及分离的页面重新添加到LRU链表中
			putback_movable_pages(&cc->migratepages);
			/*
			 * 当扫描器之间已经相遇,或者内存不足,退出循环
			 */
			if (err == -ENOMEM && !compact_scanners_met(cc)) {
				ret = COMPACT_CONTENDED;
				goto out;
			}
			/*
			 * We failed to migrate at least one page in the current
			 * order-aligned block, so skip the rest of it.
			 */
			if (cc->direct_compaction &&
						(cc->mode == MIGRATE_ASYNC)) {
				cc->migrate_pfn = block_end_pfn(
						cc->migrate_pfn - 1, cc->order);
				/* Draining pcplists is useless in this case */
				last_migrated_pfn = 0;
			}
		}

check_drain:
		/*
		 * Has the migration scanner moved away from the previous
		 * cc->order aligned block where we migrated from? If yes,
		 * flush the pages that were freed, so that they can merge and
		 * compact_finished() can detect immediately if allocation
		 * would succeed.
		 */
		if (cc->order > 0 && last_migrated_pfn) {
			int cpu;
			unsigned long current_block_start =
				block_start_pfn(cc->migrate_pfn, cc->order);

			if (last_migrated_pfn < current_block_start) {
				cpu = get_cpu();
				lru_add_drain_cpu(cpu);
				drain_local_pages(cc->zone);
				put_cpu();
				/* No more flushing until we migrate again */
				last_migrated_pfn = 0;
			}
		}

		/* Stop if a page has been captured */
		if (capc && capc->page) {
			ret = COMPACT_SUCCESS;
			break;
		}
	}

out:
	/*
	 * Release free pages and update where the free scanner should restart,
	 * so we don't leave any returned pages behind in the next attempt.
	 */
	if (cc->nr_freepages > 0) {
		unsigned long free_pfn = release_freepages(&cc->freepages);

		cc->nr_freepages = 0;
		VM_BUG_ON(free_pfn == 0);
		/* The cached pfn is always the first in a pageblock */
		free_pfn = pageblock_start_pfn(free_pfn);
		/*
		 * Only go back, not forward. The cached pfn might have been
		 * already reset to zone end in compact_finished()
		 */
		if (free_pfn > cc->zone->compact_cached_free_pfn)
			cc->zone->compact_cached_free_pfn = free_pfn;
	}

	count_compact_events(COMPACTMIGRATE_SCANNED, cc->total_migrate_scanned);
	count_compact_events(COMPACTFREE_SCANNED, cc->total_free_scanned);

	trace_mm_compaction_end(start_pfn, cc->migrate_pfn,
				cc->free_pfn, end_pfn, sync, ret);

	return ret;
}

4.2 compact_suitable()

compact_zone()函数中,该函数是根据当前的zone水位判断是否需要进行内存规整,其内部是通过__compaction_suitable()函数实现。该函数的核心工作如下:

  • 调用__compaction_suitable()函数,根据zone的空闲页面数量,判断是否满足所需的内存分配要求
  • 根据上述函数的结果,如果不能判断是否进行内存规整且order大于PAGE_ALLOC_COSTLY_ORDER,则使用函数fragmentation_index()进行内存碎片化指数计算,来做进一步判断,当指数小于等于 sysctl_extfrag_threshold(系统设定的碎片化阈值,通常为 500)时,认为当前内存碎片化不严重,不需要内存规整。
/**
 * @brief 根据zone水位来判断是否需要进行内存规整
 *
 * @param zone 指的是待评估的zone
 * @param order 请求分配页面的大小,order必须小于MAX_ORDER
 * @param alloc_flags 表示页面分配器内部使用的分配标志位
 * @param classzone_idx 表示页面分配器根据分配掩码计算出来的首选zone
 * @return 返回内存规整的结果,具体结果取决于 zone 的当前状态
*/
enum compact_result compaction_suitable(struct zone *zone, int order,
					unsigned int alloc_flags,
					int classzone_idx)
{
	enum compact_result ret;
	int fragindex
上一篇:LeetCode 11.盛最多水的容器


下一篇:两个yaml转成的 excel对比