本系列文章将对内存管理相关知识进行梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中内存管理机制进行数据实时拿取与分析。
在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:
《深入理解Linux内核》
《Linux操作系统原理与应用》
《奔跑吧Linux内核》
《深入理解Linux进程与内存》
《基于龙芯的Linux内核探索解析》
Linux内存管理 - 随笔分类 - LoyenWang - 博客园
专栏文章目录 - 知乎 (zhihu.com)
albertxu216/LinuxKernel_Learning/Linux6.5源码注释
在上篇文章中,我们介绍了分配和释放物理页面的核心接口、gfp_mask掩码以及在Linux6.5内核中是如何实现物理页面分配的,具体内容详见上一篇文章:Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(上)-CSDN博客。
通过几张图片帮助大家回顾一下zonelist与zone,node间的关系,以及alloc_pages
函数实现逻辑,便于本篇文章介绍物理页面分配中的快速路径分配;
我们可以看到,在__alloc_pages
函数中,先通过prepare_alloc_pages()
函数填充alloc_context
上下文结构体,再通过get_page_from_freelist()
快速分配物理页面,如果快速路径分配失败,则尝试通过慢速路径分配物理页面;本篇文章将围绕伙伴系统中的快速路径分配物理页面深入源码进行分析与梳理;
在快速路径分配物理页面过程中,会遍历zonelist
链表来找到一个合适的zone去分配物理页面,所以有必要温习一下zonelist
与zone
,node
之间的关系:
快速路径分配物理页面,顾名思义,讲究一个快,核心思想就是:在条件充足的时候,追求完美,面面俱到,尽可能考虑到分配效率、分配过程对系统的性能影响、节点脏页负载以及每个zone的水位线等情况;如果无法兼顾以上的追求(不能做到完美),则退一步降低要求去分配物理页面;如果依然找不到可分配的物理页面,则快速路径分配失败,尝试慢速路径分配物理页面。
用一句话宏观的介绍一下快速路径分配的流程:从zonelist
链表(包含本地node
和远端node
)中找到一个满足条件的zone
来分配物理页面,在找zone的过程中考虑到了与当前CPU是否匹配、node的脏页负载限制、碎片化、水位线等条件;在找到满足条件的zone之后,会尝试从伙伴系统或pcp链表中分配物理页面,这里会优先考虑pcp链表;如果没找到满足条件的zone,则尝试降低条件,重新遍历zonelist
链表,或去 “找“ 内存用于分配;
实现快速路径分配的内核函数是get_page_from_freelist()
,该函数会按照上述思想进行功能实现,先通过一张图宏观的看一下该函数的实现逻辑:
该函数尝试从伙伴系统的空闲页面链表中分配物理页面,用于扫描可用的 zonelist
,按照多种策略(如 NUMA、避免碎片化、分配标志等)尝试分配页面。如果分配失败,可以通过内存回收、延迟初始化的页面扩展等手段继续尝试。
将get_page_from_freelist
函数的实现思路总结如下:
一句话概括: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()
,分别实现了物理页面分配、内存回收、水位线检测功能,我们会在下面的小节中逐一分析;
水位线检查的目的是为了查看当前zone
中的物理页面是否在ALLOW_WMARK_LOW
低水位之下,如果在低水位线之下,则说明当前zone中内存不足,不建议从该zone中分配内存;
这部分功能通过函数zone_watermark_fast
实现,该函数针对一些特殊情况(如今申请一个物理页面)提供了快速通道以及回退机制(降低条件重新检查),正常情况下该函数会检查是否满足order的页面分配请求;
zone_page_state
函数对zone
结构体中vm_stat[]
进行统计,该字段存储了用于页面统计的信息;usable_free > mark + z->lowmem_reserve[highest_zoneidx]
mark + z->lowmem_reserve[highest_zoneidx]
是指: 低水位线对应的物理页面数+zone预留的页面数, lowmem_reserve
是指每个zone预留的内存,防止高端zone在没内存的情况下过度使用低端zone的内存资源;__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;
}
在找到满足条件的zone
或通过回退机制找到可用的内存后,便可以进行物理页面分配工作了,内核中通过rmqueue()
函数实现这部分的功能。
rmqueue()
函数用于从伙伴系统中申请物理页面, 其提供了高效的分配路径(pcp页面缓存页链表)以及正常的伙伴系统页面分配路径,并在最后尝试进行页面回收工作;
pcp(per-cpu-pages)
链表中分配,这样可以省去对应的上锁解锁操作; 实现细节见下面小节;rmqueue_buddy()
函数, 用来申请物理页面,该函数详细的实现过程见下面小节;kswapd
守护进程,进行内存回收工作,实现细节见下面小节;__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;
}
在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;
}
A:pcp_allowed_order()
函数:会通过比对order
与PAGE_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
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;
}
如果从pcp页表缓存链表中分配失败,则会尝试调用rmqueue_buddy
从伙伴系统中分配物理页面。该函数是由一个do while {}循环组成的,在每次循环中,都会给伙伴系统上锁,并尝试通过以下三步进行页面分配:
HIGHATOMIC
区域申请内存,通过__rmqueue_smallest
实现;__rmqueue()
函数从指定迁移类型migratetype的区域分配物理页面; __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;
}
我们首先看一下__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;
}
我们从以上两个函数中可以看出,无论是从哪个迁移类型的区域分配物理页面,最终都是调用__rmqueue_smallest()
函数进行页面切割、分配,这也是伙伴系统物理页面分配策略的实现。该函数从特定迁移类型的空闲链表中分配最小可用页面块,具体步骤如下:
该函数会从给定order阶数开始从伙伴系统空闲链表中逐级向上遍历,并会在每个阶数的空闲链表中尝试切割分配页块:
expand()
函数将其切割,并将切割后多余的页块放入对应阶数的空闲链表中;/**
* @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;
}