Linux内存管理子系统源码分析:alloc_pages物理页分配

第8章:alloc_pages()——一个物理页到底是怎么从Linux中分配出来的?

上一章建立了Buddy System的静态模型:zone->free_area[]按order和迁移类型保存空闲物理块,大块可以拆分,小块释放后可以合并。

这一章把模型放进真实调用链。我们的目标不是只记住“alloc_pages()会从Buddy取页”,而是回答:内核怎样确定可用Zone、怎样保护水位、为什么先查PCP、什么时候进入Buddy,以及快速路径失败后为什么还要进入慢速路径。

0. 源码基线与阅读边界

本文以Linux v6.1正式版本为基线:

1
2
tag:    v6.1
commit: 830b3c68c1fb1e9176028d02ef86f3cf76aa2476

主要源码文件:

1
2
3
4
5
include/linux/gfp.h
include/linux/gfp_types.h
include/linux/mmzone.h
mm/internal.h
mm/page_alloc.c

本文聚焦普通伙伴分配器的快速路径。回收、内存规整、OOM和NUMA内存策略只解释它们与主链的接口,不在本章展开全部细节。

8.1 本章要解决的核心问题

先看最普通的调用:

1
struct page *page = alloc_pages(GFP_KERNEL, 0);

表面只有两个参数,背后却必须完成一系列决策:

1
2
3
4
5
6
7
8
GFP允许在哪些Zone分配?
当前上下文能否睡眠和回收?
order代表多少连续页?
按什么顺序扫描Node与Zone?
Zone剩余页是否高于安全水位?
从PCP还是Buddy取页?
没有恰好大小的块时怎样拆分?
快速路径失败以后是否回收、规整或重试?

因此,alloc_pages()不是一个单纯的“从链表摘节点”函数,而是Linux物理页分配策略的入口。

8.2 整体架构图

