4. 内存块分配流程
伙伴系统的内存分配请求从页面分配器的顶层入口 __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:允许分配的节点掩码。
该函数的执行流程分为两个主要阶段:
- 快速路径(fast path):在允许的节点和区域中,尝试直接从伙伴系统分配。如果成功,直接返回页面。
- 慢速路径(slow path):当快速路径失败时,进入更复杂的重试逻辑,包括异步内存回收、压缩(compaction)、OOM 等。
下图展示了 __alloc_pages_nodemask() 的顶层决策流程:
关键观察:大多数分配请求在快速路径中即可满足。慢速路径的进入意味着系统内存压力较大,需要触发回收或压缩操作。
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 选择不同路径的决策树:
4.3 核心拆分器:__rmqueue_smallest()
__rmqueue_smallest() 是伙伴系统分配算法最直接的体现。它从指定迁移类型的伙伴链表中,从请求阶数开始向上搜索,找到第一个非空链表,然后执行块的拆分与摘取。
函数原型:
static inline struct page *__rmqueue_smallest(struct zone *zone,
unsigned int order,
int migratetype);
该函数的执行步骤如下:
- 搜索起始阶:从
order开始,逐阶向上检查zone->free_area[current_order].free_list[migratetype]是否为空。 - 找到非空链表:当在阶
current_order找到可用块时,从链表中摘取该块。 - 拆分块:如果
current_order > order,则将多余的块拆分成更小的伙伴块,并放回对应阶的链表中。 - 返回结果:返回摘取到的页面块的首地址。
下表总结了拆分过程中各阶的变化:
| 步骤 | 当前阶 | 操作 | 剩余块去向 |
|---|---|---|---|
| 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() 必须在持有 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 跟踪实际分配过程中的链表变化,以加深对算法细节的理解。