07 - Buddy释放与合并算法

难度 : 🔴 困难级
预计学习时间 : 80分钟
前置知识 : 06-Buddy分配算法


📋 概述

释放和合并是Buddy算法的另一半核心:

  • 🔄 自动合并: 释放时自动与伙伴块合并,减少碎片
  • 🌲 递归向上: 合并后继续向上检查父块
  • 🎯 伙伴判定: 只有互为伙伴且都空闲才能合并
  • 🔐 清零保持: 合并时维护清零标志的一致性
  • O(log n): 最多合并max_order次

本章深入分析Buddy释放和合并算法的实现细节。


7.1 释放流程概述

主释放函数

c 复制代码
// drivers/gpu/drm/drm_buddy.c

/**
 * drm_buddy_free_block - 释放单个块
 *
 * @mm: buddy管理器
 * @block: 要释放的块
 */
void drm_buddy_free_block(struct drm_buddy *mm,
                         struct drm_buddy_block *block)
{
    BUG_ON(!drm_buddy_block_is_allocated(block));
    
    // 1. 更新可用空间统计
    mm->avail += drm_buddy_block_size(mm, block);
    
    // 2. 更新清零空间统计
    if (drm_buddy_block_is_clear(block))
        mm->clear_avail += drm_buddy_block_size(mm, block);
    
    // 3. 执行释放和合并
    __drm_buddy_free(mm, block, false);
}
EXPORT_SYMBOL(drm_buddy_free_block);

/**
 * drm_buddy_free_list - 批量释放
 *
 * @mm: buddy管理器
 * @objects: 要释放的块链表
 * @flags: 标志 (DRM_BUDDY_CLEARED)
 */
void drm_buddy_free_list(struct drm_buddy *mm,
                        struct list_head *objects,
                        unsigned int flags)
{
    struct drm_buddy_block *block, *on;
    bool mark_clear = flags & DRM_BUDDY_CLEARED;
    
    list_for_each_entry_safe(block, on, objects, link) {
        // 标记清零状态
        if (mark_clear)
            mark_cleared(block);
        else
            clear_reset(block);
        
        // 释放单个块
        drm_buddy_free_block(mm, block);
        
        cond_resched();  // 避免长时间占用CPU
    }
    
    INIT_LIST_HEAD(objects);
}
EXPORT_SYMBOL(drm_buddy_free_list);

释放流程图

复制代码
drm_buddy_free_block(block)
    |
    v
+----------------------------+
| 1. check state             |
|    BUG_ON(!ALLOCATED)      |
+-------------+--------------+
              |
              v
+----------------------------+
| 2. update stats            |
|    mm->avail += size       |
|    mm->clear_avail += size |
+-------------+--------------+
              |
              v
    __drm_buddy_free(block)
              |
              v
+----------------------------+
| 3. find buddy              |
|    buddy = __get_buddy()   |
+-------------+--------------+
              |
              v
       +--------------+
       | buddy free?  |
       +--+--------+--+
       no |        | yes
          v        v
    +---------+  +-----------------+
    | mark    |  | merge buddy     |
    | FREE    |  | remove buddy    |
    | done    |  | free block      |
    +---------+  | free buddy      |
                 | block = parent  |
                 +--------+--------+
                          |
                          +-- back to step 3 (recurse)

7.2 查找伙伴块

__get_buddy 函数

c 复制代码
// drivers/gpu/drm/drm_buddy.c

static struct drm_buddy_block *
__get_buddy(struct drm_buddy_block *block)
{
    struct drm_buddy_block *parent;
    
    parent = block->parent;
    if (!parent)
        return NULL;  // 根节点没有伙伴
    
    // 如果block是左子块,伙伴是右子块
    if (parent->left == block)
        return parent->right;
    
    // 否则block是右子块,伙伴是左子块
    return parent->left;
}

// 公开接口
struct drm_buddy_block *
drm_get_buddy(struct drm_buddy_block *block)
{
    return __get_buddy(block);
}
EXPORT_SYMBOL(drm_get_buddy);

伙伴关系示例

复制代码
二叉树结构:
        [32KB, SPLIT]
          /        \
    [16KB, FREE]  [16KB, ALLOCATED]
      左块            右块

查找伙伴:
- 对于左块 (16KB):
  parent = [32KB, SPLIT]
  parent->left == block ✓
  返回 parent->right (右块)

