Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(中)

《Linux6.5源码分析:内存管理系列文章》

本系列文章将对内存管理相关知识进行梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中内存管理机制进行数据实时拿取与分析。

在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:

  • 《深入理解Linux内核》

  • 《Linux操作系统原理与应用》

  • 《奔跑吧Linux内核》

  • 《深入理解Linux进程与内存》

  • 《基于龙芯的Linux内核探索解析》

  • Linux内存管理 - 随笔分类 - LoyenWang - 博客园

  • 专栏文章目录 - 知乎 (zhihu.com)

  • albertxu216/LinuxKernel_Learning/Linux6.5源码注释

Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(中)——快速路径分配物理页面

在上篇文章中,我们介绍了分配和释放物理页面的核心接口、gfp_mask掩码以及在Linux6.5内核中是如何实现物理页面分配的,具体内容详见上一篇文章:Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(上)-CSDN博客。

通过几张图片帮助大家回顾一下zonelist与zone,node间的关系,以及alloc_pages函数实现逻辑,便于本篇文章介绍物理页面分配中的快速路径分配;

Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(中)_第1张图片

我们可以看到,在__alloc_pages函数中,先通过prepare_alloc_pages()函数填充alloc_context上下文结构体,再通过get_page_from_freelist()快速分配物理页面,如果快速路径分配失败,则尝试通过慢速路径分配物理页面;本篇文章将围绕伙伴系统中的快速路径分配物理页面深入源码进行分析与梳理;

在快速路径分配物理页面过程中,会遍历zonelist链表来找到一个合适的zone去分配物理页面,所以有必要温习一下zonelistzone,node之间的关系:

Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(中)_第2张图片

1. 快速路径分配物理页面流程

快速路径分配物理页面,顾名思义,讲究一个快,核心思想就是:在条件充足的时候,追求完美,面面俱到,尽可能考虑到分配效率、分配过程对系统的性能影响、节点脏页负载以及每个zone的水位线等情况;如果无法兼顾以上的追求(不能做到完美),则退一步降低要求去分配物理页面;如果依然找不到可分配的物理页面,则快速路径分配失败,尝试慢速路径分配物理页面。

用一句话宏观的介绍一下快速路径分配的流程:从zonelist链表(包含本地node和远端node)中找到一个满足条件的zone来分配物理页面,在找zone的过程中考虑到了与当前CPU是否匹配、node的脏页负载限制、碎片化、水位线等条件;在找到满足条件的zone之后,会尝试从伙伴系统或pcp链表中分配物理页面,这里会优先考虑pcp链表;如果没找到满足条件的zone,则尝试降低条件,重新遍历zonelist链表,或去 “找“ 内存用于分配;

实现快速路径分配的内核函数是get_page_from_freelist(),该函数会按照上述思想进行功能实现,先通过一张图宏观的看一下该函数的实现逻辑:

Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(中)_第3张图片

该函数尝试从伙伴系统的空闲页面链表中分配物理页面,用于扫描可用的 zonelist,按照多种策略(如 NUMA、避免碎片化、分配标志等)尝试分配页面。如果分配失败,可以通过内存回收、延迟初始化的页面扩展等手段继续尝试。

get_page_from_freelist函数的实现思路总结如下:

  • 1.遍历zonelist:他会从首选zone开始遍历zonelist中highest_zoneidx以上的zone(包括本地节点的zone以及远端节点的zone, 即NUMA/UMA中所有node),检查每个zone是否满足分配条件;
  • 1.1.约束条件检查
    • 1.1.1.cpuset zone集合限制的检查:如果设置了cpuset,则检查当前zone是否属于允许分配的内存区域;
    • 1.1.2.脏页负载限制:如果分配页面是需要分散脏页,检查当前节点脏页负载是否超过限制,超过了则跳过该节点;
      • :每个节点都有脏页负载限制,防止单个节点脏页过多,优先选择脏页负载未超限制的节点。
    • 1.1.3.碎片化控制:如果当前zone是属于远端节点且要求避免碎片化,那么尝试放宽要求——允许碎片化,从本地节点开始重新遍历;
      • :这里是因为本地节点内存要优于远端节点的内存,即使放宽要求(允许碎片化)也要本地内存;
    • 1.1.4.水位线限制:检查当前zone的水位线是否满足要求,即当前是否有足够的物理页面,如果不满足要求则需要触发回退与扩展机制;
      • :这里需要注意,回退与扩展机制是指:去寻找更多可用内存,用来物理页面分配;
  • 1.2.回退与扩展机制
    • 1.2.1.系统未初始化或未识别的内存块:如果检测到有系统未识别未初始化的内存块,则尝试初始化该内存块,并进行物理内存分配工作;
    • 1.2.2.延迟初始化页面:如果内核启用了延迟初始化功能,则检查是否存在延迟初始化的页面,将这部分初始化并加入内存池中,进行物理内存分配工作;
    • 1.2.3.忽略水位线要求:如果对当前分配请求可以忽视水位线要求,则直接进行物理内存分配工作;
    • 1.2.4.内存回收:如果当前节点以及当前zone允许回收内存,则尝试执行当前节点的内存回收工作;
  • 1.3.在当前zone中分配物理内存
    • 1.3.1.rmqueue()从当前zone的伙伴系统空闲链表中分配物理内存
    • 1.3.2分配成功则初始化这些物理页面;
    • 1.3.3.分配失败则再次通过回退与扩展机制,检查是否有系统未初始化及延迟初始化的页面;
  • 1.4.分配失败:如果以上操作都是在禁止碎片化情况下分配的,那么尝试放宽条件,解出避免碎片化标志,重新遍历所有zone,去尝试分配内存;

