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 | 页数 | 大小 | 典型用途 |
|---|---|---|---|
| 0 | 1 | 4 KB | slab 对象、页表 |
| 1 | 2 | 8 KB | 小文件系统缓存 |
| 3 | 8 | 32 KB | DMA 缓冲区 |
| 5 | 32 | 128 KB | 中等大小内核分配 |
| 8 | 256 | 1 MB | 大块连续内存需求 |
| 10 | 1024 | 4 MB | 巨型页、设备驱动 |
分配过程遵循 “最小满足 + 分裂” 策略:
- 从请求 order 对应的链表查找空闲块;
- 若链表为空,向上一级 order 链表请求一个更大的块;
- 将大块分裂成两个等大的伙伴块,一个返回给请求者,另一个挂入低一级链表;
- 重复分裂直到满足请求 order。
关键指标:伙伴系统的分配时间复杂度为 O(1)(通过位图快速判断伙伴状态),释放合并的最坏情况为 O(log N)。这是 Linux 内核选择它作为基础分配器的重要原因。
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);
释放时伙伴系统执行 反向合并:
- 检查待释放块的伙伴是否空闲(通过
struct page中的private字段和位图); - 若伙伴空闲且 order 相同,合并为更大的块;
- 递归向上检查,直到无法合并或达到最大 order;
- 将最终块插入对应 order 的空闲链表。
注意:释放时必须传入与分配时完全相同的 order。传入错误的 order 会导致伙伴系统状态损坏,可能引发内存泄漏或内核崩溃。内核提供了
virt_to_page 和 page_address 辅助转换。
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 或极低值时,系统已进入碎片化状态,需要主动干预。
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。