先建立全景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
调用者
|
| alloc_pages(gfp, order)
v
__alloc_pages(gfp, order, preferred_nid, nodemask)
|
+-- prepare_alloc_pages()
| +-- gfp_zone() -> highest_zoneidx
| +-- node_zonelist() -> zonelist
| +-- gfp_migratetype() -> migratetype
| `-- preferred_zoneref -> 扫描起点
|
+-- get_page_from_freelist() 快速路径
| |
| +-- 按zonelist遍历Zone
| +-- zone_watermark_fast()
| `-- rmqueue()
| +-- PCP允许的order -> rmqueue_pcplist()
| | `-- 空时rmqueue_bulk()补货
| `-- 其他情况 -> rmqueue_buddy()
| `-- __rmqueue_smallest()
| `-- expand()
|
+-- prep_new_page()
`-- 成功返回struct page *

快速路径失败
`-- __alloc_pages_slowpath()
+-- 唤醒kswapd
+-- direct reclaim
+-- compaction
+-- retry / OOM策略
`-- 成功或返回NULL

阅读时始终把握三层:

1
2
3
策略层:GFP、Zone、zonelist、水位、回收权限
缓存层:PCP,降低多CPU争用
底层:Buddy,维护真正的空闲连续物理块

8.3 alloc_pages()到底是什么

alloc_pages()申请的是2^order个物理连续页面,成功时返回这段连续页面中首页对应的struct page *,失败返回NULL。

1
struct page *page = alloc_pages(GFP_KERNEL, order);

它不直接返回:

1
2
3
物理地址
虚拟地址
任意字节大小的内存

需要时可继续转换:

1
2
3
unsigned long pfn = page_to_pfn(page);
phys_addr_t phys = page_to_phys(page);
void *addr = page_address(page); /* 仅在页面具有直接映射等条件下适用 */

kmalloc()、页缓存、匿名页、页表和许多驱动缓冲区,最终都可能成为页分配器的上层用户,但它们不一定直接调用这个最简单的接口。

8.4 order再次复习

order描述请求的连续页数:

1
2
页数 = 2^order
字节数 = PAGE_SIZE << order

假设PAGE_SIZE = 4 KiB:

order 连续页数 大小
0 1 4 KiB
1 2 8 KiB
2 4 16 KiB
3 8 32 KiB
4 16 64 KiB
9 512 2 MiB

在默认MAX_ORDER = 11的配置中,有效order通常为0~10;架构可以通过配置改变该值。v6.1的__alloc_pages()首先检查:

1
2
if (WARN_ON_ONCE_GFP(order >= MAX_ORDER, gfp))
return NULL;

关键不是“总共需要多少页”,而是“这些页是否必须连续”。申请order-4不是找16个零散页面,而是找一段对齐、连续的16页物理块。

8.5 为什么需要GFP Flags

内核分配内存时所处环境并不相同:

1
2
3
4
5
6
普通进程上下文:可以睡眠,可以回收,可以启动I/O
持有某些锁:可能不能进入文件系统或块I/O
中断/原子上下文:不能睡眠,不能直接回收
DMA设备:只能访问某个物理地址范围
用户页:可能允许放到HighMem或Movable区域
关键路径:可能需要动用保留内存

只传一个大小无法表达这些约束。因此gfp_t本质上是一组分配策略位:

1
GFP = Get Free Pages

它在告诉分配器:

1
2
3
4
5
我能去哪里找?
我能为等待内存做什么?
我是否允许睡眠?
失败时应多努力?
拿到页后是否需要清零、复合页元数据或memcg记账?

8.6 GFP Flags的理解方式

不要把GFP只背成“能睡/不能睡”。更好的方法是按职责分组。

8.6.1 Zone与放置约束

1
2
3
4
5
6
__GFP_DMA       需要最低地址Zone
__GFP_DMA32 需要32位可寻址范围
__GFP_HIGHMEM 允许HighMem
__GFP_MOVABLE 页面可迁移,同时参与Zone选择
__GFP_THISNODE 只在指定Node尝试,不做通常的回退
__GFP_HARDWALL 强制遵守cpuset内存节点限制

其中Zone修饰位会被gfp_zone()解释成highest_zoneidx。

8.6.2 回收能力

1
2
3
4
5
__GFP_DIRECT_RECLAIM  调用者可进入直接回收并睡眠
__GFP_KSWAPD_RECLAIM 可唤醒kswapd后台回收
__GFP_RECLAIM 上面两位的组合
__GFP_IO 回收路径允许启动物理I/O
__GFP_FS 回收路径允许进入文件系统

8.6.3 优先级与保留内存

1
2
3
4
__GFP_HIGH       高优先级,可使用更低水位
__GFP_ATOMIC 调用者不能回收/睡眠,并属于高优先级请求
__GFP_MEMALLOC 允许访问更多紧急储备,必须极其谨慎
__GFP_NOMEMALLOC 明确禁止访问紧急储备

8.6.4 失败与重试策略

1
2
3
4
__GFP_NORETRY        只作轻量尝试,压力大时容易失败
__GFP_RETRY_MAYFAIL 比默认更努力,但最终仍可失败
__GFP_NOFAIL 必须无限重试,使用范围非常受限
__GFP_NOWARN 失败时不打印常规分配警告

8.6.5 返回页面的处理

1
2
3
4
__GFP_ZERO      返回前清零
__GFP_COMP 为高阶分配建立compound page元数据
__GFP_ACCOUNT 计入kmemcg
__GFP_WRITE 调用者计划把页弄脏,影响节点间脏页分布策略

同一个GFP值会同时影响Zone选择、水位、回收、迁移类型和页面初始化。

8.7 常见GFP组合

v6.1定义了常见组合,调用者通常应从它们开始,而不是手工拼所有位。

8.7.1 GFP_KERNEL

1
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS)

典型语义:普通内核上下文,可睡眠、可直接回收、可启动I/O、可进入文件系统。它不是“内核专属物理区域”,而是一套上下文和回收权限。

8.7.2 GFP_ATOMIC

1
#define GFP_ATOMIC (__GFP_HIGH | __GFP_ATOMIC | __GFP_KSWAPD_RECLAIM)

它不能通过直接回收让当前调用者睡眠,但可以唤醒kswapd,并以较高优先级访问一部分储备。因此它延迟更可控,但成功率通常低于允许直接回收的分配。

8.7.3 GFP_NOWAIT、GFP_NOIO与GFP_NOFS

1
2
3
#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO (__GFP_RECLAIM)
#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO)

区别是:

1
2
3
GFP_NOWAIT:不直接回收,不启动I/O,不进入文件系统
GFP_NOIO:可直接回收,但回收不能启动物理I/O
GFP_NOFS:可直接回收、可I/O,但不能递归进入文件系统

8.7.4 GFP_HIGHUSER_MOVABLE

用于通常可迁移的用户页面,允许更宽的Zone范围,并携带__GFP_MOVABLE。它和GFP_KERNEL在放置目标、可迁移性上都不同。

8.8 GFP不只是“能不能睡”

下面两次请求即使order相同,也可能走向不同结果:

1
2
alloc_pages(GFP_KERNEL, 0);
alloc_pages(GFP_DMA32, 0);

第一种可在普通可用Zone中寻找并进入回收;第二种强调地址范围,本身并未携带__GFP_RECLAIM。再比如:

1
alloc_pages(GFP_KERNEL | __GFP_ZERO, 0);

Zone范围基本不变,但返回前需要清零。由此可见GFP同时回答:

1
2
3
4
Where:去哪里分配
How:允许怎样争取内存
How hard:失败前努力到什么程度
Post action:返回前如何处理页面

8.9 alloc_pages()入口

alloc_pages()是头文件中的便捷入口。NUMA配置下,它会结合当前任务的内存策略调用alloc_pages_current();非NUMA配置下则可直接基于当前Node进入alloc_pages_node()。最终主线汇聚到:

1
2
3
struct page *__alloc_pages(gfp_t gfp, unsigned int order,
int preferred_nid,
nodemask_t *nodemask);

因此分析时不要停在内联包装层,核心入口是mm/page_alloc.c中的__alloc_pages()。

8.10 第一条源码主链

先只看骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
alloc_pages()
|
v
alloc_pages_current() / alloc_pages_node()
|
v
__alloc_pages()
|
+-- prepare_alloc_pages()
|
+-- get_page_from_freelist()
| `-- 成功:快速路径
|
`-- __alloc_pages_slowpath()
`-- 快速路径失败后补救

这一章最重要的边界是:

1
2
get_page_from_freelist() = 尝试从当前可用资源快速拿页
__alloc_pages_slowpath() = 资源不足时改变系统状态再重试

8.11 __alloc_pages()做什么

抽离v6.1核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct page *__alloc_pages(gfp_t gfp, unsigned int order,
int preferred_nid, nodemask_t *nodemask)
{
struct page *page;
unsigned int alloc_flags = ALLOC_WMARK_LOW;
gfp_t alloc_gfp;
struct alloc_context ac = { };

if (WARN_ON_ONCE_GFP(order >= MAX_ORDER, gfp))
return NULL;

gfp &= gfp_allowed_mask;
gfp = current_gfp_context(gfp);
alloc_gfp = gfp;

if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask,
&ac, &alloc_gfp, &alloc_flags))
return NULL;

alloc_flags |= alloc_flags_nofragment(
ac.preferred_zoneref->zone, gfp);

page = get_page_from_freelist(alloc_gfp, order,
alloc_flags, &ac);
if (likely(page))
goto out;

alloc_gfp = gfp;
ac.spread_dirty_pages = false;
page = __alloc_pages_slowpath(alloc_gfp, order, &ac);
out:
...
return page;
}

它做六件核心事情:

1
2
3
4
5
6
1. 拒绝越界order
2. 应用系统和当前任务的GFP约束
3. 构造alloc_context
4. 以LOW水位开始快速扫描
5. 快速路径失败则进入slowpath
6. 完成记账、trace并返回page