一句话概括:get_page_from_freelist函数会在zonelist链表中从首选zone依次遍历各个内存节点中的各zone去找到最符合条件的那个zone进行页面分配;在遍历每个zone时,首先会检查限制条件,如果当前zone下的物理页面不够,水位线不满足要求,那么接着会触发回退与扩展机制,去找一些未初始化的内存块或者进行内存回收操作;最后在物理页面充足且满足限制条件的环境下,会调用rmqueue()函数进行内存分配

下面是源码实现部分:

static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
						const struct alloc_context *ac)
{
	struct zoneref *z;//当前正在处理的 zone 的引用
	struct zone *zone;//当前正在处理的 zone
	struct pglist_data *last_pgdat = NULL;//最近访问的 NUMA 节点数据
	bool last_pgdat_dirty_ok = false;//最近访问节点是否可分配脏页
	bool no_fallback;//是否避免碎片化的标志

retry:

	no_fallback = alloc_flags & ALLOC_NOFRAGMENT;//表示是否需要避免内存碎片化;
	z = ac->preferred_zoneref;//获取zonelist中首选和推荐的zone;

/*1.使用for_next_zone_zonelist_nodemask宏遍历zonelist链表,
 *  从推荐的zone开始遍历当前节点中highest_zoneidx以上的zone
 */
	for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx,
					ac->nodemask) {
		struct page *page;
		unsigned long mark;
	/*1.1 约束条件的检查
	 *    a. cpu集合限制;
	 *    b. 脏页限制;
	 *    c. 碎片化控制;
	 *    d. 水位检测;
	 */
		/*1.1.1 cpu集合限制
		 *      如果开启了cpuset限制,应检查当前zone是否允许分配;
		 *      如果zone不在允许的cpu集合中,则跳过该zone;
		 */
		if (cpusets_enabled() &&
			(alloc_flags & ALLOC_CPUSET) &&
			!__cpuset_zone_allowed(zone, gfp_mask))
				continue;
				
		/*1.1.2 脏页负载限制
		 * a.当分配页面用于写入时,需要确保脏页不会超过节点的限制;
		 * b.检查当前 zone 所属的节点是否可以分配脏页。如果不允许,跳过该 zone。
		 *
		 *   在为写操作分配页面缓存时,内核会优先选择脏页未超出限制的节点,
		 *   以避免单个节点负担过多脏页,从而影响全局内存平衡和性能。
		 *   在慢速路径中(spread_dirty_pages 未设置时),
		 *   允许暂时超出节点的脏页限制,以提高分配成功率,
		 *   特别是在 NUMA 系统中节点内存较小时。
		 *   这种实现是权宜之计,未来需要通过节点感知的脏页限制和优化刷新线程机制进一步完善。
		 */
		if (ac->spread_dirty_pages) {
			if (last_pgdat != zone->zone_pgdat) {//检查节点是否发生变化;
				last_pgdat = zone->zone_pgdat;
				last_pgdat_dirty_ok = node_dirty_ok(zone->zone_pgdat);
			}

			if (!last_pgdat_dirty_ok)//当前节点不允许分配脏页,跳过
				continue;
		}
		/*1.1.3 碎片化控制
		 * 		如果当前节点是远端节点,则尝试将避免碎片化标志改为允许碎片化
		 *      这是为了在NUMA系统中,尽量从本地节点分配内存,减少远端内存的访问
		 *      分配本地内存就算造成碎片化,也好过,分配远端内存;
		 */
		if (no_fallback && nr_online_nodes > 1 &&
		    zone != ac->preferred_zoneref->zone) {
			int local_nid;

			local_nid = zone_to_nid(ac->preferred_zoneref->zone);
			if (zone_to_nid(zone) != local_nid) {
				alloc_flags &= ~ALLOC_NOFRAGMENT;
				goto retry;
			}
		}

		/*1.1.4 内存水位条件检测:
		 *      当前zone是否满足内存分配的水位条件
		 *      ALLOW_WMARK_LOW:最低分配水位;
		 *      ALLOW_WMARK_HIGH:更高的分配水位;
		 *      ALLOW_WMARK_MIN:绝对最低水位,低于此值需要回收;
		 *
		 *      不满足水位条件:
		 *      则触发1.2 回退与扩展机制,
		 */
		/*1.1.4.1 wmark_pages计算水位值*/
		mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
		/*1.1.4.2 zone_watermark_fast()检查当前zone的水位情况,
		 *        是否有足够的空闲物理页面
		 *        检查是否有满足order的空闲内存块
		 *        如果水位不足即没有足够的物理页面,则触发1.2 回退与扩展机制
		 */
		if (!zone_watermark_fast(zone, order, mark,
				       ac->highest_zoneidx, alloc_flags,
				       gfp_mask)) {
			int ret;
	/*1.2 回退与扩展机制
	 *    不满足水位条件,触发
	 *    1.2.1.退回去查看 是否有 系统未初始化未识别的内存块,将其加入zone中,直接去分配;
	 *    1.2.2.退回去查看 是否有 延迟页面初始化的内存块; 
	 *    1.2.3.若可以忽略水位限制,则直接进行物理内存分配;  
	 *    1.2.4.内存回收,若允许内存回收,则尝试回收内存;
	 */
			/*1.2.1 检查是否存在未初始化、未被系统识别的内存块*/
			if (has_unaccepted_memory()) {
				/*初始化并将这部分内存块加入zone中
				 *跳到try_this_zone中 真正分配物理内存;
				 */
				if (try_to_accept_memory(zone, order))
					goto try_this_zone;
			}

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT//内核启用了延迟页面初始化功能
			/*1.2.2 检查是否存在延迟初始化页面
			 *      若内核启动了延迟初始化功能,
			 *      则初始化这部分内存,并将其扩展到当前zone的内存池
			 *
			 *======延迟初始化是为了加速系统启动时的内存初始化过程,
			 *		将部分页面的初始化推迟到了实际需要时执行,
			 *		那么现在zone中的物理内存不够了,便可以尝试初始化这部分的内存;
			 */
			if (deferred_pages_enabled()) {
			/*尝试初始化 之前延迟初始化的页面,扩展当前zone的内存池,
			 *初始化成功后,直接去try_this_zone分配物理内存;
			 */
				if (_deferred_grow_zone(zone, order))
					goto try_this_zone;
			}
#endif
			/*1.2.3 忽略水位限制:
			 *      如果启用了ALLOC_NO_WATERMARKS标志,表示可以忽略所有水位线限制
			 *      直接去try_this_zone 分配物理内存
			 */
			BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
			if (alloc_flags & ALLOC_NO_WATERMARKS)
				goto try_this_zone;
			
			/*1.2.4 内存回收:
			 *      a.node_reclaim_enabled 节点允许回收内存,
			 *        且zone_allows_reclaim zone允许回收内存,
			 *      b.则通过node_reclaim进行内存回收
			 *      c.zone_watermark_ok()去检查回收后的zone是否满足分配需求
			 */
			if (!node_reclaim_enabled() || 
			    !zone_allows_reclaim(ac->preferred_zoneref->zone, zone))
				continue;//不允许的话,跳去下一个zone;
			
			ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);//执行当前节点的内存回收
			switch (ret) {
			case NODE_RECLAIM_NOSCAN://未执行内存回收操作,跳过当前zone
				/* did not scan */
				continue;
			case NODE_RECLAIM_FULL://节点资源耗尽,跳过当前zone
				/* scanned but unreclaimable */
				continue;
			default://回收了一些页面,释放了一些内存
				/*调用zone_watermark_ok()去检查回收后的zone是否满足分配需求*/
				if (zone_watermark_ok(zone, order, mark,
					ac->highest_zoneidx, alloc_flags))
					goto try_this_zone;

				continue;
			}
		}

