3. 伙伴系统(Buddy System):页分配器原理、order与free_area结构、内存碎片化问题

伙伴系统是 Linux 内核物理内存页分配的核心算法,负责管理连续物理页框的分配与回收。它通过将内存按 2 的幂次方分割成块(称为 order),有效平衡了分配速度与外部碎片问题。本节将深入剖析其数据结构、分配/释放流程以及固有的碎片化挑战。

3.1 核心数据结构:order 与 free_area

伙伴系统的管理数据位于每个内存节点(pg_data_t)的 free_area 数组中。该数组的索引即为 order(阶),每个元素对应一个特定大小的空闲块链表。

order (阶) 块大小 (页数) 块大小 (字节, 4K页) 典型用途
0 2⁰ = 1 4 KB 匿名页、小文件页
1 2¹ = 2 8 KB 小型 slab 对象
2 2² = 4 16 KB 中等 slab 对象
3 2³ = 8 32 KB 文件系统元数据
4 2⁴ = 16 64 KB DMA 缓冲区
… … … …
10 2¹⁰ = 1024 4 MB 大块连续内存 (如显存)
MAX_ORDER-1 2^(MAX_ORDER-1) 最大连续块 系统最大分配单元
关键常量: MAX_ORDER 在主流内核中通常定义为 11,因此最大 order 为 10,对应 4 MB 的连续物理内存块。嵌入式系统可根据内存大小裁剪此值以节省管理开销。

free_area 结构体包含两个核心成员:

  • struct list_head free_list[MIGRATE_TYPES]:按迁移类型(MIGRATE_UNMOVABLE、MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE 等)分组的空闲块链表。同一 order 的空闲页块通过 page->lru 链接。
  • unsigned long nr_free:当前 order 中空闲块的总数。
数据结构关系: pg_data_t → struct zone → free_area[MAX_ORDER] → free_list[MIGRATE_TYPES] → struct page 链表。

3.2 伙伴系统分配与释放流程

分配和释放均遵循“分裂-合并”的递归逻辑,确保相邻块能重新组合成更大的连续块。

3.2.1 分配流程(以请求 order=2 为例)

  1. 查找空闲块: 从 free_area[2] 的链表中摘取一个空闲块。如果链表非空,直接返回。
  2. 向上分裂: 如果 free_area[2] 为空,则查找 free_area[3]。找到后,将 order=3 的块分裂成两个 order=2 的伙伴块:一个返回给请求者,另一个插入 free_area[2] 链表。
  3. 递归分裂: 若 free_area[3] 也为空,继续向 free_area[4] 查找,直到找到非空链表或达到 MAX_ORDER-1。
  4. 分配失败: 若所有 order 均无空闲块,则触发内存回收或 OOM。

3.2.2 释放流程(以释放 order=2 块为例)

  1. 查找伙伴: 计算当前释放块的伙伴地址。伙伴与当前块大小相同、物理地址相邻,且由同一个父块分裂而来。
  2. 伙伴空闲检查: 检查伙伴块是否在 free_area[2] 链表中且未被分配。
  3. 合并: 如果伙伴空闲,则从链表中摘除伙伴,将当前块与伙伴合并成一个 order=3 的块。
  4. 递归合并: 将合并后的 order=3 块作为新块,继续向上查找其伙伴(order=3 级别),尝试进一步合并。
  5. 插入空闲链表: 当无法继续合并时,将最终块插入对应 order 的 free_list。
伙伴关系判定: 两个块互为伙伴的充要条件是:它们由同一个更大的块分裂而来,且物理地址相差一个块的大小。内核通过 page_idx ^ (1 << order) 快速计算伙伴的页框号。

3.3 伙伴系统分配与合并流程 SVG 图

伙伴系统分配与合并流程 分配路径 (order=2) 请求 order=2 块 free_area[2] 非空? 是 摘取并返回 否 向上查找 order=3 分裂 order=3 块 一半返回,一半入 free_area[2] 释放路径 (order=2) 释放 order=2 块 计算伙伴地址 伙伴在 free_area[2]? 是 合并 否 插入 free_area[2] 关键说明 • 分配时向上分裂:从低 order 到高 order 查找,找到后逐级分裂。 • 释放时向下合并:释放后立即检查伙伴,递归合并以形成更大连续块。 • 伙伴计算:page_idx ^ (1 << order) 快速定位伙伴页框号。

3.4 内存碎片化问题

尽管伙伴系统通过合并机制缓解了外部碎片,但在长期运行的系统(尤其是嵌入式设备)中,碎片化仍然不可避免。主要分为两类:

3.4.1 外部碎片

指虽然总空闲内存足够,但无法分配出连续的物理大块。例如:系统有 10 个空闲页,但分散在 5 个不同的 order=0 块中,无法满足一个 order=2(4 页)的请求。

3.4.2 内部碎片

指分配出的块大于实际需求,造成块内空间浪费。例如:请求 3 页,伙伴系统只能分配 order=2(4 页)的块,浪费 1 页。

碎片类型 产生原因 影响 缓解策略
外部碎片 频繁分配/释放不同大小的块,导致空闲块无法合并 大块连续分配失败,触发内存回收或 OOM 迁移类型分组、内存规整 (compaction)、防碎片 (anti-fragmentation)
内部碎片 分配粒度固定为 2 的幂次方 内存利用率下降,有效容量减少 slab 分配器、vmalloc 区域、页面着色

3.5 碎片化问题可视化 SVG 图

伙伴系统外部碎片化示意 初始空闲内存 (连续 16 页) 连续 16 页空闲 (order=4) 分配 3 次 order=2 (4页) 后 已分配 A 空闲 已分配 B 空闲 释放 A 和 B 后 (无法合并为 order=4) 空闲 A 已分配 C (不可移动) 空闲 B 空闲 碎片化结果:空闲页总数 12 页,但最大连续块仅为 4 页 (order=2),无法满足 order=3 (8页) 的分配请求。 解决方案:内存规整 (compaction) 将已分配 C 迁移,使空闲 A 和空闲 B 合并为连续 8 页。

3.6 嵌入式环境下的碎片化应对策略

在内存资源受限的嵌入式系统中,碎片化可能导致关键驱动(如 DMA、显示控制器)分配大块连续内存失败。以下是常用优化手段:

  • 迁移类型分组 (MIGRATE_TYPES):将不可移动页、可回收页、可移动页分别放入不同链表,避免不可移动页阻塞大块连续内存的形成。
  • 内存规整 (Memory Compaction):通过扫描并迁移可移动页,将分散的空闲页合并成大块连续区域。内核通过 /proc/sys/vm/compact_memory 手动触发。
  • 预分配大块内存池:在系统初始化阶段,通过 alloc_pages_exact() 或 CMA (Contiguous Memory Allocator) 预留连续内存区域,专供驱动使用。
  • 调整 MAX_ORDER:对于内存极小的嵌入式设备(如 64 MB),可将 MAX_ORDER 从 11 降低到 8 或 9,减少管理链表开销,但会限制最大连续分配大小。
  • 使用 slab 分配器:对于小于一页的小对象,绕过伙伴系统直接使用 slab,避免内部碎片。
内核裁剪实践: 在嵌入式 Linux 中,通过内核配置 CONFIG_COMPACTION 和 CONFIG_CMA 可启用内存规整与连续内存分配器。对于不需要大块连续内存的设备,可以关闭这些功能以节省代码段和运行时开销。