4. 内存块分配流程:__alloc_pages_nodemask()、rmqueue()、__rmqueue_smallest()

伙伴系统的内存分配请求从页面分配器的顶层入口 __alloc_pages_nodemask() 开始,经过快速路径与慢速路径的多次尝试,最终通过 rmqueue() 从特定内存区域取出物理页面。本节将沿着这条调用链,逐层剖析核心函数的执行逻辑,并重点展示 __rmqueue_smallest() 如何在伙伴链表中完成块的拆分与摘取。

4.1 顶层入口:__alloc_pages_nodemask()

__alloc_pages_nodemask() 是 Linux 内核中用于分配连续物理页面的核心函数,位于 mm/page_alloc.c。它封装了从快速路径到慢速路径的完整分配逻辑,是伙伴系统对外暴露的主要接口。

函数原型:
struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
                                    int preferred_nid, nodemask_t *nodemask);
  • gfp_mask:分配标志,控制行为(如是否允许回收、I/O、移动等)。
  • order:请求的阶数(0 表示单页,最大一般为 10 或 11)。
  • preferred_nid:首选 NUMA 节点。
  • nodemask:允许分配的节点掩码。

该函数的执行流程分为两个主要阶段:

  1. 快速路径(fast path):在允许的节点和区域中,尝试直接从伙伴系统分配。如果成功,直接返回页面。
  2. 慢速路径(slow path):当快速路径失败时,进入更复杂的重试逻辑,包括异步内存回收、压缩(compaction)、OOM 等。

下图展示了 __alloc_pages_nodemask() 的顶层决策流程:

__alloc_pages_nodemask() 顶层流程 __alloc_pages_nodemask() 快速路径:get_page_from_freelist() 成功? 返回 page 慢速路径:__alloc_pages_slowpath() 回收 | 压缩 | OOM 重试 返回 page 或 NULL
关键观察:大多数分配请求在快速路径中即可满足。慢速路径的进入意味着系统内存压力较大,需要触发回收或压缩操作。

4.2 区域分配器:rmqueue()

当 get_page_from_freelist() 在快速路径中选定一个内存区域(zone)后,会调用 rmqueue() 从该区域的伙伴系统中实际摘取页面块。rmqueue() 是伙伴系统分配器的核心枢纽,它根据请求的阶数决定使用哪个底层函数。

rmqueue() 的主要逻辑如下:

条件 调用的子函数 说明
order == 0 rmqueue_bulk() + per-CPU 页框缓存 单页分配走 per-CPU 缓存,避免锁竞争
order < MAX_ORDER __rmqueue_smallest() 小阶数分配,从伙伴链表直接摘取
order 较大或需要迁移 __rmqueue_fallback() 跨迁移类型回退分配
核心代码片段(rmqueue 简化逻辑):
static inline struct page *rmqueue(struct zone *preferred_zone,
                                   struct zone *zone, unsigned int order,
                                   gfp_t gfp_flags, unsigned int alloc_flags)
{
    struct page *page;

    if (likely(order == 0)) {
        page = rmqueue_bulk(zone, 0, alloc_flags, gfp_flags);
        if (unlikely(!page))
            goto failed;
    } else {
        page = __rmqueue_smallest(zone, order, MIGRATE_UNMOVABLE);
        if (unlikely(!page))
            page = __rmqueue_fallback(zone, order, MIGRATE_UNMOVABLE);
    }

    if (page) {
        // 更新统计、设置页面属性等
    }
    return page;
}

下图展示了 rmqueue() 内部根据 order 选择不同路径的决策树:

rmqueue() 内部路径选择 rmqueue(zone, order) order == 0 ? rmqueue_bulk() per-CPU 缓存 __rmqueue_smallest() 成功? 返回 page __rmqueue_fallback() 回退

4.3 核心拆分器:__rmqueue_smallest()

__rmqueue_smallest() 是伙伴系统分配算法最直接的体现。它从指定迁移类型的伙伴链表中,从请求阶数开始向上搜索,找到第一个非空链表,然后执行块的拆分与摘取。

函数原型:
static inline struct page *__rmqueue_smallest(struct zone *zone,
                                              unsigned int order,
                                              int migratetype);

该函数的执行步骤如下:

  1. 搜索起始阶:从 order 开始,逐阶向上检查 zone->free_area[current_order].free_list[migratetype] 是否为空。
  2. 找到非空链表:当在阶 current_order 找到可用块时,从链表中摘取该块。
  3. 拆分块:如果 current_order > order,则将多余的块拆分成更小的伙伴块,并放回对应阶的链表中。
  4. 返回结果:返回摘取到的页面块的首地址。

下表总结了拆分过程中各阶的变化:

步骤 当前阶 操作 剩余块去向
1 order=3(请求 8 页) 链表为空,向上搜索 —
2 order=4(16 页) 找到非空链表,摘取块 A —
3 order=3 将块 A 拆分为两个 8 页块:B 和 C 块 C 放入 order=3 链表
4 order=2 将块 B 拆分为两个 4 页块:D 和 E 块 E 放入 order=2 链表
5 order=3(最终) 返回块 D(8 页,满足请求) —

下图以 order=3(8 页)请求为例,展示从 order=4 链表中摘取并拆分的完整过程:

__rmqueue_smallest() 拆分示例(请求 order=3) ① 初始:order=4 链表有块 order=4 链表 块 A (16页) ② 摘取块 A,拆分为两个 order=3 块 order=3 链表 块 C (8页) 放回链表 块 B (8页) 继续拆分 ③ 拆分块 B 为两个 order=2 块 order=2 链表 块 E (4页) 放回链表 块 D (4页) 返回给调用者 ④ 最终:返回块 D(4页),但请求是 8 页? 注意:实际拆分到 order=3 时已满足请求,块 D 应为 8 页。 上图第三步仅为演示连续拆分机制;实际在 order=3 拆分后即返回块 B(8页)。
性能关键点:拆分操作会修改多个阶的伙伴链表,因此 __rmqueue_smallest() 必须在持有 zone 的 lock 自旋锁时执行。这也是为什么 order=0 的分配会优先走 per-CPU 缓存——避免锁竞争,提升性能。

4.4 分配流程总结

从 __alloc_pages_nodemask() 到 __rmqueue_smallest() 的完整调用链可以归纳为以下层级关系:

层级 函数 职责 关键操作
顶层 __alloc_pages_nodemask() 分配入口,协调快速/慢速路径 节点/区域选择,重试逻辑
区域层 get_page_from_freelist() 遍历区域,调用 rmqueue 水位检查,区域选择
分配层 rmqueue() 根据 order 选择分配策略 per-CPU 缓存 / 伙伴链表
伙伴层 __rmqueue_smallest() 伙伴链表搜索与块拆分 向上搜索、拆分、摘取
学习建议:理解 __rmqueue_smallest() 的拆分逻辑是掌握伙伴系统的关键。建议读者在阅读源码时,配合 trace_printk 或 ftrace 跟踪实际分配过程中的链表变化,以加深对算法细节的理解。