try_this_zone:
/*1.3 rmqueue()尝试从伙伴系统空闲链表中分配物理页面
 *    1.3.1 分配成功,则执行一系列初始化工作;
 *    1.3.2 分配失败,则再次触发回退与扩展机制,查看是否有未初始化的内存;
 */
		/*尝试调用rmpueue() 从zone空闲链表 (伙伴系统空闲链表) 中分配物理页面*/
		page = rmqueue(ac->preferred_zoneref->zone, zone, order,
				gfp_mask, alloc_flags, ac->migratetype);
		if (page) {
			/*分配成功的话,则对分配好的物理页面进行初始化*/
			prep_new_page(page, order, gfp_mask, alloc_flags);

			/*
			 * 如果是高阶原子分配,检查页面块是否需要保留。
			 */
			if (unlikely(alloc_flags & ALLOC_HIGHATOMIC))
				reserve_highatomic_pageblock(page, zone, order);

			return page;
		} else {
			/*未分配成功,则再次检查是否有未初始化的内存块,将其初始化,并重新尝试分配内存*/
			if (has_unaccepted_memory()) {
				if (try_to_accept_memory(zone, order))
					goto try_this_zone;
			}

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
			/*再次检查是否有延迟初始化内存,如果有则尝试初始化这部分内存,并再次重新分配*/
			if (deferred_pages_enabled()) {
				if (_deferred_grow_zone(zone, order))
					goto try_this_zone;
			}
#endif
		}
	}