- 对于右块 (16KB):
  parent = [32KB, SPLIT]
  parent->left != block
  返回 parent->left (左块)

- 对于根节点 (32KB):
  parent = NULL
  返回 NULL (根节点没有伙伴)

伙伴地址计算验证

c 复制代码
// 验证伙伴关系的数学方法

u64 get_buddy_offset(u64 offset, u64 size)
{
    // 异或操作:翻转size位
    return offset ^ size;
}

// 示例
offset = 0x0000, size = 16KB
buddy_offset = 0x0000 ^ 0x4000 = 0x4000  ← 右伙伴

offset = 0x4000, size = 16KB
buddy_offset = 0x4000 ^ 0x4000 = 0x0000  ← 左伙伴

offset = 0x8000, size = 16KB
buddy_offset = 0x8000 ^ 0x4000 = 0xC000  ← 右伙伴

特性:
- 对称性: A的伙伴是B, B的伙伴是A
- 相邻性: 伙伴块地址相邻
- 对齐性: 伙伴对的起始地址是2*size的整数倍

7.3 合并条件判断

合并条件

c 复制代码
// 两个块能合并需要满足所有条件:

bool can_merge(struct drm_buddy_block *block)
{
    struct drm_buddy_block *parent, *buddy;
    
    // 条件1: 有父节点 (不是根)
    parent = block->parent;
    if (!parent)
        return false;
    
    // 条件2: 有伙伴块
    buddy = __get_buddy(block);
    if (!buddy)
        return false;
    
    // 条件3: 伙伴块是空闲的
    if (!drm_buddy_block_is_free(buddy))
        return false;
    
    // 条件4: 清零状态一致 (可选)
    if (!force_merge) {
        if (drm_buddy_block_is_clear(block) !=
            drm_buddy_block_is_clear(buddy))
            return false;
    }
    
    return true;
}

清零状态处理

复制代码
场景分析:

场景1: 两个块都已清零
[4KB, FREE, CLEAR] [4KB, FREE, CLEAR]
                ↓ 合并
        [8KB, FREE, CLEAR] ✓
        
场景2: 两个块都未清零
[4KB, FREE, !CLEAR] [4KB, FREE, !CLEAR]
                ↓ 合并
        [8KB, FREE, !CLEAR] ✓

场景3: 清零状态不一致
[4KB, FREE, CLEAR] [4KB, FREE, !CLEAR]
                ↓ 不合并 ✗
保持分离状态,避免清零信息丢失

原因:
- 如果合并,父块应该标记为CLEAR还是!CLEAR?
- 标记CLEAR: 错误,因为有部分未清零
- 标记!CLEAR: 浪费,因为有部分已清零
- 最佳策略: 不合并,保持细粒度信息

7.4 递归合并过程

__drm_buddy_free 函数

c 复制代码
// drivers/gpu/drm/drm_buddy.c

static unsigned int __drm_buddy_free(struct drm_buddy *mm,
                                    struct drm_buddy_block *block,
                                    bool force_merge)
{
    struct drm_buddy_block *parent;
    unsigned int order;
    
    // 递归向上合并
    while ((parent = block->parent)) {
        struct drm_buddy_block *buddy;
        
        // 1. 查找伙伴
        buddy = __get_buddy(block);
        
        // 2. 检查伙伴是否空闲
        if (!drm_buddy_block_is_free(buddy))
            break;  // 伙伴不空闲,停止合并
        
        // 3. 检查清零状态 (非强制合并模式)
        if (!force_merge) {
            if (drm_buddy_block_is_clear(block) !=
                drm_buddy_block_is_clear(buddy))
                break;  // 清零状态不一致,停止合并
            
            // 继承清零标志到父块
            if (drm_buddy_block_is_clear(block))
                mark_cleared(parent);
        }
        
        // 4. 从空闲列表移除伙伴
        list_del(&buddy->link);
        
        // 5. 更新清零统计 (强制合并模式)
        if (force_merge && drm_buddy_block_is_clear(buddy))
            mm->clear_avail -= drm_buddy_block_size(mm, buddy);
        
        // 6. 释放子块的内存
        drm_block_free(mm, block);
        drm_block_free(mm, buddy);
        
        // 7. 向上移动到父块
        block = parent;
    }
    
    // 8. 标记最终块为空闲
    order = drm_buddy_block_order(block);
    mark_free(mm, block);
    
    return order;
}