ALLOC_WMARK_LOW值得注意:普通第一次尝试不是“只要有空闲页就拿”,而是要求Zone满足低水位约束。

8.12 什么是Allocation Context

mm/internal.h中的struct alloc_context把一次分配过程中反复使用的条件集中起来:

1
2
3
4
5
6
7
8
struct alloc_context {
struct zonelist *zonelist;
nodemask_t *nodemask;
struct zoneref *preferred_zoneref;
int migratetype;
enum zone_type highest_zoneidx;
bool spread_dirty_pages;
};

字段含义:

字段 作用
zonelist 候选Zone的有序集合
nodemask 允许使用的NUMA Node集合
preferred_zoneref 本次扫描首选起点
migratetype 希望从哪类迁移链表取页
highest_zoneidx 允许使用的最高Zone类型
spread_dirty_pages 是否按节点脏页限制分散写页

它不是空闲页容器,而是一张本次分配的“搜索说明书”。

8.13 prepare_alloc_pages()在准备什么

核心初始化:

1
2
3
4
5
6
7
8
ac->highest_zoneidx = gfp_zone(gfp_mask);
ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
ac->nodemask = nodemask;
ac->migratetype = gfp_migratetype(gfp_mask);
...
ac->spread_dirty_pages = gfp_mask & __GFP_WRITE;
ac->preferred_zoneref = first_zones_zonelist(
ac->zonelist, ac->highest_zoneidx, ac->nodemask);

可以把它翻译为:

1
2
3
4
GFP + preferred_nid + nodemask
|
v
确定最高Zone、扫描顺序、允许节点、迁移类型和首选Zone

它还会处理cpuset限制、故障注入和CMA相关分配标志。准备失败时,分配可在尚未扫描Zone前直接返回NULL。

8.14 Zonelist是什么

zonelist不是所有Zone的无序集合,而是提前建立的、有优先顺序的候选列表。它解决两个问题:

1
2
优先从哪里分配?
首选Zone不行时按什么顺序回退?

在NUMA系统中,通常优先本地Node,再考虑距离更远的Node;在一个Node内部,还受highest_zoneidx限制和Zone回退关系影响。

8.15 Zonelist的概念模型

假设系统有两个Node,每个Node有DMA32和Normal:

1
2
3
4
5
6
7
8
9
preferred_nid = Node0
highest_zoneidx = ZONE_NORMAL

zonelist概念顺序:

Node0 / Normal <- 首选
Node0 / DMA32 <- 本地低Zone回退
Node1 / Normal <- 远端回退
Node1 / DMA32

实际顺序受架构、NUMA距离、配置和内存策略影响,不能把上图当作所有机器的固定结果。正确理解是:zonelist编码“候选Zone及其优先级”,迭代器再结合highest_zoneidx和nodemask过滤。

8.16 highest_zoneidx是什么

它表示本次请求可使用的最高Zone索引:

1
ac->highest_zoneidx = gfp_zone(gfp_mask);

例如概念上:

1
2
3
4
要求DMA      -> 最高只能到ZONE_DMA
要求DMA32 -> 最高只能到ZONE_DMA32
普通低端页 -> 可到ZONE_NORMAL
允许HighMem -> 可到ZONE_HIGHMEM(若架构存在)

“最高”不意味着只使用该Zone。迭代时仍可回退到更低Zone,但低Zone会受到lowmem_reserve[]保护,避免普通请求过早耗尽珍贵的低地址内存。

8.17 GFP怎样变成Zone限制

完整思路是:

1
2
3
4
5
6
7
8
9
10
11
12
gfp_mask
|
+-- Zone modifier bits
| __GFP_DMA / DMA32 / HIGHMEM / MOVABLE
v
gfp_zone(gfp_mask)
|
v
highest_zoneidx
|
v
zonelist迭代时跳过更高、不可用的Zone

因此,GFP的放置位不是到了Buddy层才处理。它们在构造搜索空间时就已经决定“哪些Zone根本没有资格参与”。

8.18 get_page_from_freelist()是整章核心函数

它的职责可以概括为:

按本次分配上下文扫描候选Zone;只有Zone符合策略且通过水位检查,才调用rmqueue()真正取页。

v6.1骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 = ac->preferred_zoneref;
struct zone *zone;

for_next_zone_zonelist_nodemask(zone, z,
ac->highest_zoneidx, ac->nodemask) {
unsigned long mark;

if (cpuset不允许该zone)
continue;

...

mark = wmark_pages(zone,
alloc_flags & ALLOC_WMARK_MASK);
if (!zone_watermark_fast(zone, order, mark,
ac->highest_zoneidx, alloc_flags, gfp_mask))
continue或尝试节点回收;

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);
return page;
}
}

return NULL;
}

这段代码把“选择Zone”和“从Zone取页”明确分开。