/*2. 分配失败
 *   将允许碎片化,从头遍历zonelist重新分配物理内存
 */
	if (no_fallback) {
		/*禁止避免碎片化标志,重头尝试分配*/
		alloc_flags &= ~ALLOC_NOFRAGMENT;
		goto retry;
	}

	return NULL;
}

这里有两个重点函数:rmqueue()node_reclaim()zone_watermark_fast(),分别实现了物理页面分配、内存回收、水位线检测功能,我们会在下面的小节中逐一分析;

2. zone_watermark_fast 水位线检查

水位线检查的目的是为了查看当前zone中的物理页面是否在ALLOW_WMARK_LOW低水位之下,如果在低水位线之下,则说明当前zone中内存不足,不建议从该zone中分配内存;

这部分功能通过函数zone_watermark_fast实现,该函数针对一些特殊情况(如今申请一个物理页面)提供了快速通道以及回退机制(降低条件重新检查),正常情况下该函数会检查是否满足order的页面分配请求;

  • 1.计算当前zone内的空闲页面情况: 通过zone_page_state函数对zone结构体中vm_stat[]进行统计,该字段存储了用于页面统计的信息;
  • 2.针对只申请一个页面的情况,进行快速处理: 如果空闲可用物理页面 > 低水位线对应的物理页面,则检查通过,否则继续,usable_free > mark + z->lowmem_reserve[highest_zoneidx]
    • 这里的mark + z->lowmem_reserve[highest_zoneidx]是指: 低水位线对应的物理页面数+zone预留的页面数, lowmem_reserve是指每个zone预留的内存,防止高端zone在没内存的情况下过度使用低端zone的内存资源;
  • 3.__zone_watermark_ok()进行水位线检查;
/**
 * @brief 用于测试当前zone的水位情况,快速检查是否满足order的页面分配请求
 * @param z 所检测的目标zone
 * @param order 分配物理页面个数
 * @param mark 要测试的水位标准
 * @param highest_zoneidx 最高
 * @param alloc_flags 分配器内部使用的标志位属性
**/
static inline bool zone_watermark_fast(struct zone *z, unsigned int order,
				unsigned long mark, int highest_zoneidx,
				unsigned int alloc_flags, gfp_t gfp_mask)
{
	long free_pages;
	/*1.获取zone中空闲页面的数量
	 *  zone_page_state通过zone结构体中的vm_stat[]进行统计;
	 *  vm_stat[]记录着物理页面统计数据;
	 */
	free_pages = zone_page_state(z, NR_FREE_PAGES);

	/*2.针对仅分配1个页面的情况*/
	if (!order) {
		long usable_free;
		long reserved;

		usable_free = free_pages;
		/*2.1 计算由于分配标志(如高原子分配)导致无法使用的页面数量*/
		reserved = __zone_watermark_unusable_free(z, 0, alloc_flags);

		/* reserved may over estimate high-atomic reserves. */
		/*2.2 从总空闲页面中减去保留页面数,得到实际可用页面数*/
		usable_free -= min(usable_free, reserved);
		if (usable_free > mark + z->lowmem_reserve[highest_zoneidx])//lowmem_reserve是zone预留的水位
			return true;
	}
	/*3.真正的水位线检查*/
	if (__zone_watermark_ok(z, order, mark, highest_zoneidx, alloc_flags,
					free_pages))
		return true;

	 /*4.如果只分配一页,并且允许提升水位线,当前检查的水位标志为WMARK_MIN最小水位
	  *  尝试水位标记为最小水位,并且重新调用__zone_watermark_ok检查水位线条件;
	  */
	if (unlikely(!order && (alloc_flags & ALLOC_MIN_RESERVE) && z->watermark_boost
		&& ((alloc_flags & ALLOC_WMARK_MASK) == WMARK_MIN))) {
		mark = z->_watermark[WMARK_MIN];
		return __zone_watermark_ok(z, order, mark, highest_zoneidx,
					alloc_flags, free_pages);
	}

	return false;
}

