K11 —— 物理页分配的全路径:alloc_pages 和伙伴系统
金句:每次调用 alloc_pages,都经历了一次从请求到物理页框的完整旅程。
alloc_pages 的函数签名
alloc_pages 是 Linux 分配物理页的核心函数:
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
参数:
- gfp_mask:分配标志(GFP_KERNEL / GFP_ATOMIC 等)
- order:分配 2^order 个页(order=0 → 1页,order=2 → 4页)
返回值:
- 成功:返回指向 struct page 的指针
- 失败:返回 NULL
gfp_mask 的关键标志:
域(zone)选择:
__GFP_DMA:只在 ZONE_DMA 分配
__GFP_HIGHMEM:可以在 ZONE_HIGHMEM 分配
(默认:ZONE_NORMAL + ZONE_HIGHMEM)
行为(action):
__GFP_WAIT:允许睡眠等待内存(慢路径)
__GFP_HIGH:高优先级,紧急分配
特殊:
__GFP_ZERO:返回清零的页面
__GFP_NOFAIL:不允许失败(持续重试)
GFP_KERNEL = (__GFP_WAIT | __GFP_IO | __GFP_FS)
→ 正常分配,可以等待,可以磁盘 I/O,可以文件系统分配路径的完整流程
alloc_pages 的调用链:
alloc_pages(gfp_mask, order)
→ __alloc_pages(gfp_mask, order, 0, NULL)
→ get_page_from_freelist(gfp_mask, order, ...)
→ __alloc_pages_nodemask(gfp_mask, order, ...)
→ try_this_node(node, gfp_mask, order)
→ buddy 系统分配
实际路径(有水线和回收):
alloc_pages()
→ check_new_pages() // 快速路径:检查当前 CPU 的缓存
→ if (NULL) __alloc_pages()
→ watermark 检查(pages_high / pages_low / pages_min)
→ if (watermark OK) buddy 分配(fast path)
→ if (watermark FAIL) {
// 唤醒 kswapd
// 尝试直接回收(direct reclaim)
// 再试 buddy 分配
// 还失败?返回 NULL 或触发 OOM
}Buddy System 分配的核心函数:
static inline struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
int preferred_nid, nodemask_t *nodemask)
{
struct zone *zone;
struct plist_head *queues;
enum zone_type classzone_idx;
// 1. 找到最高优先级的可用节点和 zone
// 2. 检查水位线(watermark)
// 3. 如果 OK,调用 rmqueue_pcplist(per-CPU 页框)
// 4. 如果失败,调用 __rmqueue(真正从 buddy 分配)
// 5. 如果 buddy 失败,尝试从备用节点分配
// __rmqueue(真正分配):
// for (order = requested; order <= MAX_ORDER; order++)
// 在每个 order 的 free_list 上找可用块
// 如果找到,split 进入更小的 order
// 返回分配的页面
}watermark(内存水位线)
三个关键水位线:
1. pages_high(高水位):
- 内存充足,可以自由分配
- kswapd 处于休眠状态
2. pages_low(低水位):
- 开始唤醒 kswapd
- 异步回收开始
3. pages_min(最低水位):
- 必须立即回收(direct reclaim)
- 如果回收失败,触发 OOM Killer
计算方式:
- 最低水位(pages_min)= sqrt(物理内存) * 16 / sqrt(page_size) / 4
- 高水位(pages_high)= pages_min + watermark_scale_factor * total_pages / 100
- 低水位(pages_low)= pages_min + watermark_scale_factor * total_pages / 200
查看水位:
$ cat /proc/zoneinfo
Node 0, zone Normal
pages free 123456
min 1234
low 2345
high 3456高阶分配(allocate > 4KB)
order > 0 时的分配:
alloc_pages(GFP_KERNEL, 0); // 分配 1 页(4KB)
alloc_pages(GFP_KERNEL, 2); // 分配 4 页(16KB)
高阶分配的特点:
1. 从 buddy system 的对应 order 链表分配
2. 如果 order=N 链表为空,尝试 order=N+1
3. 如果分配了 order=N+1 的块,拆成两个 order=N
4. 一个分配,另一个放入 order=N 链表
例子:分配 128KB(order=5)
free_list[5] 有块 → 直接分配
free_list[5] 空:
free_list[6] 有 256KB 块
→ 拆成两个 128KB
→ 一个分配,一个放入 free_list[5]
free_list[6] 也空:
→ 继续向上找,直到 MAX_ORDER
MAX_ORDER(11)仍然没有:
→ 触发回收或返回 NULL分配失败和 OOM
分配失败的处理:
1. 快速路径失败(fast path):
- watermark 不满足
- 唤醒 kswapd
- schedule(让出 CPU)
2. 直接回收(direct reclaim):
- 调用 shrink_page_list()
- 扫描 LRU,释放页面
- 写回 dirty 页面到磁盘
- swapout anonymous 页面
3. 仍然失败:
- 如果是 GFP_ATOMIC(中断上下文):
→ BUG() 或 WARN(),无法等待
- 如果是 GFP_KERNEL:
→ out_of_memory() 触发 OOM Killer
→ 杀掉最"坏"的进程
→ 释放其页面
查看分配失败日志:
$ dmesg | grep -i "out of memory"
[12345.678] Out of memory: Killed process 1234 (nginx)总结
- alloc_pages(gfp_mask, order):分配 2^order 个物理页,返回 struct page 指针
- GFP_MASK:GFP_KERNEL(可等待+IO)/ GFP_ATOMIC(不可等待)/ __GFP_ZERO(清零)
- watermark:pages_high(充足)/ pages_low(kswapd 唤醒)/ pages_min(强制回收/触发 OOM)
- __rmqueue:遍历 free_list[order] → 如果空向上拆分 → 分配后返回
- 分配失败:先 direct reclaim → 再失败触发 OOM Killer
下篇预告(K12):内存碎片化是怎么形成的?compaction 和内存规整——为什么分配大块内存会失败,以及怎么缓解。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论