8.19 get_page_from_freelist()整体模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
preferred_zoneref
|
v
+--------------------+
| 遍历候选Zone |
+--------------------+
|
+-- cpuset允许? -------- 否 -> 下一个Zone
|
+-- nodemask允许? ------- 否 -> 下一个Zone
|
+-- 脏页分布允许? ------- 否 -> 下一个Zone
|
+-- watermark满足? ------ 否 -> 下一个Zone/节点回收
|
+-- rmqueue取到页? ------ 否 -> 下一个Zone
|
`-- prep_new_page() -> 返回

全部候选失败 -> NULL -> slowpath

所以fast path并不等于只检查一次;它可能快速遍历多个Zone,但不会主动做完整的直接回收与规整。

8.20 为什么不能看到free page就直接拿

因为NR_FREE_PAGES > 0并不意味着这些页面都能被当前请求安全使用:

1
2
3
4
5
6
可能属于不允许的Zone
可能位于cpuset不允许的Node
可能是CMA或特定迁移类型页面
可能需要留给DMA等低地址请求
可能必须保留给原子/内存回收关键路径
可能是零散order-0页,无法满足高阶连续请求

如果普通分配把所有空闲页吃光,系统可能连执行回收、写回和网络收包所需的内存都拿不到,从而失去恢复能力。

8.21 Watermark

每个Zone维护三档水位:

1
2
3
WMARK_MIN
WMARK_LOW
WMARK_HIGH

概念图:

1
2
3
4
5
6
7
8
9
10
11
空闲页多

HIGH -------- 后台回收的目标附近
|
LOW -------- 普通快速分配重点检查;低于时通常唤醒kswapd
|
MIN -------- 更紧急的保底线
|
0

空闲页少

这些线不是三个独立内存池,而是对同一个Zone空闲状态的阈值。

8.22 为什么分配前检查Watermark

水位的核心目的不是“保证这一次能分配”,而是确保分配后系统仍有前进空间:

1
2
3
4
5
保留回收路径所需内存
保护低地址Zone
给突发和原子请求留余量
让kswapd在完全耗尽前开始工作
避免每次申请都等到最后一页才处理压力

普通__alloc_pages()快速路径初始使用ALLOC_WMARK_LOW。高优先级和slowpath可能调整alloc_flags,从而使用更低水位或紧急储备,但那不是普通分配的默认权限。

8.23 zone_watermark_fast()

调用形式:

1
2
3
4
5
6
7
mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);

if (!zone_watermark_fast(zone, order, mark,
ac->highest_zoneidx,
alloc_flags, gfp_mask)) {
...
}

它不仅比较:

1
free_pages >= mark

还要考虑:

1
2
3
4
5
当前order需要的页数
不可用于普通分配的空闲页
为更高Zone请求保护的lowmem_reserve
高阶空闲块是否真实存在
alloc_flags是否允许使用储备

order-0有优化过的快速判断;条件不够明确时会进入更完整的__zone_watermark_ok()。因此水位判断兼顾速度与正确性。

8.24 为什么order会影响Watermark与成功率

假设Zone有1000个空闲页,但全是分散的order-0页:

1
2
3
order-0:很可能成功
order-3:需要连续8页,可能失败
order-9:需要连续512页,更可能失败

高阶检查不能只减去2^order后比较水位,还要判断空闲链表中是否存在可提供该请求的高阶块。于是:

1
2
总空闲量解决“容量”问题
free_area高阶分布解决“连续性”问题

这就是高阶分配对外部碎片敏感的根本原因。

8.25 通过Watermark后进入rmqueue()

rmqueue()是真正从某个Zone拿页的统一入口:

1
2
page = rmqueue(preferred_zone, zone, order,
gfp_mask, alloc_flags, migratetype);

输入已经非常具体:

1
2
3
4
5
6
preferred_zone:用于NUMA统计等
zone:真正尝试分配的Zone
order:连续块大小
gfp_flags:调用者语义
alloc_flags:分配器内部权限
migratetype:首选迁移类型

接下来它选择PCP或Buddy。

8.26 为什么rmqueue()还要分路径

如果每个小页请求都直接修改zone->free_area[]:

1
2
3
4
每个CPU都争用zone->lock
高频order-0分配导致缓存行抖动
多核扩展性变差
中断关闭和临界区开销增加

因此Linux在Buddy上方增加Per-CPU Pageset:

1
2
3
常见小请求 -> 尽量从本CPU的PCP取
PCP不足 -> 成批从Buddy补充
不适合PCP -> 直接进入Buddy

8.27 order-0为什么优先走PCP

order-0是系统最常见的页请求,例如匿名页、页缓存和许多内核对象的底层页。给它建立CPU本地缓存能显著减少全局Zone锁竞争。

但这里要纠正一个常见简化:

Linux v6.1的PCP不只缓存order-0。pcp_allowed_order()默认还允许不超过PAGE_ALLOC_COSTLY_ORDER的低阶请求,通常是order 0~3;某些THP配置还可能允许特定order。

所以准确说法是:order-0最典型、最高频;符合pcp_allowed_order()的低成本高阶页也可能走PCP。

8.28 PCP模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CPU0                      CPU1
per_cpu_pages per_cpu_pages
+-----------+ +-----------+
| lists[] | | lists[] |
| count | | count |
| high | | high |
| batch | | batch |
+-----+-----+ +-----+-----+
| |
+-----------+-------------+
|
Zone Buddy
zone->free_area[]
|
zone->lock

PCP并不创造额外物理页。页面从Buddy批量移入PCP后,仍属于该Zone的空闲资源,只是放在CPU局部链表中以加速分配与释放。

8.29 rmqueue_pcplist()

v6.1关键流程:

1
2
3
4
5
6
7
8
9
10
pcp = pcp_spin_trylock_irqsave(zone->per_cpu_pageset, flags);
if (!pcp)
return NULL;

list = &pcp->lists[order_to_pindex(migratetype, order)];
page = __rmqueue_pcplist(zone, order, migratetype,
alloc_flags, pcp, list);

pcp_spin_unlock_irqrestore(pcp, flags);
return page;

内部取页:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (list_empty(list)) {
batch = READ_ONCE(pcp->batch);
if (batch > 1)
batch = max(batch >> order, 2);

alloced = rmqueue_bulk(zone, order, batch, list,
migratetype, alloc_flags);
pcp->count += alloced << order;
}

page = list_first_entry(list, struct page, pcp_list);
list_del(&page->pcp_list);
pcp->count -= 1 << order;

链表不空时,本CPU只需从局部链表摘一项;链表空时才批量触碰Buddy。

8.30 为什么PCP要批量refill

如果PCP每缺一页就向Buddy申请一页:

1
2
3
请求1 -> zone锁 -> 取1页
请求2 -> zone锁 -> 取1页
请求3 -> zone锁 -> 取1页

批量补货后:

1
2
第一次 -> zone锁 -> 一次取batch个块
随后多次 -> 本CPU PCP直接取

它用少量局部缓存换取:

1
2
3
4
更少zone->lock竞争
更少共享缓存行来回迁移
更短的平均分配延迟
更好的多核扩展性

代价是部分空闲页暂存在PCP中,统计和高阶连续块可用性会受到一定影响,因此内核也需要drain机制把PCP页归还Buddy。

8.31 高阶页为什么主要走Buddy

大块连续内存不适合在每个CPU上大量缓存:

1
2
3
4
占用太多页
容易加剧碎片
命中率通常低
会让Buddy难以看到和合并连续块

在v6.1中,准确边界是:

1
2
3
4
5
if (likely(pcp_allowed_order(order)))
尝试rmqueue_pcplist();

失败或order不允许PCP
-> rmqueue_buddy();

因此不能把代码机械概括成“order-0走PCP,所有order>0走Buddy”。默认order 1~3也可能命中PCP;更高阶请求则通常直接依赖Buddy。

8.32 Buddy路径

rmqueue_buddy()持有zone->lock操作共享伙伴链表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spin_lock_irqsave(&zone->lock, flags);

if (order > 0 && (alloc_flags & ALLOC_HARDER))
page = __rmqueue_smallest(zone, order,
MIGRATE_HIGHATOMIC);

if (!page)
page = __rmqueue(zone, order, migratetype, alloc_flags);

if (!page) {
spin_unlock_irqrestore(&zone->lock, flags);
return NULL;
}

__mod_zone_freepage_state(zone, -(1 << order), ...);
spin_unlock_irqrestore(&zone->lock, flags);

__rmqueue()还会处理迁移类型回退、CMA等策略。Buddy路径比PCP更重,但它掌握完整的free_area[],可以查找、拆分真正的连续物理块。

8.33 __rmqueue_smallest()的思想

大纲里常写作rmqueue_smallest(),但Linux v6.1真实函数名是__rmqueue_smallest():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (current_order = order;
current_order < MAX_ORDER;
++current_order) {

area = &zone->free_area[current_order];
page = get_page_from_free_area(area, migratetype);
if (!page)
continue;

del_page_from_free_list(page, zone, current_order);
expand(zone, page, order, current_order, migratetype);
return page;
}

return NULL;

翻译成算法:

1
2
3
4
5
6
7
8
9
从目标order开始向更高order查找
|
+-- 找到最小可用块
| |
| +-- 从free_list删除
| +-- 若过大则expand()逐级拆分
| `-- 返回目标大小块
|
`-- 扫到MAX_ORDER仍没有 -> 失败

例如申请order-1,只在order-3找到块:

1
2
3
4
5
6
7
8
order-3:8页
split
+-- order-2:4页留作继续拆
`-- order-2:4页放回free_area[2]