3. rmqueue() 分配物理页面

在找到满足条件的zone或通过回退机制找到可用的内存后,便可以进行物理页面分配工作了,内核中通过rmqueue()函数实现这部分的功能。

rmqueue()函数用于从伙伴系统中申请物理页面, 其提供了高效的分配路径(pcp页面缓存页链表)以及正常的伙伴系统页面分配路径,并在最后尝试进行页面回收工作;

  • 1.**pcp快速分配: **首先会判断是否满足快速分配的条件, 即如果申请的页面大小小于8页或申请的时一个完整地透明大页,则可以直接从pcp(per-cpu-pages)链表中分配,这样可以省去对应的上锁解锁操作; 实现细节见下面小节;
  • 2.伙伴系统分配: 如果不满足快速分配的条件, 则尝试从伙伴系统中找到一块满足要求的页面进行分配,这里使用到了rmqueue_buddy()函数, 用来申请物理页面,该函数详细的实现过程见下面小节;
  • 3.**内存回收:**如果已经触发了水位线提升,说明zone中的页面紧张,需要进行内存回收,则触发kswapd守护进程,进行内存回收工作,实现细节见下面小节;

Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(中)_第4张图片

__no_sanitize_memory
static inline
struct page *rmqueue(struct zone *preferred_zone,
			struct zone *zone, unsigned int order,
			gfp_t gfp_flags, unsigned int alloc_flags,
			int migratetype)
{
	struct page *page;

	/*0.条件验证
	 */
	WARN_ON_ONCE((gfp_flags & __GFP_NOFAIL) && (order > 1));

	/*1.pcp链表中快速分配
	 *  pcp_allowed_order()判断是否可以从pcp链表中快速分配固定的物理页面
	 *  可以的话,调用rmqueue_pcplist()函数从pcp链表中快速申请物理页面
	 */
	if (likely(pcp_allowed_order(order))) {
		if (!IS_ENABLED(CONFIG_CMA) || alloc_flags & ALLOC_CMA ||
				migratetype != MIGRATE_MOVABLE) {
			/*rmqueue_pcplist()从pcp页面缓存链表中分配物理页面*/
			page = rmqueue_pcplist(preferred_zone, zone, order,
					migratetype, alloc_flags);
			if (likely(page))
				goto out;
		}
	}

	/*2.调用rmqueue_buddy()从伙伴系统中分配物理页面*/
	page = rmqueue_buddy(preferred_zone, zone, order, alloc_flags,
							migratetype);

out:
	/*3.触发kswapd守护进程,进行内存回收工作
	 *  test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags) 检查是否触发了水位提升
	 *  如果触发了,则表示需要内存回收;
	 */
	if ((alloc_flags & ALLOC_KSWAPD) &&
	    unlikely(test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags))) {
		clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags);
		/*唤醒守护进程,进行内存回收工作*/
		wakeup_kswapd(zone, 0, 0, zone_idx(zone));
	}

	VM_BUG_ON_PAGE(page && bad_range(zone, page), page);
	return page;
}

3.1. pcp链表快速分配

rmqueue()函数中, 先是判断是否能从pcp页面缓存中申请满足order的页面(pcp_allowed_order(order)), 紧接着会通过rmqueue_pcplist()函数从pcp链表中快速分配相应的物理页面;

pcp链表是一个per-cpu变量,每个cpu都有一个本地的per_cpu_pages链表,里面存放了多个从伙伴系统缓存过来的页面块, 当系统需要order个物理页面时,会判断order是否满足pcp的要求(order<=PAGE_ALLOC_COSTLY_ORDER);从pcp链表中分配可以减少对zone中锁的相关操作,效率高。

struct per_cpu_pages {
	int count;		/* 链表中页面的数量 */
	int high;		/* 高水位 ,当缓存的页面高于该水位时,会回收页面到伙伴系统*/
	int batch;		/* 每次回收到伙伴系统的页面数量 */
	short free_factor;	/* batch scaling factor during free */
#ifdef CONFIG_NUMA
	short expire;		/* When 0, remote pagesets are drained */
#endif

