2. 伙伴系统(Buddy System)

伙伴系统是 Linux 内核物理页面管理的基础分配器,直接管理 物理内存页(4KB 页框) 的分配与释放。它位于内存管理的最底层,为 slab 分配器、vmalloc、直接映射区等上层机制提供连续的物理页面。理解伙伴系统是掌握内存碎片问题的前提。

核心思想:将物理内存按 2 的幂次方划分为块(order),相邻且大小相同的两个块互为“伙伴”。分配时从满足需求的最小块中切分;释放时检查伙伴是否空闲,若空闲则合并为更大的块,以此减少外部碎片。

2.1 伙伴系统原理

伙伴系统将物理内存组织成 11 个链表(order 0 ~ order 10),每个链表挂载大小相同的连续页块。order 0 块大小为 1 页(4KB),order 1 为 2 页(8KB),……,order 10 为 1024 页(4MB)。

Order页数大小典型用途
014 KBslab 对象、页表
128 KB小文件系统缓存
3832 KBDMA 缓冲区
532128 KB中等大小内核分配
82561 MB大块连续内存需求
1010244 MB巨型页、设备驱动

分配过程遵循 “最小满足 + 分裂” 策略:

  1. 从请求 order 对应的链表查找空闲块;
  2. 若链表为空,向上一级 order 链表请求一个更大的块;
  3. 将大块分裂成两个等大的伙伴块,一个返回给请求者,另一个挂入低一级链表;
  4. 重复分裂直到满足请求 order。
关键指标:伙伴系统的分配时间复杂度为 O(1)(通过位图快速判断伙伴状态),释放合并的最坏情况为 O(log N)。这是 Linux 内核选择它作为基础分配器的重要原因。
伙伴系统分配流程:从 order 3 分裂到 order 1 order 3 空闲块(8 页) 分裂 order 2 伙伴 A(4 页) order 2 伙伴 B(4 页) 分裂 order 1 伙伴 A1(2 页) order 1 伙伴 A2(2 页) 分配出去(2 页) 剩余块状态: • order 1 伙伴 A2 → 空闲链表 • order 2 伙伴 B → 空闲链表

2.2 页面分配与释放

内核通过 alloc_pages 和 __free_pages 两个核心函数与伙伴系统交互。分配时指定 gfp_mask(分配标志) 和 order,释放时传入页面描述符 struct page * 和 order。

2.2.1 分配路径

// 分配 2^order 个连续物理页
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);

// 常用封装:
// 分配单页
struct page *alloc_page(gfp_t gfp_mask);
// 分配并返回虚拟地址
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);

分配标志 gfp_mask 控制行为:

标志含义使用场景
GFP_KERNEL普通内核分配,可能睡眠进程上下文
GFP_ATOMIC原子分配,不睡眠中断、软中断、自旋锁内
GFP_DMA从 DMA 区域分配设备 DMA 缓冲区
__GFP_HIGHMEM允许从高端内存分配用户空间页缓存

2.2.2 释放路径

// 释放 2^order 个连续物理页
void __free_pages(struct page *page, unsigned int order);

// 常用封装:
void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned int order);

释放时伙伴系统执行 反向合并:

  1. 检查待释放块的伙伴是否空闲(通过 struct page 中的 private 字段和位图);
  2. 若伙伴空闲且 order 相同,合并为更大的块;
  3. 递归向上检查,直到无法合并或达到最大 order;
  4. 将最终块插入对应 order 的空闲链表。
注意:释放时必须传入与分配时完全相同的 order。传入错误的 order 会导致伙伴系统状态损坏,可能引发内存泄漏或内核崩溃。内核提供了 virt_to_page 和 page_address 辅助转换。
页面释放与伙伴合并流程 释放 order 1 块 检查伙伴是否空闲 合并为 order 2 块 插入 order 1 空闲链表 递归向上合并 合并条件: 1. 伙伴块必须与当前块大小相同(相同 order) 2. 伙伴块必须物理地址连续(地址相差 2^order 页)

2.3 碎片化问题

尽管伙伴系统通过合并机制努力减少碎片,但在长期运行的内核中,外部碎片 仍然是严重问题。碎片化导致即使总空闲内存充足,也无法分配连续的物理大块。

2.3.1 碎片类型

碎片类型定义对伙伴系统的影响
外部碎片空闲页面分散在物理地址空间各处,无法形成连续大块高 order 分配失败,即使总空闲页数足够
内部碎片分配块大于实际需求,块内存在未使用空间内存利用率下降,但伙伴系统本身不感知

2.3.2 碎片化成因

  • 分配与释放顺序交错:不同大小的块频繁分配释放,破坏伙伴关系,阻止合并。
  • 长生命周期页面:内核核心代码、页表、slab 缓存等长期驻留页面成为“碎片锚点”。
  • MAX_ORDER 限制:伙伴系统最大 order 为 10(4MB),超过此大小的连续需求无法满足。
  • 内存域(zone)边界:DMA、DMA32、Normal 等内存域之间的物理地址不连续,加剧碎片。

2.3.3 碎片化度量

内核通过 /proc/buddyinfo 暴露各 order 的空闲块数量,是诊断碎片化的直接工具:

# cat /proc/buddyinfo
Node 0, zone   Normal   1234   567    234    89    34    12    5    2    1    0    0
Node 0, zone  DMA32    4567  1234    567   234    89    34    12    5    2    1    0
Node 1, zone   Normal   2345   890    345   123    45    18    7    3    1    0    0

每列从左到右对应 order 0 ~ order 10。若高 order 列(右侧)长期为 0,说明存在严重外部碎片。

碎片化临界指标:当 order 3(8 页,32KB)以上连续块频繁分配失败,且 /proc/buddyinfo 中 order ≥ 3 的列持续为 0 或极低值时,系统已进入碎片化状态,需要主动干预。
伙伴系统外部碎片化示意 物理地址空间(连续) 已分配 已分配 已分配(长生命周期) 已分配 已分配 空闲 空闲 空闲 空闲 问题:无法分配连续 4 页(order 2) 空闲页总数 = 1+2+2+1 = 6 页,但最大连续块只有 2 页 后果:高 order 分配失败 → 触发内存回收或 OOM

2.4 碎片化缓解机制

Linux 内核引入了多种机制对抗伙伴系统碎片化,这些机制将在后续章节详细展开:

机制原理引入版本
反碎片化(Anti-fragmentation)将可回收页面与不可回收页面分组管理,避免不可回收页面阻塞合并2.6.24
内存压缩(Compaction)通过页面迁移将分散的空闲页合并到连续区域2.6.32
虚拟可移动域(ZONE_MOVABLE)专门用于可迁移页面的内存域,便于压缩2.6.23
巨型页(HugeTLB / THP)预分配大块连续内存,减少碎片化压力2.6.x / 2.6.38
调优切入点:监控 /proc/buddyinfo 和 /proc/pagetypeinfo,当 order ≥ 3 的连续块不足时,考虑触发手动内存压缩:echo 1 > /proc/sys/vm/compact_memory。