继续拆
+-- order-1:2页返回
`-- order-1:2页放回free_area[1]

8.34 Migratetype也参与分配

同一order的free_area不是只有一条链表:

1
2
3
4
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};

常见迁移类型:

1
2
3
4
5
MIGRATE_UNMOVABLE
MIGRATE_MOVABLE
MIGRATE_RECLAIMABLE
MIGRATE_HIGHATOMIC
MIGRATE_CMA

分配器优先从匹配链表获取,必要时按回退规则借用其他类型。这样做不是为了本次分配速度,而是尽量把生命周期和可迁移性相近的页面放在一起,降低长期外部碎片。

8.35 拿到page后为什么还不能直接返回

Buddy或PCP交出的页面只是“从空闲管理结构中取出”。在交给调用者之前,内核还要:

1
2
3
4
5
6
7
检查页面状态是否合法
执行调试、安全和内存初始化钩子
按__GFP_ZERO清零
按__GFP_COMP建立compound page
记录page owner等信息
设置或清除pfmemalloc标志
执行统计和trace

这一步把“空闲页”转换为“可以安全交给调用者的已分配页”。

8.36 prep_new_page()

在v6.1中,它的核心职责可以抽象为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static inline void prep_new_page(struct page *page,
unsigned int order,
gfp_t gfp_flags,
unsigned int alloc_flags)
{
post_alloc_hook(page, order, gfp_flags);

if (order && (gfp_flags & __GFP_COMP))
prep_compound_page(page, order);

if (alloc_flags & ALLOC_NO_WATERMARKS)
set_page_pfmemalloc(page);
else
clear_page_pfmemalloc(page);
}

post_alloc_hook()内部还承载页面清零、KASAN/KMSAN、page owner、初始化策略等工作。理解重点是:

1
2
rmqueue():从空闲结构取得所有权
prep_new_page():把页整理成调用者可接收状态

8.37 最终返回什么

成功结果始终是首页的struct page *:

1
2
order-0 -> 1页的struct page *
order-3 -> 连续8页中第1页的struct page *

后续页面可用指针算术访问:

1
2
3
4
5
6
struct page *p = alloc_pages(GFP_KERNEL, 3);
if (p) {
struct page *fifth = p + 4;
...
__free_pages(p, 3);
}

释放时必须使用与分配匹配的order,否则会破坏伙伴系统元数据。

8.38 Fast Path完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
alloc_pages(gfp, order)
|
v
__alloc_pages()
|
+-- order合法?
+-- 限制/规范化gfp
|
v
prepare_alloc_pages()
|
+-- highest_zoneidx
+-- zonelist
+-- nodemask
+-- migratetype
`-- preferred_zoneref
|
v
get_page_from_freelist()
|
v
按zonelist遍历Zone
|
+-- 策略允许?
+-- watermark通过?
|
v
rmqueue()
|
+-- PCP允许 -> rmqueue_pcplist()
| `-- 必要时rmqueue_bulk()
|
`-- 否则/未命中 -> rmqueue_buddy()
`-- __rmqueue_smallest()
`-- expand()
|
v
prep_new_page()
|
v
trace / accounting
|
v
返回struct page *