	/* 页面链表,每种迁移类型都有一个单页面 */
	struct list_head lists[NR_PCP_LISTS];
};

struct zone {
	struct pglist_data	*zone_pgdat;
	/*当前zone的pcp链表*/
	struct per_cpu_pages	__percpu *per_cpu_pageset;
	struct per_cpu_zonestat	__percpu *per_cpu_zonestats;
}
3.1.1 pcp_allowed_order() 可否从pcp链表分配

A:pcp_allowed_order()函数:会通过比对orderPAGE_ALLOC_COSTLY_ORDER以及pageblock_order, 判断是否可以从pcp页面缓存中分配物理页面;

/*判断申请order个物理页面是否可以从pcp链表中申请*/
static inline bool pcp_allowed_order(unsigned int order)
{
	/*1.PAGE_ALLOC_COSTLY_ORDER:最大普通页面分配大小,一般为3*/
	if (order <= PAGE_ALLOC_COSTLY_ORDER)
		return true;
#ifdef CONFIG_TRANSPARENT_HUGEPAGE//内核启用了透明大页
	/*2.pageblock_orde表示每个页面块的大小,通常为9,即512个页面*/
	if (order == pageblock_order)
		return true;
#endif
	return false;
}

#define PAGE_ALLOC_COSTLY_ORDER 3
3.1.2. rmqueue_pcplist() 从pcp分配

B:rmqueue_pcplist()函数从pcp页面缓存链表中分配物理页面,该函数会优先对当前pcp链表上锁,接着会根据迁移类型和阶数order获取到pcp中对应的list链表,最后会调用__rmqueue_pcplist()函数申请物理页面,无论申请成功与否,都会释放这个锁;

那么问题来了,前面提到了pcp链表相对于伙伴系统的一个优势便是省去了锁的相关操作,提高了效率,为什么这块还有锁呢?这是因为,rmqueue_pcplist()使用到的锁是pcp_spin_trylock轻量级自旋锁,且仅针对当前cpu的pcp链表上锁,相对于从伙伴系统中申请物理页面时对zone的全局数据结构free_list上锁简化了很多,锁的粒度较小,开销低。

static struct page *rmqueue_pcplist(struct zone *preferred_zone,
			struct zone *zone, unsigned int order,
			int migratetype, unsigned int alloc_flags)
{
	...
	/*1. 给pcp链表上锁*/
	pcp_trylock_prepare(UP_flags);
	pcp = pcp_spin_trylock(zone->per_cpu_pageset);
	...
	/*2.__rmqueue_pcplist从pcp链表分配物理页面 */
    list = &pcp->lists[order_to_pindex(migratetype, order)];//根据迁移类型与阶数,获取对应的list
	page = __rmqueue_pcplist(zone, order, migratetype, alloc_flags, pcp, list);
	...
	return page;
}

我们来看一下__rmqueue_pcplist()函数是如何从pcp链表中申请物理页面的:该函数是由一个do while {}循环组成的,每次循环都会检查所分配的页块是否已经初始化(check_new_pages实现);在循环中首先会检查一下pcp中对应迁移类型和阶数的链表list是否为空,如果list链表空的话,需要从伙伴系统中补充页面到list中(rmqueue_bulk);在链表页面充足的情况下,会将list中第一个页块分配出来(从链表中获取+从链表中删除)

/**  
 * @brief 用于从 PCP链表 中移除页面块,并在链表为空时,批量从伙伴系统补充页面到 PCP链表
 * 
 * @param zone 当前zone空间
 * @param order 要分配多少物理页面
 * @param migratetype 迁移类型
 * @param alloc_flags 分配标志
 * @param pcp cpu 对应的pcp链表
 * @param list 当前迁移类型和阶数对应的 链表
 **/
static inline
struct page *__rmqueue_pcplist(struct zone *zone, unsigned int order,
			int migratetype,
			unsigned int alloc_flags,
			struct per_cpu_pages *pcp,
			struct list_head *list)
{
	struct page *page;

