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页数大小典型用途
014KB普通页框分配
128KB内核栈、小型结构体
2416KB进程页表、缓冲区
3832KB网络数据包缓冲区
41664KBDMA 缓冲区
95122MB透明大页(THP)
1010244MB大页(HugePage)1GB 对齐

每个 zone(内存域)维护 11 个空闲区域(free_area[0..10]),每个 free_area[order] 是一个双向链表,链接所有大小为 2^order 的连续空闲块。

2.2 分配流程:上拆下合

当请求分配 2^order 大小的块时:

  1. 在 free_area[order] 链表查找空闲块
  2. 若命中,摘除并返回
  3. 若未命中,向上拆分(Buddy Split):从 order+1 阶取出一个块,一分为二,一块分配,另一块加入 order 阶链表
  4. 若 order+1 也未命中,继续向上查找,直到 MAX_ORDER-1(通常 11)
  5. 若所有阶都无可用页面,触发直接回收(direct reclaim)或压缩(compact)

拆分示例:请求 1 个页(order=0),free_area[0] 为空,free_area[1] 有 1 个 2 页块 → 拆为两个 1 页块,一个分配,一个加入 free_area[0]。

2.3 释放流程:伙伴合并

释放时需要检查对应"伙伴"(Buddy)是否也在空闲链表中:

  1. 计算伙伴块物理页号:buddy_pfn = pfn ^ (1 << order)
  2. 检查伙伴是否在 free_area[order] 中
  3. 若伙伴空闲,合并为 2^(order+1) 的大块,移入 free_area[order+1]
  4. 递归向上检查合并,直到无法合并或达到 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 流程

分配时:

  1. 若 pcp->count > 0,直接从本地热/冷列表取一页返回
  2. 若 pcp->count == 0,从伙伴系统批量获取 batch 页(默认 30 页 120KB),填充 pcp->list

释放时:

  1. 若 pcp->count < high,将页加入本地列表
  2. 若 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_fractionpcp->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_factorkcompactd 触发水位线缩放倍数约 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/vmstatBuddy 相关指标: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 与碎片的关系

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_threshold500调为 100-300值越小同步压缩越积极,但 CPU 开销增大
/proc/sys/vm/compaction_proactiveness20调为 50-80kcompactd 后台压缩激进度
/proc/sys/vm/watermark_scale_factor~10(1%)调为 2000(2%)降低后台回收触发延迟
/proc/sys/vm/compact_memory手动触发应急/启动写入 1 触发同步全节点压缩
/proc/sys/vm/nr_hugepages0KVM/DPDK静态预留 2MB 大页数量
/proc/sys/vm/nr_overcommit_hugepages0KVM可超额承诺的大页
/sys/kernel/mm/transparent_hugepage/enabledmadvisemadvise 或 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 结构向多层级、更智能的调度方向发展,但伙伴拆分合并的核心算法仍是其根基。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部