8.39 如果Fast Path失败怎么办

快速路径返回NULL只表示:

在当前资源状态、当前水位和当前候选Zone中,不能直接取到满足条件的页。

它不一定表示系统彻底没内存。__alloc_pages()接着调用:

1
page = __alloc_pages_slowpath(alloc_gfp, order, &ac);

慢速路径会尝试改变内存状态,再回到分配动作。

8.40 Slow Path整体预览

概念流程:

1
2
3
4
5
6
7
8
9
10
fast path失败
|
+-- 根据GFP决定是否唤醒kswapd
+-- 调整alloc_flags和水位权限
+-- 再尝试freelist
+-- direct reclaim(若允许)
+-- compaction(尤其高阶请求)
+-- 判断是否重试
+-- 必要时进入OOM相关策略
`-- 成功返回page / 最终返回NULL

不是每个请求都有上述全部权限:

1
2
3
4
5
GFP_ATOMIC:不能直接回收睡眠
GFP_NOWAIT:不进入直接回收
__GFP_NORETRY:很快放弃
__GFP_NOFAIL:要求持续努力
高阶请求:更依赖compaction,但也更可能失败

下一专题可单独分析slowpath;本章先守住快速主链。

8.41 Fast Path与Slow Path的区别

维度 Fast Path Slow Path
核心目标 从现有可用空闲结构快速取页 创造条件后重试
主要函数 get_page_from_freelist() __alloc_pages_slowpath()
是否回收 通常不做完整直接回收 按GFP可能直接回收
是否规整 否 高阶请求可能规整
延迟 较短、较可预测 可能很长
失败含义 当前不能直接满足 策略允许的补救也失败

8.42 完整调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
alloc_pages()
|
v
alloc_pages_current() / alloc_pages_node()
|
v
__alloc_pages()
|
+-- prepare_alloc_pages()
| +-- gfp_zone()
| +-- node_zonelist()
| +-- gfp_migratetype()
| `-- first_zones_zonelist()
|
+-- get_page_from_freelist()
| +-- for_next_zone_zonelist_nodemask()
| +-- zone_watermark_fast()
| `-- rmqueue()
| +-- rmqueue_pcplist()
| | `-- __rmqueue_pcplist()
| | `-- rmqueue_bulk()
| | `-- Buddy
| `-- rmqueue_buddy()
| `-- __rmqueue()
| `-- __rmqueue_smallest()
| `-- expand()
|
+-- prep_new_page()
| `-- post_alloc_hook()
|
`-- 失败:__alloc_pages_slowpath()

8.43 一个GFP_KERNEL order-0示例

1
2
3
4
5
6
7
8
9
struct page *page;

page = alloc_pages(GFP_KERNEL, 0);
if (!page)
return -ENOMEM;

/* 使用page */

__free_pages(page, 0);

概念执行:

1
2
3
4
5
6
7
允许直接回收/I/O/FS
-> highest_zoneidx通常为ZONE_NORMAL
-> 从首选Node的合适Zone开始
-> LOW水位检查
-> 优先命中本CPU PCP order-0链表
-> prep_new_page
-> 返回

如果PCP为空,会通过rmqueue_bulk()成批从Buddy补充,然后取出一页。

8.44 一个GFP_DMA32示例

1
struct page *page = alloc_pages(GFP_KERNEL | GFP_DMA32, 0);

核心变化不是order,而是搜索空间:

1
2
3
4
gfp_zone()
-> highest_zoneidx = ZONE_DMA32(架构存在时)
-> ZONE_NORMAL不再是合法候选
-> 只能在DMA32及允许回退的更低Zone寻找

工程上,设备DMA缓冲区通常还应优先使用DMA API,例如dma_alloc_coherent()或dma_map_*();直接使用GFP_DMA32不能代替设备地址能力、IOMMU和缓存一致性处理。

8.45 一个高阶分配示例

1
struct page *page = alloc_pages(GFP_KERNEL, 4);

在4 KiB页系统中,它要求连续64 KiB:

1
2
order = 4
页数 = 16

默认情况下order-4超过PAGE_ALLOC_COSTLY_ORDER,不会使用普通低阶PCP路径,主要进入Buddy:

1
2
3
4
5
6
watermark与连续块检查
-> rmqueue_buddy()
-> 从free_area[4]开始向上查找
-> 找到更大块则拆分
-> 没找到则fast path失败
-> slowpath可能尝试回收和规整

8.46 为什么高阶分配风险高

高阶分配同时要求:

1
2
3
4
足够总量
足够连续性
满足Zone限制
满足迁移类型和水位策略

系统运行越久,页面被不同生命周期对象占用,外部碎片越可能增加。即使回收能释放许多页面,也未必立即形成连续大块;这时需要内存规整移动可迁移页,但规整又会带来延迟和CPU开销。

因此长期运行系统中,驱动不应把大额高阶分配当作必然成功的常规操作。可考虑:

1
2
3
4
5
6
启动早期预留
CMA
分散页 + scatter-gather
vmalloc(只要求虚拟连续时)
内存池
拆小请求

8.47 为什么还有free memory也可能失败

看到free命令还有空闲内存,却出现页分配失败,并不矛盾。常见原因:

1
2
3
4
5
6
7
8
1. Zone不匹配:空闲页在Normal,请求必须DMA32
2. NUMA/cpuset限制:空闲页在不允许的Node
3. 水位保护:剩余页不能被当前优先级使用
4. lowmem_reserve:低Zone要保护给受限请求
5. 外部碎片:总量够,但没有目标order连续块
6. 迁移类型/CMA限制:可见空闲页不适合当前请求
7. GFP限制:当前上下文不能睡眠、回收或I/O
8. memcg限制:系统有内存,但该控制组已达上限

一句话:

分配成功取决于“可用、合法、连续、可访问”的页面,而不是一个全局free数字。

