Linux内存管理(六): 分配物理内存alloc_pages

基于Linux 5.10, 体系结构是aarch64


上文介绍了linux对物理内存的描述,本篇介绍linux下物理页面的分配函数alloc_pages

1.API接口

alloc_pages是内核中常用的分配物理内存页面的函数, 函数定义在[include/linux/gfp.h], 用于分配2^order连续的物理页。

#ifdef CONFIG_NUMA
static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)
{
	return alloc_pages_current(gfp_mask, order);
}
#else
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
	return alloc_pages_node(numa_node_id(), gfp_mask, order);
}
#endif

除了alloc_pages, linux内核还提供了另一个获取page frame的基本函数__get_free_pages

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);

两者参数一样,但是返回值不同,__get_free_pages()比alloc_pages()多了一个地址转换的工作。
alloc_pages()返回指向第一个page的struct page指针,__get_free_pages()返回第一个page映射后的虚拟地址。

2.参数说明

gfp_mask: 分配掩码

定义在include/linux/gfp.h
gfp的全称是get free page, 因此gfp_mask表示页面分配的方法。
为了兼容多种内存分配的场景,gfp_mask主要分为以下几类:

1. 内存管理区修饰符 (zone modifiers)
内存管理区修饰符主要描述从哪些内存管理区来分配内存

flagdescription
__GFP_DMA从ZONE_DMA区中分配内存                                   
__GFP_HIGNMEM从ZONE_HIGHMEM区中分配内存
__GFP_DMA32从ZONE_DMA32区中分配内存
__GFP_MOVABLE内存规整时可以迁移或回收页面

2. 移动和替换修饰符(mobility and placement modifiers)
移动和替换修饰符主要表示分配出来的页面具有的迁移属性

flagdescription
__GFP_RECLAIMABLE分配的内存页面可以回收                                                               
__GFP_WRITE申请的页面会被弄成脏页
__GFP_HARDWALL强制使用cpuset内存分配策略
__GFP_THISNODE在指定的节点上分配内存
__GFP_ACCOUNTkmemcg会记录分配过程

3. 水位修饰符 (watermark modifiers)

flagdescription
__GFP_ATOMIC高优先级分配内存,分配器可以分配最低警戒水位线下的预留内存
__GFP_HIGH分配内存的过程中不可以睡眠或执行页面回收动作
__GFP_MEMALLOC允许访问所有的内存
__GFP_NOMEMALLOC不允许访问最低警戒水位线下的系统预留内存

4. 页面回收修饰符(reclaim modifiers)

flagdescription
__GFP_IO启动物理I/O传输
__GFP_FS允许调用底层FS文件系统。可避免分配器递归到可能已经持有锁的文件系统中, 避免死锁
__GFP_DIRECT_RECLAIM               分配内存过程中可以使用直接内存回收
__GFP_KSWAPD_RECLAIM内存到达低水位时唤醒kswapd线程异步回收内存
__GFP_RECLAIM表示是否可以直接内存回收或者使用kswapd线程进行回收
__GFP_RETRY_MAYFAIL分配内存可以可能会失败,但是在申请过程中会回收一些不必要的内存,是整个系统受益
__GFP_NOFAIL内存分配失败后无限制的重复尝试,知道分配成功
__GFP_NORETRY直接页面回收或者内存规整后还是无法分配内存时,不启用retry反复尝试分配内存,直接返回NULL

5. 行为修饰符 (action modifiers)

flagdescription
__GFP_NOWARN关闭内存分配过程中的WARNING
__GFP_COMP分配的内存页面将被组合成复合页compound page
__GFP_ZERO返回一个全部填充为0的页面

6. 组合类型标志(Useful GFP flag combinations)
前面描述的修饰符种过于繁多,因此linux定义了一些组合的类型标志,供开发者使用。

