2. 伙伴系统核心思想

伙伴系统(Buddy System)是 Linux 内核管理物理内存的核心算法,其设计目标是在分配与释放频繁发生的场景下,既能快速找到连续物理页,又能有效抑制外部碎片。理解伙伴系统的三个基石——伙伴定义、阶(order)与二叉树、合并与分裂规则,是掌握整个内存子系统的前提。

2.1 伙伴(Buddy)的严格定义

在伙伴系统中,两个内存块互为“伙伴”必须同时满足以下三个条件:

  1. 大小相等:两个块包含的连续物理页框(page frame)数量相同,均为 2order 页。
  2. 物理地址连续:两个块在物理地址空间上紧邻,前一块的结束地址与后一块的起始地址相差一个块大小。
  3. 由同一个大块分裂而来:两个块必须来自同一个大小为 2order+1 的父块的一次分裂,且它们的物理起始地址满足“伙伴对齐”条件。
伙伴对齐公式:对于阶为 order 的块,其物理起始地址必须对齐到 2order × PAGE_SIZE。两个伙伴块的起始地址相差恰好 2order × PAGE_SIZE。

下表总结了不同阶下伙伴块的关键属性:

阶(order) 块大小(页数) 块大小(典型 4K 页) 伙伴对齐要求 伙伴地址差
0 1 4 KB 4 KB 对齐 4 KB
1 2 8 KB 8 KB 对齐 8 KB
2 4 16 KB 16 KB 对齐 16 KB
3 8 32 KB 32 KB 对齐 32 KB
… … … … …
MAX_ORDER-1 2MAX_ORDER-1 通常 4 MB 4 MB 对齐 4 MB
关键理解:伙伴关系不是任意的两个相邻块,而是“由同一个父块分裂出来的孪生兄弟”。这一严格定义保证了合并时不会破坏更大块的边界对齐,是系统能递归合并的根本前提。

2.2 阶(order)与二叉树结构

伙伴系统使用阶(order)来量化内存块的大小。一个阶为 order 的块包含 2order 个连续物理页。内核维护一个以阶为索引的空闲链表数组 free_area[order],每个链表串连该阶下所有空闲块。

从数据结构视角看,伙伴系统天然对应一棵完全二叉树:

  • 根节点:代表整个管理区(zone)的最大连续内存块,阶为 MAX_ORDER-1。
  • 内部节点:代表一个大小为 2order 的块,其左子节点和右子节点互为伙伴。
  • 叶子节点:阶为 0,代表单个物理页框。

下图展示了一个从 order=3(8 页)到 order=0(1 页)的伙伴二叉树结构:

伙伴系统二叉树结构(MAX_ORDER=4) order=3 (8页) order=2 (4页) order=2 (4页) order=1 (2页) order=1 (2页) order=1 (2页) order=1 (2页) order=0 order=0 order=0 order=0 order=0 order=0 order=0 order=0 左子节点 ↔ 右子节点互为伙伴 同一父节点分裂产生伙伴对
内核实现要点:Linux 使用 struct free_area 数组管理空闲块,每个 free_area[order] 包含一个链表头和一个 nr_free 计数器。二叉树结构并非显式存储,而是通过物理地址的位运算隐式推导伙伴关系。

2.3 内存块分裂规则

当内核请求分配 2order 个连续页时,若对应阶的空闲链表为空,则触发分裂(split)操作:

分裂流程(自顶向下):
  1. 向上查找:从 free_area[order] 开始,逐级检查更高阶的空闲链表,直到找到第一个非空链表 free_area[higher]。
  2. 取出大块:从 free_area[higher] 链表中摘取一个空闲块。
  3. 递归分裂:将大块一分为二,得到两个大小为 2higher-1 的伙伴块。
  4. 插入空闲链表:将其中一个伙伴块(通常是地址较低的那个)插入 free_area[higher-1] 链表。
  5. 重复:对另一个伙伴块继续分裂,直到得到目标 order 的块。
  6. 返回:将最终得到的 2order 大小的块返回给调用者。

分裂过程可以用以下伪代码清晰表达:

/* 伙伴系统分裂核心逻辑(简化) */
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order)
{
    unsigned int current_order;
    struct free_area *area;
    struct page *page;

    /* 从目标 order 开始向上查找 */
    for (current_order = order; current_order < MAX_ORDER; current_order++) {
        area = &zone->free_area[current_order];
        if (list_empty(&area->free_list))
            continue;  /* 当前阶无空闲块,继续向上 */

        /* 取出第一个空闲块 */
        page = list_first_entry(&area->free_list, struct page, lru);
        list_del(&page->lru);
        area->nr_free--;

        /* 将多余部分分裂后放回低阶链表 */
        while (current_order > order) {
            current_order--;
            area = &zone->free_area[current_order];
            /* 伙伴块地址 = 当前块地址 + 2^current_order 页 */
            struct page *buddy = page + (1 << current_order);
            list_add(&buddy->lru, &area->free_list);
            area->nr_free++;
        }
        return page;
    }
    return NULL;  /* 内存不足 */
}