8.48 page allocation failure怎么分析

典型日志:

1
page allocation failure: order:4, mode:0x...

建议顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
第一步:看order
order越高,先怀疑连续性和碎片

第二步:解读GFP/mode
能否回收?要求哪个Zone?是否atomic?

第三步:看调用栈
谁申请、在什么上下文申请、是否持锁或中断

第四步:看各Zone水位与free_area
/proc/zoneinfo
/proc/buddyinfo

第五步:看NUMA、cpuset、memcg和CMA限制

第六步:判断是容量不足、碎片还是策略不允许

不要只看到order:4就认定“内存泄漏”,也不要只看到系统还有缓存就认定“内核误报”。

8.49 实验1:观察Zone和Buddy

1
2
cat /proc/zoneinfo
cat /proc/buddyinfo

/proc/buddyinfo示例列:

1
Node 0, zone Normal  1000 500 120 30 8 2 0 0 0 0 0

每列表示对应order的空闲块数量。第n列的页数贡献是:

1
blocks[n] * 2^n

重点观察:

1
2
3
低阶列很多而高阶列接近0 -> 可能存在外部碎片
Zone的min/low/high -> 水位保护规模
lowmem_reserve[] -> 低Zone保护关系

8.50 实验2:申请一个order-0 page

可写一个最小内核模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gfp.h>

static int __init alloc_demo_init(void)
{
struct page *page;

page = alloc_pages(GFP_KERNEL | __GFP_ZERO, 0);
if (!page)
return -ENOMEM;

pr_info("alloc_demo: page=%p pfn=%lu\n",
page, page_to_pfn(page));
__free_pages(page, 0);
return 0;
}

static void __exit alloc_demo_exit(void) { }

module_init(alloc_demo_init);
module_exit(alloc_demo_exit);
MODULE_LICENSE("GPL");

验证目标不是证明“函数能用”,而是确认:

1
2
3
返回值是struct page *
可以转换为PFN
释放时order必须匹配

8.51 实验3:比较不同order

让模块依次申请order 0、1、2、3、4、5,并记录成功率和耗时。每次成功后立即释放,避免实验本身耗尽内存。

1
2
3
4
5
6
7
for (order = 0; order <= 5; order++) {
page = alloc_pages(GFP_KERNEL | __GFP_NOWARN, order);
pr_info("order=%u result=%s\n",
order, page ? "success" : "failed");
if (page)
__free_pages(page, order);
}

注意:一次结果不能代表系统整体能力。要结合/proc/buddyinfo,并在不同负载、不同运行时长下重复观察。

8.52 实验4:观察page allocation tracepoint

先确认事件:

1
ls /sys/kernel/tracing/events/kmem/

常见可观察事件包括:

1
2
3
4
mm_page_alloc
mm_page_alloc_zone_locked
mm_page_pcpu_drain
mm_page_free

使用tracefs:

1
2
3
4
5
6
7
8
9
10
cd /sys/kernel/tracing
echo 0 > tracing_on
echo > trace
echo 1 > events/kmem/mm_page_alloc/enable
echo 1 > tracing_on

# 运行测试模块或负载

echo 0 > tracing_on
cat trace

也可用perf:

1
2
sudo perf record -e kmem:mm_page_alloc -a -- sleep 5
sudo perf script

实验前后都要恢复开关,生产系统上注意trace开销和日志量。

8.53 性能与工程意义

页分配器同时优化三个相互冲突的目标:

1
2
3
快:常见请求尽量命中PCP
稳:水位和储备保证系统前进能力
久:迁移类型和规整机制降低长期碎片

任何一个设计单独看都可能显得复杂;放在这三个目标中就容易理解:

1
2
3
4
5
PCP解决并发性能
Watermark解决系统稳定性
Migratetype解决长期连续内存质量
Buddy解决不同大小连续物理块管理
Slowpath解决压力下的恢复

8.54 高阶分配与实时性

高阶请求的延迟波动来源包括:

1
2
3
4
5
6
扫描多个Zone
直接回收
页面写回
内存规整和页面迁移
锁竞争
多次重试

实时或低延迟路径应尽量避免临时高阶分配。常见设计是初始化阶段预分配、使用mempool/CMA、限制最大请求大小,或把物理连续要求改成scatter-gather。

GFP_ATOMIC也不是“实时成功保证”。它只是限制不能睡眠的行为并提供较高优先级;内存不足时仍可失败,所以关键路径仍应准备失败处理或预留池。

8.55 PCP与多核性能

如果没有PCP:

1
2
3
4
CPU0 --+
CPU1 --+--> zone->lock --> free_area[]
CPU2 --+
CPU3 --+

有PCP后:

1
2
3
4
CPU0 -> CPU0 PCP --\
CPU1 -> CPU1 PCP ---+--> 偶尔批量访问Buddy
CPU2 -> CPU2 PCP ---+
CPU3 -> CPU3 PCP --/

这是一种典型内核优化:

把高频、细粒度的全局操作变成CPU局部操作,再用低频批处理与全局结构同步。

同类思想还出现在per-CPU计数器、slab的CPU缓存和网络收发队列中。

8.56 GFP选择本身也是性能设计

GFP选错不只是功能问题,也会改变延迟:

1
2
3
4
5
6
7
8
9
10
11
不必要地使用GFP_ATOMIC
-> 回收能力受限,失败率升高,还可能消耗紧急储备

可睡眠路径使用GFP_KERNEL
-> 成功率高,但压力下可能产生直接回收延迟

错误允许__GFP_FS或__GFP_IO
-> 持锁路径可能递归或死锁

滥用__GFP_NOFAIL
-> 请求可能无限等待,掩盖架构问题

选择GFP前应先问:当前上下文能否睡眠、能否I/O、能否进入文件系统、失败能否处理、需要哪个地址范围。

8.57 常见误区

误区1:alloc_pages()返回虚拟地址