	do {
		/*1. 检查pcp中对应迁移类型和阶数的链表是否为空
		 *   若为空,则调用 rmqueue_bulk 从伙伴系统中补充相应的页面;
		 */
		if (list_empty(list)) {
			int batch = READ_ONCE(pcp->batch);//批量补充的页块数量
			int alloced;
			if (batch > 1)
				batch = max(batch >> order, 2);
			/*批量从伙伴系统分配页面块,并加入 PCP链表*/
			alloced = rmqueue_bulk(zone, order,
					batch, list,
					migratetype, alloc_flags);

			pcp->count += alloced << order;
			if (unlikely(list_empty(list)))
				return NULL;
		}
		/*2.获取list链表中第一个页块,并分配*/
		page = list_first_entry(list, struct page, pcp_list);
		/*3.从list中删掉分配出去的页块*/
		list_del(&page->pcp_list);
		pcp->count -= 1 << order;
	} while (check_new_pages(page, order));//check_new_pages确保内存块经过校验,避免返回未正确初始化的页面

	return page;
}

3.2. rmqueue_buddy 从伙伴系统中分配物理页面

如果从pcp页表缓存链表中分配失败,则会尝试调用rmqueue_buddy从伙伴系统中分配物理页面。该函数是由一个do while {}循环组成的,在每次循环中,都会给伙伴系统上锁,并尝试通过以下三步进行页面分配:

  • 1.对于高优先级的任务,可以优先从HIGHATOMIC 区域申请内存,通过__rmqueue_smallest实现;
  • 2.正常路径分配,则尝试通过__rmqueue()函数从指定迁移类型migratetype的区域分配物理页面;
  • 3.对于正常路径分配失败的情况, 如果是在OOM上下文环境下,则普通任务允许使用HIGHATOMIC 区域内存,则尝试通过 __rmqueue_smallest()申请分配物理页面;
/** 
 * @brief 当无法从pcp链表中获取到页面时,尝试从伙伴系统中申请物理页面 
 * 
 * @param preferred_zone 优先选择的 zone,通常是 NUMA 系统中本地节点的
 * @param zone 当前zone区域
 * @param order 阶数
 * @param alloc_flags 分配标志
 * @param migratetype 迁移类型
 * 
 **/
static __always_inline
struct page *rmqueue_buddy(struct zone *preferred_zone, struct zone *zone,
			   unsigned int order, unsigned int alloc_flags,
			   int migratetype)
{
	struct page *page;
	unsigned long flags;
	/*1.do while 循环 去分配页块*/
	do {
		page = NULL;
		/*1.1 关中断自旋锁,用于保护zone的全局伙伴系统*/
		spin_lock_irqsave(&zone->lock, flags);

		/*1.2 优先尝试高原子分配,保证高优先级任务优先申请HIGHATOMIC区域
		 *    允许从HIGHATOMIC 区域申请内存,则直接调用__rmqueue_smallest申请
		 *    HIGHATOMIC是伙伴系统中的一种迁移类型,只有高优先级、高阶分配需求可用;
		 */
		if (alloc_flags & ALLOC_HIGHATOMIC)
			page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);

		/*1.3 正常分配路径
		 *    调用__rmqueue()从指定迁移类型migratetype的区域分配物理页面
		 */
		if (!page) {
			page = __rmqueue(zone, order, migratetype, alloc_flags);
			/*1.3.1 普通分配路径失败,
			 *      如果在OOM上下文情况下,则允许普通任务使用MIGRATE_HIGHATOMIC区域内存
			 *      尝试在MIGRATE_HIGHATOMIC 区域再次分配
			 */
			if (!page && (alloc_flags & ALLOC_OOM))
				page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
			/*1.2.3 还是分配失败,则释放锁,返回NULL*/
			if (!page) {
				spin_unlock_irqrestore(&zone->lock, flags);
				return NULL;
			}
		}
		/*1.4 zone中信息更新*/
		__mod_zone_freepage_state(zone, -(1 << order),
					  get_pcppage_migratetype(page));
		/*1.5 释放锁*/
		spin_unlock_irqrestore(&zone->lock, flags);
	} while (check_new_pages(page, order));//验证分配的页面块是否有效(如未被污染或未正确初始化
	
	/*2. 更新统计信息*/
	__count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order);
	zone_statistics(preferred_zone, zone, 1);

	return page;
}
3.2.1 __rmqueue()

我们首先看一下__rmqueue()函数,该函数首先会考虑CMA区域和普通区域的平衡问题,如果CMA区域空闲页面占zone空闲页面一半以上,说明可以优先从CMA区域分配(__rmquue_cma_fallback实现);如果未分配成功,需要通过正常路径分配页面,通过__rmqueue_smallest()进行页面分配工作;如果依然没有分配成功,则忽略cma平衡问题,直接从cma区域分配页面,或从其他迁移类型空闲链表分配页面;以下是具体的源码实现;