flagelementdescription
GFP_ATOMIC__GFP_HIGH |__GFP_ATOMIC
|__GFP_KSWAPD_RECLAIM
分配过程不能休眠,分配具有高优先级,可以访问系统预留内存
GFP_KERNEL__GFP_RECLAIM |__GFP_IO
|__GFP_FS
分配内存时可以被阻塞(即休眠)
GFP_KERNEL_ACCOUNTGFP_KERNEL |__GFP_ACCOUNT和GFP_KERNEL作用一样,但是分配的过程会被kmemcg记录
GFP_NOWAIT__GFP_KSWAPD_RECLAIM分配过程中不允许因直接内存回收而导致停顿
GFP_NOIO__GFP_RECLAIM不需要启动任何的I/O操作
GFP_NOFS__GFP_RECLAIM |__GFP_IO不会有访问任何文件系统的操作
GFP_USER__GFP_RECLAIM |__GFP_IO
|__GFP_FS |__GFP_HARDWALL
用户空间的进程分配内存
GFP_DMA__GFP_DMA从ZONE_DMA区分配内存
GFP_DMA32__GFP_DMA32从ZONE_DMA32区分配内存
GFP_HIGHUSERGFP_USER | __GFP_HIGHMEM用户进程分配内存,优先使用ZONE_HIGHMEM, 且这些页面不允许迁移
GFP_HIGHUSER_MOVABLEGFP_HIGHUSER | __GFP_MOVABLE和GFP_HIGHUSER类似,但是页面可以迁移
GFP_TRANSHUGE_LIGHTGFP_HIGHUSER_MOVABLE
| __GFP_COMP | __GFP_NOMEMALLOC
| __GFP_NOWARN) & ~__GFP_RECLAIM
透明大页的内存分配, light表示不进行内存压缩和回收
GFP_TRANSHUGEGFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM和GFP_TRANSHUGE_LIGHT类似,通常khugepaged使用该标志

note: GFP_KERNEL是最常见使用的标志,需要注意的是该标志会引起休眠,所以避免在中断上下文使用该标志来分配内存

order: 分配级数

页面分配器使用伙伴系统按照顺序请求页面分配。所以只能以2的幂内存分配。例如,请求order=3的页面分配,最终会分配2 ^ 3 = 8页。
arm64当前默认MAX_ORDER为11, 即最多一次性分配2 ^(MAX_ORDER-1)个页

/* Free memory management - zoned buddy allocator.  */
#define MAX_ORDER 11

3.内核实现

从上面函数的定义来看,在NUMA和UMA的内存架构下,函数的实现有区别
在这里插入图片描述

alloc_pages_current

在NUMA内存架构下,会调用到alloc_pages_current()函数
如上图所示,该函数主要有2个动作:

  1. 如果当前是__GFP_THISNODE标志 限制为本地节点, 则使用默认内存策略; 如果当前处于中断上下文,也使用默认内存分配策略(本地节点)
  2. 获取当前NUMA的内存分配策略, 如果当前的策略是MPOL_INTERLEAVE, 说明需要跨节点分配内存,就会走alloc_page_interleave()分支; 反之,则按默认的方式分配内存,即在当前的节点分配内存,就和UMA的流程一致了。

NUMA的内存分配策略定义在[include/uapi/linux/mempolicy.h]

enum {
	MPOL_DEFAULT,
	MPOL_PREFERRED,
	MPOL_BIND,
	MPOL_INTERLEAVE,
	MPOL_LOCAL,
	MPOL_MAX,	/* always last member of enum */
};

MPOL_PREFERRED: 从preferred节点分配内存,如果分配不到再选择其他node
MPOL_BIND:从绑定的1个或多个节点上分配内存
MPOL_INTERLEAVE:在所有可满足需求的节点上交叉分配上分配内存。
MPOL_LOCAL: 从本地节点上分配内存

__alloc_pages_nodemask

the ‘heart’ of the zoned buddy allocator“ .
该函数是内存分配的核心,无论是UMA架构或者NUMA架构都会调用到这里。
__alloc_pages_nodemask()主要执行以下步骤:
1. prepare_alloc_pages()
初始化页面分配器中会用到的参数,这些参数会临时存放在alloc_context数据结构中