合并过程可视化

复制代码
初始状态: 释放4KB块 (order=0)

        [32KB, SPLIT]
          /        \
    [16KB, SPLIT] [16KB, FREE]
      /      \
  [8KB,   [8KB, SPLIT]
   FREE]    /      \
        [4KB,    [4KB, 
         FREE]    ALLOCATED] ← 要释放

步骤1: 释放4KB右块
        [32KB, SPLIT]
          /        \
    [16KB, SPLIT] [16KB, FREE]
      /      \
  [8KB,   [8KB, SPLIT]
   FREE]    /      \
        [4KB,    [4KB, 
         FREE]    FREE] ← 现在是FREE

步骤2: 检查伙伴 (4KB左块)
buddy = 4KB左块
drm_buddy_block_is_free(buddy) = true ✓
可以合并!

        [32KB, SPLIT]
          /        \
    [16KB, SPLIT] [16KB, FREE]
      /      \
  [8KB,   [8KB, FREE] ← 合并成8KB
   FREE]
           ↑ 删除两个4KB子块

步骤3: 向上移动,继续检查
block = 8KB右块
buddy = 8KB左块
drm_buddy_block_is_free(buddy) = true ✓
可以合并!

        [32KB, SPLIT]
          /        \
    [16KB, FREE]  [16KB, FREE] ← 合并成16KB左块
           ↑ 删除两个8KB子块

步骤4: 向上移动,继续检查
block = 16KB左块
buddy = 16KB右块
drm_buddy_block_is_free(buddy) = true ✓
可以合并!

        [32KB, FREE] ← 最终合并成32KB
              ↑ 删除两个16KB子块

步骤5: 向上移动
block = 32KB
parent = NULL (根节点)
停止合并

最终结果:
[32KB, FREE] - 进入 free_list[5]

递归深度

复制代码
最大递归深度 = max_order

例如: 8GB VRAM, chunk_size=4KB
max_order = log2(8GB / 4KB) = log2(2M) = 21

释放最小块 (4KB) 可能触发最深递归:
order 0 → 1 → 2 → ... → 20 → 21
共21次合并

每次合并:
- 查找伙伴: O(1)
- 检查状态: O(1)
- 删除链表: O(1)
- 释放内存: O(1)
总复杂度: O(max_order) = O(log n)

7.5 合并优化技术

延迟合并

c 复制代码
// 有些场景可能不希望立即合并

// 示例: 频繁分配释放小块
for (int i = 0; i < 1000; i++) {
    block = drm_buddy_alloc_blocks(mm, ..., 4KB, ...);
    use_block(block);
    drm_buddy_free_block(mm, block);  // 每次都合并?
}

// 问题:
// - 每次释放都可能触发多次合并
// - 下次分配又要分裂回来
// - 浪费CPU时间

// 优化策略 (未实现):
// - 延迟合并: 释放时先不合并,标记为"待合并"
// - 批量合并: 定期或内存压力时批量合并
// - 阈值控制: 只有空闲块数量超过阈值才合并

强制合并模式

c 复制代码
// force_merge = true 时的特殊行为

static int __force_merge(struct drm_buddy *mm,
                        u64 start,
                        u64 end,
                        unsigned int min_order)
{
    // 从小到大遍历空闲列表
    for (int i = min_order - 1; i >= 0; i--) {
        struct drm_buddy_block *block;
        
        list_for_each_entry_safe_reverse(block,
                                        &mm->free_list[i],
                                        link) {
            u64 block_start = drm_buddy_block_offset(block);
            u64 block_end = block_start +
                           drm_buddy_block_size(mm, block);
            
            // 只合并范围内的块
            if (!contains(start, end, block_start, block_end))
                continue;
            
            struct drm_buddy_block *buddy = __get_buddy(block);
            if (!drm_buddy_block_is_free(buddy))
                continue;
            
            // 强制合并,忽略清零状态
            __drm_buddy_free(mm, block, true);
        }
    }
    
    return 0;
}

// 使用场景:
// - 重置分配器状态
// - 清理碎片
// - 回收内存前的整理

合并统计

c 复制代码
// 追踪合并效果 (可选实现)

struct merge_stats {
    atomic64_t merge_count;      // 合并次数
    atomic64_t merge_depth;      // 平均合并深度
    atomic64_t blocks_merged;    // 合并的块数量
};

