基于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)
内存管理区修饰符主要描述从哪些内存管理区来分配内存
flag | description |
---|---|
__GFP_DMA | 从ZONE_DMA区中分配内存 |
__GFP_HIGNMEM | 从ZONE_HIGHMEM区中分配内存 |
__GFP_DMA32 | 从ZONE_DMA32区中分配内存 |
__GFP_MOVABLE | 内存规整时可以迁移或回收页面 |
2. 移动和替换修饰符(mobility and placement modifiers)
移动和替换修饰符主要表示分配出来的页面具有的迁移属性
flag | description |
---|---|
__GFP_RECLAIMABLE | 分配的内存页面可以回收 |
__GFP_WRITE | 申请的页面会被弄成脏页 |
__GFP_HARDWALL | 强制使用cpuset内存分配策略 |
__GFP_THISNODE | 在指定的节点上分配内存 |
__GFP_ACCOUNT | kmemcg会记录分配过程 |
3. 水位修饰符 (watermark modifiers)
flag | description |
---|---|
__GFP_ATOMIC | 高优先级分配内存,分配器可以分配最低警戒水位线下的预留内存 |
__GFP_HIGH | 分配内存的过程中不可以睡眠或执行页面回收动作 |
__GFP_MEMALLOC | 允许访问所有的内存 |
__GFP_NOMEMALLOC | 不允许访问最低警戒水位线下的系统预留内存 |
4. 页面回收修饰符(reclaim modifiers)
flag | description |
---|---|
__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)
flag | description |
---|---|
__GFP_NOWARN | 关闭内存分配过程中的WARNING |
__GFP_COMP | 分配的内存页面将被组合成复合页compound page |
__GFP_ZERO | 返回一个全部填充为0的页面 |
6. 组合类型标志(Useful GFP flag combinations)
前面描述的修饰符种过于繁多,因此linux定义了一些组合的类型标志,供开发者使用。
flag | element | description |
---|---|---|
GFP_ATOMIC | __GFP_HIGH |__GFP_ATOMIC |__GFP_KSWAPD_RECLAIM | 分配过程不能休眠,分配具有高优先级,可以访问系统预留内存 |
GFP_KERNEL | __GFP_RECLAIM |__GFP_IO |__GFP_FS | 分配内存时可以被阻塞(即休眠) |
GFP_KERNEL_ACCOUNT | GFP_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_HIGHUSER | GFP_USER | __GFP_HIGHMEM | 用户进程分配内存,优先使用ZONE_HIGHMEM, 且这些页面不允许迁移 |
GFP_HIGHUSER_MOVABLE | GFP_HIGHUSER | __GFP_MOVABLE | 和GFP_HIGHUSER类似,但是页面可以迁移 |
GFP_TRANSHUGE_LIGHT | GFP_HIGHUSER_MOVABLE | __GFP_COMP | __GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM | 透明大页的内存分配, light表示不进行内存压缩和回收 |
GFP_TRANSHUGE | GFP_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个动作:
- 如果当前是__GFP_THISNODE标志 限制为本地节点, 则使用默认内存策略; 如果当前处于中断上下文,也使用默认内存分配策略(本地节点)
- 获取当前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); --------------- (4)
if (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)) ------------------- (8)
goto 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) { -------------------- (1)
if (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); ----------------- (2)
if (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/