难度 : 🔴 困难级
预计学习时间 : 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));
💡 重点提示
-
递归合并: 释放一个块可能触发连锁合并,一直向上到根节点。
-
伙伴判定简单: 通过parent的left/right指针直接获取伙伴。
-
清零状态重要: 不一致的清零状态会阻止合并,保持信息精确性。
-
合并是自动的: 用户只需调用free,Buddy自动处理合并。
-
时间复杂度: O(log n),即使最坏情况也很快。
-
批量释放 : 使用
drm_buddy_free_list()比多次调用free_block()更高效。
⚠️ 常见陷阱
❌ 陷阱1: "释放后块会立即消失"
- ✅ 正确: 块可能被合并成更大的块,原block指针失效。
❌ 陷阱2: "所有相邻空闲块都会合并"
- ✅ 正确: 只有互为伙伴的块才能合并,不是所有相邻块都是伙伴。
❌ 陷阱3: "合并总会到根节点"
- ✅ 正确: 如果伙伴块不空闲,合并会提前停止。
❌ 陷阱4: "清零状态不重要"
- ✅ 正确: 清零状态不一致会阻止合并,影响内存利用率。
📝 实践练习
- 手动模拟合并:
c
初始状态:
[32KB, SPLIT]
/ \
[16KB, SPLIT] [16KB, ALLOCATED]
/ \
[8KB, FREE] [8KB, SPLIT]
/ \
[4KB, [4KB,
ALLOCATED] FREE]
操作: 释放 [4KB, ALLOCATED]
问题:
-
合并会发生几次?
-
最终状态是什么?
-
哪些块被销毁?
-
哪些块进入空闲列表?
-
清零状态影响:
场景A:
4KB, FREE, CLEAR\] \[4KB, FREE, CLEAR
释放后能合并吗?合并后父块清零标志是什么?
场景B:
4KB, FREE, !CLEAR\] \[4KB, FREE, CLEAR
释放后能合并吗?为什么?
场景C (force_merge=true):
4KB, FREE, !CLEAR\] \[4KB, FREE, CLEAR
强制合并后父块清零标志是什么?
-
优化分析:
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]审核中...