错误。它返回struct page *。是否能通过page_address()获得永久虚拟地址取决于架构和页面类型。

误区2:有足够free内存,高阶分配就一定成功

错误。高阶分配需要物理连续块,总量足够不等于连续性足够。

误区3:GFP_ATOMIC一定成功

错误。它不能睡眠和直接回收,虽然优先级更高、可用部分储备,仍然可能返回NULL。

误区4:PCP只缓存order-0

对旧式教学模型有帮助,但不符合v6.1完整实现。v6.1默认允许低成本order 0~3进入PCP,具体由pcp_allowed_order()决定。

误区5:rmqueue()只操作Buddy

错误。它是统一取页入口,先判断PCP路径,未命中或不允许时才进入rmqueue_buddy()。

误区6:fast path失败就是OOM

错误。它只表示当前条件下不能直接取页,之后可能通过回收、规整、调整权限或重试成功。

8.58 本章最重要的设计思想

8.58.1 先缩小搜索空间,再取页

1
GFP -> highest_zoneidx / zonelist / migratetype

策略先决定“可以去哪”,底层才决定“怎样拿”。

8.58.2 空闲页也分可不可用

Watermark、lowmem reserve、cpuset、CMA和迁移类型共同决定当前请求能否使用某页。

8.58.3 高频路径局部化

PCP把最常见分配从共享Zone锁前移到CPU局部缓存。

8.58.4 快慢路径分离

绝大多数请求保持快速;只有资源紧张时才支付回收、规整和重试成本。

8.58.5 现在成功不能破坏未来恢复能力

水位和储备不是浪费内存,而是在压力下为系统保留自救空间。

8.59 本章核心总图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
                        alloc_pages(gfp, order)
|
v
__alloc_pages()
|
+-------------+-------------+
| |
v v
prepare_alloc_pages() order / GFP约束检查
|
+------------+-------------+
| | |
v v v
highest_zoneidx zonelist migratetype
\ | /
+-----------+-----------+
|
v
get_page_from_freelist()
|
按zonelist遍历Zone
|
+------------+-------------+
| |
v v
策略过滤/cpuset Watermark检查
| |
+------------+-------------+
|
v
rmqueue()
|
+-----------+------------+
| |
v v
pcp_allowed_order() 其他/PCP失败
| |
v v
rmqueue_pcplist() rmqueue_buddy()
| |
PCP有页? v
| \否 __rmqueue()
| \ |
| rmqueue_bulk() v
| \ __rmqueue_smallest()
| \ |
| +------> expand()
| |
+-------------+--------------+
|
v
prep_new_page()
|
v
返回首页struct page *

任何Zone都无法满足
|
v
__alloc_pages_slowpath()
|
回收 / 规整 / 重试 / OOM策略

8.60 博客总结

alloc_pages()的真实含义不是“从Buddy随便取几页”,而是:

1
2
3
4
5
6
7
8
9
1. 用GFP描述分配环境、地址范围和补救权限
2. 用order描述连续物理页数量
3. 构造alloc_context,确定Zone搜索空间和迁移类型
4. 沿zonelist扫描候选Zone
5. 通过Watermark保护系统储备与低地址内存
6. 常见低阶请求优先尝试PCP,降低多核锁竞争
7. Buddy负责查找并拆分真正的连续空闲块
8. prep_new_page完成页面交付前初始化
9. 快速路径失败后,slowpath再回收、规整和重试

最值得记住的主链:

1
2
3
4
5
6
7
8
9
10
11
12
alloc_pages()
-> __alloc_pages()
-> prepare_alloc_pages()
-> get_page_from_freelist()
-> zone_watermark_fast()
-> rmqueue()
-> rmqueue_pcplist()
或 rmqueue_buddy()
-> __rmqueue_smallest()
-> expand()
-> prep_new_page()
-> return struct page *

8.61 学完检查题

  1. alloc_pages(GFP_KERNEL, 3)申请多少个页面?为什么必须连续?
  2. GFP除了“能否睡眠”,还控制哪些维度?
  3. highest_zoneidx和zonelist分别解决什么问题?
  4. 为什么空闲页数大于请求页数,水位检查仍可能失败?
  5. WMARK_MIN/LOW/HIGH是三个内存池吗?
  6. 普通快速路径最初使用哪一级水位?
  7. PCP为什么能提高多核扩展性?它是否只支持order-0?
  8. PCP为空时,为什么通过rmqueue_bulk()批量补货?
  9. rmqueue_buddy()为什么必须持有zone->lock?
  10. __rmqueue_smallest()找不到目标order时怎样处理?
  11. Migratetype对长期外部碎片有什么意义?
  12. rmqueue()拿到页面后,为什么还要调用prep_new_page()?
  13. 为什么系统仍有很多空闲页,高阶分配仍可能失败?
  14. Fast Path失败与OOM有什么区别?
  15. 为什么不能在所有场景都使用GFP_ATOMIC?

如果能脱离源码画出第59节的总图,并解释每一层解决的问题,就已经掌握本章主干。

8.62 下一章

下一章进入反方向:

1
第9章:free_pages()——物理页如何释放并重新回到Buddy?

重点问题将是:

1
2
3
4
5
6
__free_pages()如何判断走PCP还是Buddy?
释放页怎样进入per-CPU链表?
PCP何时批量drain回Zone?
Buddy怎样计算伙伴PFN?
哪些条件满足时才能合并?
释放后的页怎样重新出现在free_area[]?

分配路径强调“选择、保护与拆分”,释放路径则强调“校验、缓存与合并”。两条路径合起来,才是完整的Linux伙伴分配器。


Linux内存管理子系统源码分析:alloc_pages物理页分配
https://cj0510.github.io/2026/10/07/Linux Kernel/Memory Management/Linux内存管理子系统源码分析-第8章-alloc_pages物理页分配/
作者
CJ1018
发布于
2026年10月7日
许可协议
CC BY-NC-SA 4.0