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执行实际分配:

  1. 首先尝试从指定order的free_area直接分配
  2. 如果失败,尝试从更高order分裂获取所需块
  3. 更新伙伴系统位图和统计信息
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)是解决内存碎片化的关键手段,其主要步骤包括:

  1. 扫描zone中的可移动页
  2. 尝试将这些页迁移到其他位置
  3. 合并产生的连续空闲块

核心函数__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选择并终止一个进程以释放内存。其核心逻辑包括:

  1. 选择分数最高的"bad"进程
  2. 考虑因素包括:
    • 进程内存使用量
    • 进程运行时间
    • 进程优先级
    • 子进程内存
  3. 发送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 监控与诊断工具

  1. /proc/buddyinfo
    查看伙伴系统各order的剩余块数,诊断内存碎片

  2. /proc/pagetypeinfo
    显示各迁移类型的页面分布

  3. vmstat 1
    监控系统内存事件:

    • pgscan_kswapd: kswapd扫描页数
    • pgsteal_direct: 直接回收获得的页数
    • oom_kill: OOM触发次数
  4. 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显示有足够零散内存

可能原因:
内存碎片导致无法满足连续分配

解决方案:

  1. 启用内存压缩:
    echo 1 > /proc/sys/vm/compact_memory
    
  2. 调整迁移类型:
    // 在驱动中设置GFP标志
    gfp_mask |= __GFP_MOVABLE;
    
  3. 预分配大块内存池

5. 高级话题与最新发展

5.1 CMA与伙伴系统的集成

连续内存分配器(CMA)与伙伴系统的协同工作:

  • CMA区域在系统启动时保留
  • 平时被伙伴系统作为可移动页面管理
  • 需要时通过alloc_contig_range分配连续大块内存

5.2 内存控制组(memcg)的影响

当使用cgroup v1内存控制器时,分配路径会增加:

  1. memcg水印检查
  2. memcg内存回收
  3. 统计计数更新

关键修改点:

// 在__alloc_pages_nodemask中
if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT))
    memcg_kmem_charge(page, gfp_mask, order);

5.3 异构内存系统支持

针对NUMA和异构内存设备(如PMEM),伙伴系统的扩展:

  1. NUMA策略感知的zonelist排序
  2. 设备特定内存域(ZONE_DEVICE)
  3. 迁移类型扩展(MIGRATE_CMA、MIGRATE_DEVICE)

在内存分配实践中,理解这些底层机制对于构建高性能、可靠的内核模块和驱动程序至关重要。特别是在嵌入式系统或内存受限环境中,合理利用伙伴系统的特性可以显著提升系统稳定性。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