Linux内核伙伴系统实战:alloc_pages快速分配与慢速分配全解析
Linux内核伙伴系统实战:alloc_pages快速分配与慢速分配全解析
在Linux内核开发中,内存管理是最核心也是最复杂的子系统之一。作为内核开发者,我们经常需要深入理解内存分配的底层机制,特别是在性能调优和问题排查的场景下。伙伴系统(Buddy System)作为Linux物理内存管理的基础,其alloc_pages接口的实现逻辑直接影响着系统的内存分配效率和稳定性。
本文将从一个内核开发者的实际调试视角出发,全面剖析alloc_pages在快速分配(get_page_from_freelist)和慢速分配(__alloc_pages_slowpath)两种路径下的实现细节。不同于简单的API说明,我们会结合真实的内核代码(基于5.x版本)和流程图,深入讲解水位检查、内存回收等核心机制,帮助中高级开发者掌握内存分配的性能优化方法。
1. 伙伴系统与alloc_pages基础
伙伴系统是Linux内核用于管理物理内存页面的核心算法,其主要特点包括:
- 基于阶(order)的分配:内存块以2^order个连续页面为单位进行管理
- 高效合并与分割:通过"伙伴"关系实现内存块的快速合并与分割
- 多zone管理:将内存划分为DMA、NORMAL、HIGHMEM等不同zone区域
- 水位控制:通过min/low/high三个水位线管理内存压力状态
alloc_pages是伙伴系统最核心的分配接口,其函数调用链如下:
alloc_pages()
→ alloc_pages_node()
→ __alloc_pages_node()
→ __alloc_pages()
→ __alloc_pages_nodemask() // 核心实现
在__alloc_pages_nodemask中,内核会根据当前内存压力选择不同的分配路径:
struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
struct zonelist *zonelist, nodemask_t *nodemask)
{
// 快速路径尝试
page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
if (likely(page))
goto out;
// 快速路径失败,进入慢速路径
page = __alloc_pages_slowpath(alloc_mask, order, &ac);
out:
return page;
}
2. 快速分配路径:get_page_from_freelist详解
快速分配路径是内存分配的首选路径,当系统内存充足时,绝大多数分配请求都能在此路径完成。其核心逻辑可以分解为以下几个关键步骤:
2.1 内存域(zone)遍历检查
快速路径首先会遍历zonelist中的所有合适zone,检查每个zone是否满足分配条件:
for_next_zone_zonelist_nodemask(zone, z, ac->zonelist,
ac->high_zoneidx, ac->nodemask) {
// 检查CPU允许性
if (cpusets_enabled() && (alloc_flags & ALLOC_CPUSET) &&
!__cpuset_zone_allowed(zone, gfp_mask))
continue;
// 检查脏页限制
if (ac->spread_dirty_pages && !node_dirty_ok(zone->zone_pgdat))
continue;
// 检查水位线
mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
if (!zone_watermark_fast(zone, order, mark,
ac_classzone_idx(ac), alloc_flags)) {
// 水位不足时的处理
if (alloc_flags & ALLOC_NO_WATERMARKS)
goto try_this_zone;
if (node_reclaim_mode == 0 ||
!zone_allows_reclaim(ac->preferred_zoneref->zone, zone))
continue;
// 尝试节点回收
ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
...
}
try_this_zone:
// 实际分配尝试
page = buffered_rmqueue(ac->preferred_zoneref->zone, zone, order,
gfp_mask, alloc_flags, ac->migratetype);
if (page) {
prep_new_page(page, order, gfp_mask, alloc_flags);
return page;
}
}
2.2 水位检查机制
水位检查是快速路径的核心决策点,Linux内核定义了三种水位线:
| 水位线 | 描述 | 典型用途 |
|---|---|---|
| WMARK_MIN | 最低水位,分配将触发直接回收 | 紧急分配 |
| WMARK_LOW | 低水位,分配可能唤醒kswapd | 常规分配 |
| WMARK_HIGH | 高水位,kswapd休眠点 | 系统空闲状态 |
水位检查的关键函数是zone_watermark_fast,其简化逻辑如下:
bool zone_watermark_fast(struct zone *z, unsigned int order, unsigned long mark,
int classzone_idx, unsigned int alloc_flags)
{
long free_pages = zone_page_state(z, NR_FREE_PAGES);
// 快速检查:总空闲页是否足够
if (free_pages <= mark + z->lowmem_reserve[classzone_idx])
return false;
// 高阶分配需要检查连续空闲块
if (order) {
free_pages -= z->free_area[order].nr_free << order;
if (free_pages <= mark)
return false;
}
return true;
}
2.3 实际分配操作
当zone通过所有检查后,内核通过buffered_rmqueue执行实际分配:
- 首先尝试从指定order的free_area直接分配
- 如果失败,尝试从更高order分裂获取所需块
- 更新伙伴系统位图和统计信息
page = buffered_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(order && (alloc_flags & ALLOC_HARDER)))
reserve_highatomic_pageblock(page, zone, order);
return page;
}
3. 慢速分配路径:__alloc_pages_slowpath深度解析
当快速路径分配失败时,内核会进入慢速分配路径,执行更复杂的内存回收和压缩操作。慢速路径的主要阶段包括:
3.1 慢速路径总体流程
慢速路径的核心逻辑可以用以下伪代码表示:
retry:
1. 调整分配标志(降低水位要求)
2. 唤醒kswapd进行后台回收
3. 再次尝试快速分配
4. 如果失败,尝试直接内存压缩
5. 如果仍然失败,尝试直接内存回收
6. 最后手段:OOM killer
实际代码中的主要控制流:
static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
struct alloc_context *ac)
{
// 初始设置
alloc_flags = gfp_to_alloc_flags(gfp_mask);
// 唤醒kswapd
if (gfp_mask & __GFP_KSWAPD_RECLAIM)
wake_all_kswapds(order, ac);
// 第一次重试
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)
goto got_pg;
// 尝试内存压缩
if (can_direct_reclaim && order > PAGE_ALLOC_COSTLY_ORDER)
page = __alloc_pages_direct_compact(...);
retry:
// 可能调整zonelist和alloc_flags后再次尝试
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
// 尝试直接回收
page = __alloc_pages_direct_reclaim(...);
// 再次尝试压缩
page = __alloc_pages_direct_compact(...);
// 最后手段:OOM
page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
got_pg:
return page;
}
3.2 内存回收机制
慢速路径中的内存回收主要通过以下两种方式:
1. 后台回收(kswapd)
- 由
wake_all_kswapds()触发 - 异步执行,不阻塞当前进程
- 回收目标是将所有zone的空闲页提升到WMARK_HIGH
2. 直接回收(direct reclaim)
- 由
__alloc_pages_direct_reclaim()执行 - 同步执行,阻塞当前进程
- 使用更激进的回收策略
直接回收的关键调用链:
__alloc_pages_direct_reclaim()
→ try_to_free_pages()
→ do_try_to_free_pages()
→ shrink_zones()
→ shrink_node()
→ shrink_lruvec()
3.3 内存压缩机制
内存压缩(compaction)是解决内存碎片化的关键手段,其主要步骤包括:
- 扫描zone中的可移动页
- 尝试将这些页迁移到其他位置
- 合并产生的连续空闲块
核心函数__alloc_pages_direct_compact的执行流程:
page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags,
ac, compact_priority, &compact_result)
{
// 尝试压缩
compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac);
// 压缩后尝试分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
// 处理压缩结果
if (compact_result == COMPACT_SKIPPED ||
compact_result == COMPACT_DEFERRED)
return NULL;
return page;
}
3.4 OOM Killer机制
当所有回收和压缩手段都失败后,内核会触发OOM Killer选择并终止一个进程以释放内存。其核心逻辑包括:
- 选择分数最高的"bad"进程
- 考虑因素包括:
- 进程内存使用量
- 进程运行时间
- 进程优先级
- 子进程内存
- 发送SIGKILL终止选中的进程
OOM相关的关键函数:
page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress)
{
if (!oom_killer_disabled) {
oom_killer_process(gfp_mask, order, ac);
// 终止后再次尝试分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
}
return page;
}
4. 性能调优实战技巧
基于对alloc_pages两种路径的理解,我们可以采用以下方法优化内存分配性能:
4.1 水位线调优
通过修改/proc/sys/vm/下的参数调整内存水位行为:
| 参数 | 描述 | 调优建议 |
|---|---|---|
| min_free_kbytes | 最小保留内存 | 增大可减少直接回收,但会降低可用内存 |
| watermark_scale_factor | 水位计算因子 | 调整水位线之间的间隔比例 |
| extra_free_kbytes | 额外保留内存 | 在内存紧张系统可适当增加 |
4.2 分配标志选择
合理设置gfp_mask可以显著影响分配行为:
__GFP_WAIT:允许阻塞,启用慢速路径__GFP_HIGH:高优先级分配,可突破水位限制__GFP_NOFAIL:禁止失败,可能导致死锁__GFP_NORETRY:快速失败,不尝试回收
4.3 监控与诊断工具
-
/proc/buddyinfo
查看伙伴系统各order的剩余块数,诊断内存碎片 -
/proc/pagetypeinfo
显示各迁移类型的页面分布 -
vmstat 1
监控系统内存事件:- pgscan_kswapd: kswapd扫描页数
- pgsteal_direct: 直接回收获得的页数
- oom_kill: OOM触发次数
-
tracepoint
使用ftrace跟踪分配事件:echo 1 > /sys/kernel/debug/tracing/events/kmem/mm_page_alloc/enable cat /sys/kernel/debug/tracing/trace_pipe
4.4 实际案例:高阶分配优化
在处理大块内存分配(如order>3)时,常见问题及解决方案:
问题现象:
分配高阶内存失败,但/proc/buddyinfo显示有足够零散内存
可能原因:
内存碎片导致无法满足连续分配
解决方案:
- 启用内存压缩:
echo 1 > /proc/sys/vm/compact_memory - 调整迁移类型:
// 在驱动中设置GFP标志 gfp_mask |= __GFP_MOVABLE; - 预分配大块内存池
5. 高级话题与最新发展
5.1 CMA与伙伴系统的集成
连续内存分配器(CMA)与伙伴系统的协同工作:
- CMA区域在系统启动时保留
- 平时被伙伴系统作为可移动页面管理
- 需要时通过
alloc_contig_range分配连续大块内存
5.2 内存控制组(memcg)的影响
当使用cgroup v1内存控制器时,分配路径会增加:
- memcg水印检查
- memcg内存回收
- 统计计数更新
关键修改点:
// 在__alloc_pages_nodemask中
if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT))
memcg_kmem_charge(page, gfp_mask, order);
5.3 异构内存系统支持
针对NUMA和异构内存设备(如PMEM),伙伴系统的扩展:
- NUMA策略感知的zonelist排序
- 设备特定内存域(ZONE_DEVICE)
- 迁移类型扩展(MIGRATE_CMA、MIGRATE_DEVICE)
在内存分配实践中,理解这些底层机制对于构建高性能、可靠的内核模块和驱动程序至关重要。特别是在嵌入式系统或内存受限环境中,合理利用伙伴系统的特性可以显著提升系统稳定性。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)