#ifdef CONFIG_CMA
static __always_inline struct page *__rmqueue_cma_fallback(struct zone *zone,
					unsigned int order)
{	
    /*调用__rmqueue_smallest在CMA区域分配物理页面*/
	return __rmqueue_smallest(zone, order, MIGRATE_CMA);
}
#else

static __always_inline struct page *
__rmqueue(struct zone *zone, unsigned int order, int migratetype,
						unsigned int alloc_flags)
{
	struct page *page;

	/*1.从CMA区域分配, 平衡CMA区域与普通区域内存分配
	 *  由于CMA区域与普通区域共享zone的内存,故需要平衡二者;
	 *  当CMA区域的空闲页数占zone中空闲页的一半以上,则优先从cma区域分配内存;
	 */
	if (IS_ENABLED(CONFIG_CMA)) {
		if (alloc_flags & ALLOC_CMA &&						//支持CMA区域分配
		    zone_page_state(zone, NR_FREE_CMA_PAGES) >		//判断cma区域空闲页面是否大于zone空闲页面的一半
		    zone_page_state(zone, NR_FREE_PAGES) / 2) 
		{
			page = __rmqueue_cma_fallback(zone, order);
			if (page)
				return page;
		}
	}
retry:
	/*2. 普通路径分配物理页面*/
	page = __rmqueue_smallest(zone, order, migratetype);
	/*3. 普通路径分配失败,则回退,尝试从cma或其他迁移类型区域分配页面*/
	if (unlikely(!page)) {
		/*3.1 尝试从cma区域分配页面*/
		if (alloc_flags & ALLOC_CMA)
			page = __rmqueue_cma_fallback(zone, order);
		/*3.2 尝试从其他迁移类型分配物理页面*/
		if (!page && __rmqueue_fallback(zone, order, migratetype,
								alloc_flags))
			goto retry;
	}
	return page;
}
3.2.2 __rmqueue_smallest() 页面切分, 分配

我们从以上两个函数中可以看出,无论是从哪个迁移类型的区域分配物理页面,最终都是调用__rmqueue_smallest()函数进行页面切割、分配,这也是伙伴系统物理页面分配策略的实现。该函数从特定迁移类型的空闲链表中分配最小可用页面块,具体步骤如下:

该函数会从给定order阶数开始从伙伴系统空闲链表中逐级向上遍历,并会在每个阶数的空闲链表中尝试切割分配页块:

  • 1.找到当前阶数对应的空闲链表,并在该空闲链表中找到符合迁移类型的页块;
  • 2.找到对应的页块后,将其从链表中移除,并尝试切割该页块,因为所找到的页块很有可能大于所需要的页块,需要通过expand()函数将其切割,并将切割后多余的页块放入对应阶数的空闲链表中;
  • 3.设置所分配页块的迁移类型;
/** 
 * @brief 从特定迁移类型的空闲链表中分配最小可用页面块
 * 
 * @param zone 当前zone区域
 * @param order 阶数
 * @param migratetype 迁移类型
 **/
static __always_inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
						int migratetype)
{
	unsigned int current_order;
	struct free_area *area;
	struct page *page;

	/*1. 遍历空闲链表,在最合适的链表中找到大小最合适的页块 
	 *   从order开始,逐级向上找,直到MAX_ORDER
	 *   会先找到当前阶数的空闲链表,再在改链表中找到对应迁移类型的页块;
	 *   若在当前空闲链表中没找到,则在下一阶数的空闲链表中查找;
	 *   若找到了,则执行分配操作;
	 */
	for (current_order = order; current_order <= MAX_ORDER; ++current_order) {
		/*1.1 从伙伴系统中获取当前阶数的空闲链表*/
		area = &(zone->free_area[current_order]);
		/*1.2 从空闲链表中获取指定迁移类型的页块*/
		page = get_page_from_free_area(area, migratetype);
		if (!page)
			continue;
		/*1.3 将找到的页块从空闲链表中移除*/
		del_page_from_free_list(page, zone, current_order);

		/*1.4 拆分页块
		 *    如果找到的页块大小current_order 大于 需要的页块大小order
		 *    则需要递归将页块拆分成更小的块,
		 *    将多余的页块拆分并返回到对应阶数的空闲链表中
		 */
		expand(zone, page, order, current_order, migratetype);
		/*1.5 设置分配页块的迁移类型*/
		set_pcppage_migratetype(page, migratetype);
		/*tracepoint跟踪点*/
		trace_mm_page_alloc_zone_locked(page, order, migratetype,
				pcp_allowed_order(order) &&
				migratetype < MIGRATE_PCPTYPES);
		return page;
	}
	return NULL;
}

你可能感兴趣的:(内存管理,linux,运维,服务器)