struct alloc_context {
	struct zonelist *zonelist;
	nodemask_t *nodemask;
	struct zoneref *preferred_zoneref;
	int migratetype;
	enum zone_type high_zoneidx;
	bool spread_dirty_pages;
};

zonelist:指向用于分配页面的区域列表;
nodemask:指定内存分配的Node,如果没有指定,则在所有节点中进行分配;
preferred_zone:指定要在快速路径中首先分配的区域,在慢路径中指定了zonelist中的第一个可用区域;
migratetype:页面迁移类型;
high_zoneidx:允许内存分配的最高zone;
spread_dirty_pages:指定是否进行脏页的传播;

2. alloc_flags_nofragment()
根据区域和gfp掩码请求添加分配标志

static inline unsigned int
alloc_flags_nofragment(struct zone *zone, gfp_t gfp_mask)
{
	unsigned int alloc_flags;

	alloc_flags = (__force int) (gfp_mask & __GFP_KSWAPD_RECLAIM);   ---------1#ifdef CONFIG_ZONE_DMA32
	if (!zone)
		return alloc_flags;

	if (zone_idx(zone) != ZONE_NORMAL)
		return alloc_flags;

	BUILD_BUG_ON(ZONE_NORMAL - ZONE_DMA32 != 1);
	if (nr_online_nodes > 1 && !populated_zone(--zone))
		return alloc_flags;

	alloc_flags |= ALLOC_NOFRAGMENT;                           ------------ (2)
#endif /* CONFIG_ZONE_DMA32 */
	return alloc_flags;
}

(1) 如果gfp_mask限定了使用__GFP_KSWAPD_RECLAIM, 则在alloc标志中添加ALLOC_KSWAPD, 在内存不足时以唤醒kswapd
(2) ZONE_DMA32分配内存时,增加一个alloc标志位ALLOC_NOFRAGMENT, 表示需要避免碎片化。

alloc_flags主要是函数内部使用,gfp_to_alloc_flags()函数可以根据gfp_mask对alloc flags进行调整
更多的alloc flags定义在[mm/internal.h]

#define ALLOC_WMARK_MIN		WMARK_MIN
#define ALLOC_WMARK_LOW		WMARK_LOW
#define ALLOC_WMARK_HIGH	WMARK_HIGH
#define ALLOC_NO_WATERMARKS	0x04 /* don't check watermarks at all */
#define ALLOC_OOM		ALLOC_NO_WATERMARKS
#define ALLOC_HARDER		 0x10 /* try to alloc harder */
#define ALLOC_HIGH		 0x20 /* __GFP_HIGH set */
#define ALLOC_CPUSET		 0x40 /* check for correct cpuset */
#define ALLOC_CMA		 0x80 /* allow allocations from CMA areas */
#define ALLOC_NOFRAGMENT	0x100 /* avoid mixing pageblock types */
#define ALLOC_KSWAPD		0x800 /* allow waking of kswapd, __GFP_KSWAPD_RECLAIM set */

ALLOC_WMARK_MIN:仅在最小水位water mark及以上限制页面分配;
ALLOC_WMARK_LOW:仅在低水位water mark及以上限制页面分配;
ALLOC_WMARK_HIGH:仅在高水位water mark及以上限制页面分配;
ALLOC_NO_WATERMARKS: 页面分配时不检查水位
ALLOC_HARDER: 尽力分配,一般在gfp_mask设置了__GFP_ATOMIC时会使用。如果页面分配失败,则尽可能分配MIGRATE_HIGHATOMIC类型的空闲页面。
ALLOC_HIGH:高优先级分配,一般在gfp_mask设置了__GFP_HIGH时使用
ALLOC_CPUSET:检查是否为正确的cpuset;
ALLOC_CMA: 允许从CMA区域进行分配
ALLOC_KSWAPD: 内存不足时唤醒kswapd内核线程

