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