2.4 内存块合并规则

当内存块被释放时,内核尝试将其与伙伴块合并(coalesce),以构建更大的连续内存区域。合并是分裂的逆操作,遵循以下规则:

合并流程(自底向上):
  1. 计算伙伴地址:根据释放块的物理地址和阶 order,通过异或运算 buddy_pfn = pfn ^ (1 << order) 得到伙伴块的页框号。
  2. 检查伙伴状态:确认伙伴块是否空闲且位于同一个管理区(zone),并且未被预留或隔离。
  3. 从链表摘除:若伙伴空闲,将其从 free_area[order] 链表中移除。
  4. 合并成大块:将释放块与伙伴块合并,形成阶为 order+1 的新块。
  5. 递归尝试:以新块为起点,继续尝试向更高阶合并,直到无法合并为止。
  6. 插入最终链表:将合并后的最终块插入对应阶的空闲链表。
合并终止条件:当伙伴块不空闲、伙伴块不属于同一管理区、或合并后的块达到 MAX_ORDER-1 时,停止合并。MAX_ORDER 通常为 11,对应最大连续块为 210 = 1024 页(4 MB)。

合并操作的核心代码片段:

/* 伙伴系统合并核心逻辑(简化) */
static inline unsigned long
__free_one_page(struct page *page, unsigned long pfn,
                struct zone *zone, unsigned int order)
{
    unsigned long buddy_pfn;
    struct page *buddy;

    while (order < MAX_ORDER - 1) {
        /* 计算伙伴页框号:关键位运算 */
        buddy_pfn = pfn ^ (1 << order);
        buddy = page + (buddy_pfn - pfn);

        /* 检查伙伴是否可合并 */
        if (!page_is_buddy(page, buddy, order))
            break;  /* 伙伴不满足条件,停止合并 */

        /* 从空闲链表摘除伙伴 */
        list_del(&buddy->lru);
        zone->free_area[order].nr_free--;

        /* 合并:取两个伙伴中地址较小的作为新块起始 */
        if (buddy_pfn < pfn) {
            page = buddy;
            pfn = buddy_pfn;
        }
        order++;
    }

    /* 将合并后的块放入对应阶的空闲链表 */
    list_add(&page->lru, &zone->free_area[order].free_list);
    zone->free_area[order].nr_free++;

    return order;
}

2.5 分裂与合并的完整流程可视化

下图展示了一个从 order=2 分配 4 页,以及后续释放并触发合并的完整生命周期:

伙伴系统分裂与合并生命周期 ① 初始:order=2 空闲块 (16页) 连续 16 页物理内存 (order=2) ② 分裂:分配 4 页 (order=0),剩余块放回低阶链表 已分配 (order=0) 空闲 (order=0) 空闲 (order=1, 8页) ③ 释放:释放 order=0 块,触发合并 合并为 order=1 (8页) 空闲 (order=1, 8页) ④ 继续合并:两个 order=1 伙伴合并为 order=2 恢复为连续 16 页物理内存 (order=2) 关键观察 分裂时“先分配、后放回”;合并时“先摘伙伴、后插入”。 伙伴地址通过异或运算 O(1) 计算,无需遍历树结构。

2.6 伙伴系统的核心优势与局限

维度 优势 局限
分配速度 分裂过程是 O(log N),最坏情况从 MAX_ORDER-1 分裂到 0 小对象(< PAGE_SIZE)分配浪费严重,需配合 slab 分配器
释放速度 合并过程也是 O(log N),且伙伴查找通过位运算 O(1) 完成 频繁分配释放可能导致高阶块稀缺,触发 compaction
外部碎片 合并机制能有效抑制外部碎片,回收大块连续内存 无法解决内部碎片(分配 2order 页但只用部分)
实现复杂度 数据结构简单(数组+链表),代码路径清晰 对实时性要求高的场景,分裂/合并的锁竞争可能成为瓶颈
总结:伙伴系统的“伙伴定义”通过严格的地址对齐和大小相等约束,保证了合并的正确性;“阶”的概念将内存块大小量化为 2 的幂次,配合隐式二叉树结构,使分裂与合并操作都能在 O(log N) 时间内完成。这一设计是 Linux 内核在内存分配效率与碎片控制之间取得的精妙平衡。