3. get_page_from_freelist
该函数的主要作用是从空闲页面链表中尝试分配内存,是内存分配的fastpath, 流程如下图所示:
在这里插入图片描述
(1) 从preferred zone开始遍历zonelist, 这里使用的是for_next_zone_zonelist_nodemask宏来遍历。 需要注意的是扫描zone的方向是从高端zone到低端zone; preferred zone是通用first_zones_zonelist计算得到的。
(2)判断该zone是否满足分配需求,如果zone空间不足,则进行node_reclaim(), 尝试页面回收
(3)如果回收后的空间满足要求,则调用rmqueue()从伙伴系统中进行内存分配。分配成功则返回page.

4. __alloc_pages_slowpath
如果快速路径分配内存失败了,则跳转到该函数进行慢速路径的内存分配。 流程如下图所示:
在这里插入图片描述

慢速路径的内存分配比较复杂:

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
						struct alloc_context *ac)
{
	bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
	const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
	struct page *page = NULL;

retry_cpuset:
	compaction_retries = 0;
	no_progress_loops = 0;
	compact_priority = DEF_COMPACT_PRIORITY;
	cpuset_mems_cookie = read_mems_allowed_begin();

	alloc_flags = gfp_to_alloc_flags(gfp_mask);                  --------------------1)
	ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,              --------------------2)
					ac->highest_zoneidx, ac->nodemask);
	if (!ac->preferred_zoneref->zone)
		goto nopage;

	if (alloc_flags & ALLOC_KSWAPD)
		wake_all_kswapds(order, gfp_mask, ac);                       ----------------------3)

	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);          ---------------4if (page)
		goto got_pg;

	if (can_direct_reclaim &&
			(costly_order ||
			   (order > 0 && ac->migratetype != MIGRATE_MOVABLE))
			&& !gfp_pfmemalloc_allowed(gfp_mask)) {
		page = __alloc_pages_direct_compact(gfp_mask, order,            -----------------5)
						alloc_flags, ac,
						INIT_COMPACT_PRIORITY,
						&compact_result);
		if (page)
			goto got_pg;
		if (costly_order && (gfp_mask & __GFP_NORETRY)) {

			if (compact_result == COMPACT_SKIPPED ||
			    compact_result == COMPACT_DEFERRED)
				goto nopage;

			compact_priority = INIT_COMPACT_PRIORITY;
		}
	}

(1) 通过gfp_to_alloc_flags(), 根据gfp_mask对内存分配标识进行调整
(2)通过first_zones_zonelist()重新计算preferred zone; 因为可能在fastpath中使用的nodemask不同,或者cpuset进行了修改,正在retry, 这样需要重新计算preferred zone,以免无限的遍历不符合要求的zone
(3) 如果alloc_flag标志ALLOC_KSWAPD, 那么会通过wake_all_kswapds唤醒kswapd内核线程
(4)使用调整后的标志来尝试第一次慢速路径内存分配,分配的函数也是get_page_from_freelist
(5)如果分配失败,满足“允许直接回收内存(can_direct_reclaim)” 或者 "不适用pfmemalloc的内存分配请求"等条件,将会进行一次内存的压缩并分配页面

如果上面的分配都失败了,会进行retry操作。

