Buddy System 深度工程实战:Linux 物理页分配器架构设计与碎片治理
一、为什么物理页分配器是内存管理的基石
Linux 内核内存管理子系统是一个精密的层级结构,而处于最底层的 Buddy System(伙伴系统) 负责直接管理物理页面的分配与回收。所有上层分配器——Slab/SLUB/SLOB、vmalloc、透明大页(THP)、KSM——最终都通过 Buddy System 获取物理内存。理解 Buddy System 的行为特征,是诊断内存碎片、优化大页分配、排查 OOM 问题的关键前提。
本文将从伙伴系统的核心算法出发,完整拆解其数据结构设计、分配与释放流程、Per-CPU 页缓存机制、反碎片策略、内存主动压缩、与其他子系统的交互,最后提供生产环境调优参数速查表与故障排查案例。
二、伙伴系统核心算法
2.1 分配阶(Order)概念
Buddy System 管理的最小单位是页(通常 4KB),但实际分配以 2 的幂次页数 为单位:
| Order | 页数 | 大小 | 典型用途 |
|---|---|---|---|
| 0 | 1 | 4KB | 普通页框分配 |
| 1 | 2 | 8KB | 内核栈、小型结构体 |
| 2 | 4 | 16KB | 进程页表、缓冲区 |
| 3 | 8 | 32KB | 网络数据包缓冲区 |
| 4 | 16 | 64KB | DMA 缓冲区 |
| 9 | 512 | 2MB | 透明大页(THP) |
| 10 | 1024 | 4MB | 大页(HugePage)1GB 对齐 |
每个 zone(内存域)维护 11 个空闲区域(free_area[0..10]),每个 free_area[order] 是一个双向链表,链接所有大小为 2^order 的连续空闲块。
2.2 分配流程:上拆下合
当请求分配 2^order 大小的块时:
- 在
free_area[order]链表查找空闲块 - 若命中,摘除并返回
- 若未命中,向上拆分(Buddy Split):从 order+1 阶取出一个块,一分为二,一块分配,另一块加入 order 阶链表
- 若 order+1 也未命中,继续向上查找,直到
MAX_ORDER-1(通常 11) - 若所有阶都无可用页面,触发直接回收(direct reclaim)或压缩(compact)
拆分示例:请求 1 个页(order=0),free_area[0] 为空,free_area[1] 有 1 个 2 页块 → 拆为两个 1 页块,一个分配,一个加入 free_area[0]。
2.3 释放流程:伙伴合并
释放时需要检查对应"伙伴"(Buddy)是否也在空闲链表中:
- 计算伙伴块物理页号:
buddy_pfn = pfn ^ (1 << order) - 检查伙伴是否在 free_area[order] 中
- 若伙伴空闲,合并为 2^(order+1) 的大块,移入 free_area[order+1]
- 递归向上检查合并,直到无法合并或达到 MAX_ORDER
伙伴的定义:两个大小相同的连续块,且低地址块的起始页号是 2^(order+1) 的倍数。伙伴关系的数学表达为 buddy_pfn = pfn XOR (1 << order),使得合并时只需一次异或即可找到伙伴。
三、内存域(Zone)与节点(Node)架构
3.1 多域设计的历史背景
32 时代,DMA 控制器只能寻址前 16MB 内存,因此定义了 ZONE_DMA。随着发展, ZONE_DMA32(低 4GB)和 ZONE_HIGHMEM(高端内存)也被引入。虽然后两者在现代 64 位系统中已不必要,但为兼容硬件保留。
现代服务器的典型 ZONE 布局(x86_64):
- ZONE_DMA: 0 ~ 16MB(兼容 legacy DMA 设备)
- ZONE_DMA32: 16MB ~ 4GB(兼容 32 位 DMA 设备)
- ZONE_NORMAL: 16MB ~ 896MB(线性映射区,kernel 空间)
- ZONE_MOVABLE: 动态定义(仅当配置 CONFIG_MOVABLE_NODE 启用,将内存分为可移动和不可移动区域便于热插拔和碎片整理)
3.2 NUMA 节点与 Zone 层次
每个 NUMA 节点(由 struct pg_data_t 描述)包含多个 ZONE:
Node 0
├── ZONE_DMA (free: 15M)
├── ZONE_DMA32 (free: 3.8G)
├── ZONE_NORMAL (free: 12G)
└── ZONE_MOVABLE (free: 0)
Node 1
├── ZONE_DMA (free: 15M)
├── ZONE_DMA32 (free: 3.8G)
├── ZONE_NORMAL (free: 14G)
└── ZONE_MOVABLE (free: 0)
3.3 水位线(Watermark)机制
每个 Zone 有三个水位线,决定内存回收的触发时机:
| 水位 | 含义 | 作用 |
|---|---|---|
| WMARK_MIN | 最低水位 | zone_reclaim_mode=1 时直接触发同步回收 |
| WMARK_LOW | 低水位 | 触发 kswapd 异步页回收(默认) |
| WMARK_HIGH | 高水位 | kswapd 停止回收,Zone 平衡 |
当空闲页面低于 WMARK_LOW 时,kswapd 内核线程被唤醒,根据 vm.min_free_kbytes 计算出的差值决定每次扫描的页面数。
四、Per-CPU 页缓存(pageset):冷热分离
4.1 为什么需要 Per-CPU 缓存
伙伴系统全局锁(zone->lock)在高并发场景下成为瓶颈。Linux 2.6 引入 Per-CPU pageset 机制:每个 CPU 维护一个本地页批量缓存,小分配优先走本地缓存,减少全局锁争用。
4.2 热页(Hot Page)与冷页(Cold Page)
pageset 进一步区分热页和冷页:
- 热页(hot):刚从伙伴系统取出、可能还在 CPU 缓存中的页。分配时倾向返回热页,提升缓存命中率
- 冷页(cold):已被回收或长时间未使用、缓存可能已失效的页。批量缓存耗尽时先返回冷页
对应的数据结构是 pageset->pcp(per_cpu_pages),包含 count(当前缓存页数)、high(高水位)、batch(批量大小):
struct per_cpu_pages {
int count; // 当前 pcp 列表中的页数
int high; // 超过此值则归还伙伴系统
int batch; // 从伙伴系统批量申请/归还的页数
struct list_head list; // 热/冷页链表
};
4.3 缓存 replenish 与 drain 流程
分配时:
- 若 pcp->count > 0,直接从本地热/冷列表取一页返回
- 若 pcp->count == 0,从伙伴系统批量获取
batch页(默认 30 页 120KB),填充 pcp->list
释放时:
- 若 pcp->count < high,将页加入本地列表
- 若 pcp->count >= high,将
batch页归还伙伴系统
4.4 可调参数
| 参数 | 说明 | 默认值 |
|---|---|---|
/proc/sys/vm/per_cpu_pagelist_high_fraction | 控制 pcp->high 相对于 zone 大小的分数 | 0(使用 zone 大小自动计算) |
/proc/sys/vm/per_cpu_pagelist_fraction | pcp->high = zone->managed_pages / fraction | 动态计算 |
五、反碎片机制:页面迁移类型
5.1 碎片问题的本质
伙伴系统经过长期运行后,原本连续的物理页面被随机分配的小块使用,导致无法合并出大块——这就是外部碎片。在 order-9(2MB)请求场景(如 THP 大页分配、DMA 连续缓冲区)下,碎片问题尤为严重。
碎片分为三种:
- 外部碎片:空闲页面不连续,无法满足高阶分配
- 内部碎片:分配大小超过实际需求(如申请 8KB 但实际只用 800B)
- 时间维度碎片:应用中短期大量分配导致临时碎片
5.2 页面迁移类型分类(Migration Types)
Linux 3.0 引入基于迁移类型的反碎片,在伙伴系统中将空闲页面按移动性分为四类:
| 类型 | 英文 | 说明 | 典型来源 |
|---|---|---|---|
| 不可移动 | MIGRATE_UNMOVABLE | 物理地址固定,无法迁移 | 内核代码、页表分配(alloc_pages GFP_KERNEL) |
| 可移动 | MIGRATE_MOVABLE | 页框可被迁移或回收 | 用户空间内存、页缓存(GFP_HIGHUSER_MOVABLE) |
| 可回收 | MIGRATE_RECLAIMABLE | 无法迁移但可回收释放 | 内核 slab 对象、dcache/inode 缓存 |
| 可隔离 | MIGRATE_ISOLATE | 完全隔离区域(用于 CMA/hotplug) | Contiguous Memory Allocator 预留 |
每种 ZONE 维护独立free_area[order][migratetype] 链表,分配时严格按请求的迁移类型匹配。这保证了相同移动性的页面聚集在同一区域,相邻页框能长期保持有效合并。
5.3 迁移类型分配策略
内核通过 zone->page_alloc_fallbacks 定义优先失败后回退的迁移类型:
// mm/page_alloc.c (典型配置)
static int zonetables[] = {
[MIGRATE_UNMOVABLE] = MIGRATE_RECLAIMABLE_FALLBACK, // 不可移动失败 → 尝试可回收区的不可移动
[MIGRATE_RECLAIMABLE] = MIGRATE_UNMOVABLE_FALLBACK, // 可回收失败 → 尝试不可移动区的可回收
[MIGRATE_MOVABLE] = MIGRATE_RECLAIMABLE_FALLBACK, // 可移动失败 → 尝试可回收区的可移动
};
这种回退策略兼顾分配成功率和碎片控制——当 MOVABLE 区的所需类型不足时,允许从 RECLAIMABLE 区获取,避免完全失败。
六、内存主动压缩(Memory Compaction)
6.1 同步压缩 vs 异步压缩
伙伴系统提供了两种碎片治理方式:
- 同步 compaction(直接压缩):在高阶分配失败时,由分配者同步执行,尽可能整理碎片后重试分配。触发条件:
GFP_DIRECT_RECLAIM且分配阶>= sysctl_extfrag_threshold - 异步 compaction(后台压缩):由
kcompactd内核线程周期性扫描 Zone,发现碎片严重时主动整理
6.2 kcompactd 后台线程
每个 NUMA 节点的每个 Zone 可以有一个 kcompactd 内核线程,在 /proc/sys/vm/watermark_boost_factor 和 compaction_proactiveness 指导下工作:
// 伪代码:kcompactd 工作循环
while (!kthread_should_stop()) {
// 检查当前 Zone 碎片指数
fragmentation_score = calc_zone_fragmentation(zone);
if (fragmentation_score > threshold) {
// 执行压缩:从 Zone 底部扫描,移动 MOVABLE 页面
compact_zone(zone, cc);
}
wait_event_idle(zone->kcompactd_wait, ...);
}
6.3 手动触发 compaction
可通过 /proc/sys/vm/compact_memory 接口手动触发全节点压缩:
echo 1 > /proc/sys/vm/compact_memory # 触发所有节点的同步压缩
6.4 Compaction 可调参数
| 参数 | 说明 | 默认值 |
|---|---|---|
/proc/sys/vm/compaction_proactiveness | 后台压缩激进程度(0-100) | 20 |
/proc/sys/vm/extfrag_threshold | 触发同步压缩的碎片阈值(0-1000) | 500 |
/proc/sys/vm/watermark_scale_factor | kcompactd 触发水位线缩放倍数 | 约 6(对应 ~0.6% 内存) |
/proc/sys/vm/page_lock_unfairness | 页面锁不公平度参数 | 5(新版内核) |
七、与 Slab 分配器的交互关系
7.1 层次结构
Buddy System 与 Slab 分配器的关系:
用户态 alloc (malloc)
↓
brk()/mmap()
↓
内核: 页缓存/匿名页分配
↓
Buddy System (alloc_pages/get_free_pages)
内核内部 kmalloc()
↓
Slab/SLUB 分配器
↓
alloc_pages() → Buddy System
7.2 Slab 从 Buddy System 分配
Slab 分配器以 object(如 task_struct、dentry)为粒度管理内存,但向 Buddy System 申请的是 2^order 大小 的页框,然后在内部划分为多个 object。
典型分配模式:
- 小型 cache(object < 页大小 1/8):使用 order=0/1 从 Buddy 获取
- 中型 cache(object ~ 页大小级别):使用 order=1/2
- 大型 cache(object > 页大小):使用高阶 order(如 kmalloc-8K→order=1, kmalloc-16K→order=2, 最大通常 2^5=128KB)
7.3 Slab 碎片 vs Buddy 碎片的关系
两者相互影响:Slab 长期持有大量 buddy 页面会导致 buddy 外部碎片加剧;反过来,buddy 碎片又限制了 slab 高阶 cache 的内存供给。
缓解措施:
SLAB/SLUB的 shrinker 回调主动释放空闲 object- sysctl
vm.min_slab_ratio/vm.min_unmapped_ratio控制 slab/未映射页的最低保留比例 drop_caches可紧急释放 Slab 对象(非持久,仅调试用)
八、诊断工具与性能观察
8.1 /proc/buddyinfo
显示每个 Zone 各 order 级别的空闲块数量:
$ cat /proc/buddyinfo
Node 0, zone DMA 1 1 0 0 2 1 1 0 1 1 3
Node 0, zone DMA32 15 23 9 3 2 1 1 2 1 3 11
Node 0, zone Normal 3342 1283 708 295 131 67 18 8 2 1 1
Node 1, zone Normal 4097 1528 891 367 162 82 22 11 3 2 1
解读:
- DMA zone 的 order 0 有 1 块,order 1 有 1 块,order 2 有 0 块 → 无法分配 4 页以上连续内存
- Normal zone order 9(第10列)只有 1 块 → 只有 1 个 2MB 连续块,THP 分配压力大
- order 10(最后一列)Normal zone 有 1/1 → 1MB 连续块,小于 2MB
碎片告警条件:order < MAX_ORDER-1 的非零项很少,而总空闲内存充足,说明存在严重外部碎片。
8.2 /proc/pagetypeinfo
buddyinfo 的进阶版本,按迁移类型展示:
$ cat /proc/pagetypeinfo | head -40
Page block order: 9
Pages per block: 512
Free pages count at different migratetype in order 0-10
Node 0, zone DMA, type Unmovable 0 1 1 0 2 1 1 0 1 1 3
Node 0, zone DMA, type Movable 1 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type Reclaimable 0 0 0 0 0 0 0 0 0 0 0
...
Node 1, zone Normal, type Unmovable 128 89 29 14 8 3 0 0 0 0 0
Node 1, zone Normal, type Movable 356 294 310 145 75 42 11 5 1 1 0
Node 1, zone Normal, type Reclaimable 261 128 79 46 20 14 6 3 1 0 0
诊断场景:
- Normal zone Unmovable 高阶项不为零而 Movable 高阶项为零 → 不可移动页面散布在高地址区,碎片化可移动页面
- Reclaimable 占用大量低阶 → slab 缓存过多,回收效率低
- 所有迁移类型高阶寥寥无几 → 系统即将出现大页分配失败
8.3 其他诊断接口
| 工具 | 说明 |
|---|---|
/proc/zoneinfo | 详细 Zone 信息:管理页、空闲页、水位线、各类统计 |
/proc/vmstat | Buddy 相关指标:pgalloc_dma/normal/movable(分配次数)、pgcompact_success/fail(压缩成功率) |
/sys/kernel/debug/extfrag/unusable_index | 各 Zone 不可用指数(0-1000,值越大碎片越严重) |
dmesg | grep -i 'compaction\|buddy\|fragment' | 内核日志中碎片告警和 compaction 通知 |
8.4 监控指标
# 实时监控 buddy 分配失败与压缩
watch -n1 'grep -E "compact|buddy|oom" /proc/vmstat'
# 查看 compaction 成功率
compact_success = $(cat /proc/vmstat | grep compact_success | awk '{print $2}')
compact_fail = $(cat /proc/vmstat | grep compact_fail | awk '{print $2}')
rate=$((compact_success * 100 / (compact_success + compact_fail)))
echo "Compaction success rate: ${rate}%"
九、生产环境调优策略
9.1 大页分配优化(HugePage/THP)
透明大页(THP)依赖 Buddy System 分配 order-9 连续页面。碎片化会导致 THP 频繁失败,回退到 4K 页:
# 查看 THP 分配统计
cat /proc/vmstat | grep thp
# thp_fault_alloc: THP 缺页分配成功次数
# thp_fault_fallback: THP 分配失败回退到普通页次数
# thp_compact_fail: Compaction 失败后 THP 分配失败次数
# 调优方案 1:提升 compaction 激进度
echo 80 > /proc/sys/vm/compaction_proactiveness
# 调优方案 2:提高 sync compaction 触发阈值(更积极地压缩)
echo 100 > /proc/sys/vm/extfrag_threshold
# 调优方案 3:牺牲延迟换成功率(允许同步压缩)
echo 1 > /proc/sys/vm/compact_unevictable_allowed
# 调优方案 4:为预留大页池扩容(推荐生产环境)
echo 1024 > /proc/sys/vm/nr_hugepages # 静态预留 2MB×1024 = 2GB
9.2 普通服务器通用碎片优化
# 提升 kcompactd 积极性
echo 50 > /proc/sys/vm/compaction_proactiveness
# 降低同步压缩触发阈值(attemp more aggressively)
echo 300 > /proc/sys/vm/extfrag_threshold
# 提升 watermark_scale_factor(更早触发后台回收)
echo 2000 > /proc/sys/vm/watermark_scale_factor # 最大 3000
# 合理设置 min_free_kbytes(预留给紧急分配)
echo 262144 > /proc/sys/vm/min_free_kbytes # 256MB on 512GB RAM
9.3 虚拟化/KVM 环境特殊处理
KVM 大量使用 HugePage 和 THP 来减少 EPT 切换开销,需保证虚拟机启动时有足够大页:
#!/bin/bash
# 启动前预压缩
echo 1 > /proc/sys/vm/compact_memory
# 预留大页给 VM
echo 2048 > /proc/sys/vm/nr_hugepages
# 验证
cat /proc/meminfo | grep HugePages_Total
9.4 高主频分配调优(低延迟场景)
- 如果业务可预测内存使用情况,可使用
GFP_NOWAIT 标志避免进入 compaction 路径
- 通过
mlock 锁定关键内存防止被回收和迁移
- 利用
cgroup memory.low 保护关键服务的内存供给
十、OOM 与 Buddy System 的关联
10.1 OOM 触发链路
alloc_pages()
→ __alloc_pages_nodemask()
→ get_page_from_freelist() fail
→ __alloc_pages_direct_reclaim() (同步回收)
→ __alloc_pages_direct_compact() (压缩整理)
→ 全部失败 → out_of_memory() → oom_kill()
10.2 OOM 与碎片的关系
GFP_NOWAIT 标志避免进入 compaction 路径mlock 锁定关键内存防止被回收和迁移cgroup memory.low 保护关键服务的内存供给
alloc_pages()
→ __alloc_pages_nodemask()
→ get_page_from_freelist() fail
→ __alloc_pages_direct_reclaim() (同步回收)
→ __alloc_pages_direct_compact() (压缩整理)
→ 全部失败 → out_of_memory() → oom_kill()
OOM 不一定意味着"真的没内存":即使系统有足够空闲物理页,但如果碎片化严重(所有空闲页都是 order-0 的碎片,没有任何连续大块),高阶请求也会导致 OOM kill。
诊断公式:
空闲页面数 > min_free_kbytes 但 order-9 空闲块数 = 0 + 日志出现 page allocation failure: order:9 → 确认是碎片导致的"假 OOM"。
10.3 避免碎片型 OOM 的策略
- 静态 HugePage 预留:在系统启动早期、碎片尚未形成时预分配
nr_hugepages,buddy 会专门保留这些大页 - 启动时 compaction:在系统服务启动前执行
compact_memory,消除启动阶段的碎片 - 隔离不可移动页:将用户态服务限制在 MOVABLE zone,减少不可搬移页面的散布
- CMA(Contiguous Memory Allocator):让硬件消耗者(摄像头/DMA 显示)从连续区分配,避免与 Buddy 竞争
十一、Buddy System 新特性与演进趋势
11.1 Zone MOVABLE 独立化
Linux 5.x 引入了更激进的 Zone Movable 隔离,配合 MPOL_MF_MOVE NUMA 迁移策略,将用户态与内核态页面物理分离。
11.2 Variable Order 支持(5.16+)
引入struct folio 概念,允许 Buddy System 支持非 2 的幂次的大页框分配,减少大页分配时的内部碎片。
11.3 Memory Tiering 分层感知
Linux 6.x 为异构内存(DRAM + PMem + CXL)引入分层管理,Buddy System 的 Zone 层级扩展为多 Tier,配合 DAMON 做智能页面升降级。
11.4 Generalized OOM
逐步引入 cgroup-aware OOM 机制,OOM 决策不再只看全局空闲页,而是按 cgroup 水位判断,避免碎片误杀非关键服务。
十二、参数速查表
| 参数 | 默认值 | 推荐场景 | 说明 |
|---|---|---|---|
/proc/sys/vm/min_free_kbytes | 系统自动计算 | 通用 | 过低→频繁 direct reclaim;过高→内存浪费 |
/proc/sys/vm/extfrag_threshold | 500 | 调为 100-300 | 值越小同步压缩越积极,但 CPU 开销增大 |
/proc/sys/vm/compaction_proactiveness | 20 | 调为 50-80 | kcompactd 后台压缩激进度 |
/proc/sys/vm/watermark_scale_factor | ~10(1%) | 调为 2000(2%) | 降低后台回收触发延迟 |
/proc/sys/vm/compact_memory | 手动触发 | 应急/启动 | 写入 1 触发同步全节点压缩 |
/proc/sys/vm/nr_hugepages | 0 | KVM/DPDK | 静态预留 2MB 大页数量 |
/proc/sys/vm/nr_overcommit_hugepages | 0 | KVM | 可超额承诺的大页 |
/sys/kernel/mm/transparent_hugepage/enabled | madvise | madvise 或 always | 是否对未 madvise 区域开启 THP |
十三、总结
Buddy System 并非简单的"拆了就分、分了就合"的朴素算法,实际上是一个融合了 Per-CPU 缓存、迁移类型反碎片、主动压缩、水位线回收 的复杂生产级内存分配器。其设计哲学是:
- 用时间换空间:通过主动压缩消耗 CPU 时间换取大块连续空间
- 用局部性换效率:Per-CPU 缓存减少全局锁竞争
- 用分类换预防:迁移类型从源头避免碎片产生
在运维实践中,判断系统是否存在 Buddy 碎片问题,首选检查 /proc/buddyinfo 高阶项分布,结合 /proc/vmstat 的压缩指标即可定位。对于数据库、Java 堆、KVM 场景,建议静态预留 HugePage 配合适度提升 compaction 激进度,在碎片型 OOM 和 CPU 开销之间取得平衡。
随着异构内存(CXL/PMem)和 Memory Tiering 的演进,Buddy System 正在从单一 Zone 结构向多层级、更智能的调度方向发展,但伙伴拆分合并的核心算法仍是其根基。

发表评论 取消回复