static void update_merge_stats(struct drm_buddy *mm,
                               unsigned int depth)
{
    atomic64_inc(&mm->stats.merge_count);
    atomic64_add(depth, &mm->stats.merge_depth);
    atomic64_add(1 << depth, &mm->stats.blocks_merged);
}

// 查询统计
u64 avg_merge_depth = atomic64_read(&mm->stats.merge_depth) /
                     atomic64_read(&mm->stats.merge_count);

printk("Average merge depth: %llu\n", avg_merge_depth);
printk("Total merges: %llu\n",
      atomic64_read(&mm->stats.merge_count));

💡 重点提示

  1. 递归合并: 释放一个块可能触发连锁合并,一直向上到根节点。

  2. 伙伴判定简单: 通过parent的left/right指针直接获取伙伴。

  3. 清零状态重要: 不一致的清零状态会阻止合并,保持信息精确性。

  4. 合并是自动的: 用户只需调用free,Buddy自动处理合并。

  5. 时间复杂度: O(log n),即使最坏情况也很快。

  6. 批量释放 : 使用drm_buddy_free_list()比多次调用free_block()更高效。


⚠️ 常见陷阱

陷阱1: "释放后块会立即消失"

  • ✅ 正确: 块可能被合并成更大的块,原block指针失效。

陷阱2: "所有相邻空闲块都会合并"

  • ✅ 正确: 只有互为伙伴的块才能合并,不是所有相邻块都是伙伴。

陷阱3: "合并总会到根节点"

  • ✅ 正确: 如果伙伴块不空闲,合并会提前停止。

陷阱4: "清零状态不重要"

  • ✅ 正确: 清零状态不一致会阻止合并,影响内存利用率。

📝 实践练习

  1. 手动模拟合并:
c 复制代码
   初始状态:
        [32KB, SPLIT]
          /        \
    [16KB, SPLIT]  [16KB, ALLOCATED]
      /      \
  [8KB, FREE] [8KB, SPLIT]
                /      \
            [4KB,      [4KB,
             ALLOCATED] FREE]

操作: 释放 [4KB, ALLOCATED]

问题:

  1. 合并会发生几次?

  2. 最终状态是什么?

  3. 哪些块被销毁?

  4. 哪些块进入空闲列表?

  5. 清零状态影响:

    场景A:

    4KB, FREE, CLEAR\] \[4KB, FREE, CLEAR

    释放后能合并吗?合并后父块清零标志是什么?

    场景B:

    4KB, FREE, !CLEAR\] \[4KB, FREE, CLEAR

    释放后能合并吗?为什么?

    场景C (force_merge=true):

    4KB, FREE, !CLEAR\] \[4KB, FREE, CLEAR

    强制合并后父块清零标志是什么?

  6. 优化分析:

c 复制代码
   // 两种释放模式

   // 模式A: 逐个释放
   for (int i = 0; i < 100; i++) {
       drm_buddy_free_block(mm, blocks[i]);
   }

   // 模式B: 批量释放
   drm_buddy_free_list(mm, &block_list, 0);

问题:

  • 哪种模式更快?为什么?
  • 合并效果有区别吗?
  • 何时应该用哪种模式?

📚 本章小结

  • 释放入口 : drm_buddy_free_block()单个块,drm_buddy_free_list()批量
  • 查找伙伴: 通过parent指针的left/right快速定位
  • 合并条件: 伙伴空闲 + 清零状态一致
  • 递归合并: 自底向上递归合并,直到无法继续
  • 时间复杂度: O(log n),最多合并max_order次
  • 优化技术: 强制合并、批量释放、合并统计

Buddy的释放和合并算法通过自动的递归合并,有效减少了外部碎片,是算法高效的关键。


➡️ 下一步

理解了释放和合并算法后,下一章将详细分析块的分裂过程和二叉树的构建。

👉 [08-块的分裂与重组.md]审核中...


相关推荐
DeeplyMind4 天前
05 - AMDGPU中的VRAM管理器
drm·amdgpu·drm_buddy·ttm
DeeplyMind7 天前
04 - 核心数据结构详解
drm·drm_buddy·vram
DeeplyMind9 天前
03 - DRM子系统与AMDGPU架构
drm·drm_buddy·vram分配
DeeplyMind3 个月前
Linux DRM 内存管理子系统的概念关系理解:gem、ttm、drm_buddy
drm·tm·drm_buddy