retry:
	if (alloc_flags & ALLOC_KSWAPD)
		wake_all_kswapds(order, gfp_mask, ac);                ---------------------- (1)

	reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
	if (reserve_flags)
		alloc_flags = current_alloc_flags(gfp_mask, reserve_flags);

	if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
		ac->nodemask = NULL;
		ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,             
					ac->highest_zoneidx, ac->nodemask);
	}

	/* Attempt with potentially adjusted zonelist and alloc_flags */
	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);  --------------- (2)
	if (page)
		goto got_pg;

	/* Caller is not willing to reclaim, we can't balance anything */
	if (!can_direct_reclaim)                              ------------------------- (3)
		goto nopage;

	/* Avoid recursion of direct reclaim */
	if (current->flags & PF_MEMALLOC)
		goto nopage;

	/* Try direct reclaim and then allocating */
	page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
							&did_some_progress);          ----------------------- (4)
	if (page)
		goto got_pg;

	/* Try direct compaction and then allocating */
	page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
					compact_priority, &compact_result);            ------------------ (5)
	if (page)
		goto got_pg;

	/* Do not loop if specifically requested */
	if (gfp_mask & __GFP_NORETRY)
		goto nopage;

	if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
		goto nopage;

	if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
				 did_some_progress > 0, &no_progress_loops))        --------------------- (6)
		goto retry;


	if (did_some_progress > 0 &&
			should_compact_retry(ac, order, alloc_flags,                ------------------- (7)
				compact_result, &compact_priority,
				&compaction_retries))
		goto retry;


	/* Deal with possible cpuset update races before we start OOM killing */
	if (check_retry_cpuset(cpuset_mems_cookie, ac))         -------------------8goto retry_cpuset;

	/* Reclaim has failed us, start killing things */
	page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);  ----------------- (9)
	if (page)
		goto got_pg;

	/* Avoid allocations with no watermarks from looping endlessly */
	if (tsk_is_oom_victim(current) &&                            ----------------------- (10)
	    (alloc_flags & ALLOC_OOM ||
	     (gfp_mask & __GFP_NOMEMALLOC)))
		goto nopage;

	/* Retry as long as the OOM killer is making progress */
	if (did_some_progress) {
		no_progress_loops = 0;
		goto retry;
	}

(1) retry 的过程中会重新唤醒kswapd线程(防止意外的休眠)
(2)调整zone后通过get_page_from_freelist 重新进行内存分配
(3) 如果分配失败了,并且不能够直接内存回收, 就跳转到"no_page"
(4)__alloc_pages_direct_reclaim()尝试直接内存回收后分配页面
(5) __alloc_pages_direct_compact()进行第二次直接内存压缩后分配页面
(6) should_reclaim_retry()会判断是否需要重新回收,然后调转到“retry”. 如果gfp_mask中有noretry标志或者__GFP_RETRY_MAYFAIL标志,那么不会重新retry, 直接跳转到"no_page"
(7) should_compact_retry()会判断是否需要重新压缩,然后跳转到”retry"
(8)check_retry_cpuset(), 如果检测到由于cpuset发生变化而检测到竞争条件,跳转到最开始的"retry_cpuset"
(9) __alloc_pages_may_oom(), 如果内存回收失败,会尝试进行oom kill 一些进程,进行内存的回收
(10) 如果当前task由于OOM而处于被杀死的状态,则跳转移至“nopage”

“no_page”标签主要是对slowpath最后的补充处理


nopage:
	/* Deal with possible cpuset update races before we fail */
	if (check_retry_cpuset(cpuset_mems_cookie, ac))             
		goto retry_cpuset; 
	if (gfp_mask & __GFP_NOFAIL) {                      --------------------1if (WARN_ON_ONCE(!can_direct_reclaim))
			goto fail;
		WARN_ON_ONCE(current->flags & PF_MEMALLOC);
		WARN_ON_ONCE(order > PAGE_ALLOC_COSTLY_ORDER);
		page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);  -----------------2if (page)
			goto got_pg;
		cond_resched();
		goto retry;
	}
fail:
	warn_alloc(gfp_mask, ac->nodemask,
			"page allocation failure: order:%u", order);
got_pg:
	return page;
}

(1) 如果gfp_mask标志位有nofail选项,则将重试直到分配到页面为止; 如果没有该标志,说明page没有分配成功,直接返回NULL
(2)__alloc_pages_cpuset_fallback(), 使用ALLOC_HARDER标志,如果节点耗尽,则回退以忽略cpuset的限制。

4.小结

页面分配几乎涉及到内存管理的所有的知识点,本篇只是浅尝辄止的描述了gfp_mask分配掩码, alloc_flag分配标志, 快速分配和慢速分配的流程,其他涉及到的如伙伴系统,水线,页面回收,页面规整以及oom killer放到以后再进行分析。

5.参考资料

http://jake.dothome.co.kr/zonned-allocator-alloc-pages-fastpath/

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值