1. 概述与作用
1.1 基本定位
drm_mm 是 Linux DRM子系统提供的通用范围分配器(Range Allocator),用于管理连续的地址空间。它是一个轻量级的内存管理基础设施,被广泛用于 GPU 驱动中管理各种地址空间资源。
核心特点(源自 drivers/gpu/drm/drm_mm.c):
“Generic simple memory manager implementation. Intended to be used as a base class implementation for more advanced memory managers.”
1.2 设计目标
根据源码文档,drm_mm 的设计目标包括:
- 简单高效:提供基础的范围分配功能,算法简单但实用
- 可扩展性:作为基础类,可被更复杂的内存管理器(如 TTM)扩展
- GPU 特化:针对 GPU 的特殊需求进行优化,如支持对齐、颜色标记等
- 零开销抽象:不做实际物理内存分配,由驱动管理实际的数据结构嵌入
1.3 主要应用场景
drm_mm 在 GPU 驱动中广泛应用于以下场景:
| 应用场景 | 管理对象 | 典型用途 |
|---|---|---|
| VRAM 管理 | 显存物理地址空间 | 分配 GPU 本地显存(VRAM)区域 |
| GTT/GART 管理 | GTT 地址空间 | 管理 Graphics Translation Table 地址 |
| 地址空间映射 | 虚拟地址偏移 | drm_vma_offset_manager 底层实现 |
| 保留区域 | 固件预留内存 | 保留 BIOS/固件使用的内存区域 |
2. 核心数据结构
2.1 struct drm_mm
/**
* struct drm_mm - DRM allocator
*
* DRM range allocator with a few special functions and features geared towards
* managing GPU memory.
*/
struct drm_mm {
/**
* @color_adjust:
* 可选的驱动回调,用于进一步限制 hole 的使用范围
* 例如:在不同缓存域之间插入 guard pages
*/
void (*color_adjust)(const struct drm_mm_node *node,
unsigned long color,
u64 *start, u64 *end);
/* private: */
struct list_head hole_stack; /* 最近释放的 hole 栈 */
struct drm_mm_node head_node; /* 哨兵节点,标记范围末尾 */
struct rb_root_cached interval_tree; /* 区间树,按地址组织已分配节点 */
struct rb_root_cached holes_size; /* 按 hole 大小组织的红黑树 */
struct rb_root holes_addr; /* 按 hole 地址组织的红黑树 */
unsigned long scan_active; /* 驱逐扫描状态标志 */
};
关键字段说明:
-
interval_tree:使用区间树(Interval Tree)存储所有已分配的节点
- 支持快速查找某个地址范围内的节点
- 基于 Linux 内核的
INTERVAL_TREE_DEFINE宏实现 - 时间复杂度:O(log n + m),m 为重叠节点数
-
holes_size:按 hole 大小组织的缓存红黑树
- 用于
DRM_MM_INSERT_BEST策略(最佳适配) - 最大的 hole 位于树的最左侧
- 时间复杂度:O(log n)
- 用于
-
holes_addr:按 hole 地址组织的增强红黑树
- 用于
DRM_MM_INSERT_LOW和DRM_MM_INSERT_HIGH策略 - 使用
subtree_max_hole进行增强,支持快速查找最大 hole - 时间复杂度:O(log n)
- 用于
-
hole_stack:最近释放的 hole 链表栈
- 用于
DRM_MM_INSERT_EVICT策略 - 优化局部性,减少碎片化
- 访问复杂度:O(num_holes)
- 用于
-
head_node:哨兵节点技巧
- 起始地址:
start + size(范围末尾) - 大小:
-size(负数) - 作用:简化边界处理,避免空指针检查
- 起始地址:
核心优势:
- O(log n) 查找复杂度:使用红黑树和区间树优化查找性能
- O(1) 删除复杂度:节点删除操作非常快速
- 灵活的分配策略:支持最佳适配、低地址优先、高地址优先等多种策略
- 碎片管理:维护最近释放的 hole 栈,减少碎片化
- 无锁设计:由调用者控制同步,避免内部锁开销
- 调试支持:提供栈跟踪、泄漏检测等调试功能
2.2 struct drm_mm_node:分配节点
/**
* struct drm_mm_node - allocated block in the DRM allocator
*
* This represents an allocated block in a &drm_mm allocator.
* Drivers can embed this structure into their own data structures.
*/
struct drm_mm_node {
unsigned long color; /* 不透明的驱动私有标签 */
u64 start; /* 起始地址 */
u64 size; /* 大小 */
/* private: */
struct drm_mm *mm; /* 所属的分配器 */
struct list_head node_list; /* 按地址顺序的链表 */
struct list_head hole_stack; /* hole 栈链表 */
struct rb_node rb; /* 区间树节点 */
struct rb_node rb_hole_size; /* hole 大小树节点 */
struct rb_node rb_hole_addr; /* hole 地址树节点 */
u64 __subtree_last; /* 区间树增强字段 */
u64 hole_size; /* 后续 hole 的大小 */
u64 subtree_max_hole; /* 子树中最大 hole */
unsigned long flags; /* 状态标志 */
#define DRM_MM_NODE_ALLOCATED_BIT 0
#define DRM_MM_NODE_SCANNED_BIT 1
#ifdef CONFIG_DRM_DEBUG_MM
depot_stack_handle_t stack; /* 调试:分配栈跟踪 */
#endif
};
关键字段说明:
-
color:颜色标记
- 用途:标识不兼容的内存域(如不同的缓存策略)
- 示例:Intel i915 驱动用它在不同缓存域之间插入 guard pages
- 配合
color_adjust回调使用
-
hole_size 和 subtree_max_hole:
hole_size:当前节点后面紧跟的空闲空间大小subtree_max_hole:子树中最大的 hole 大小(用于优化搜索)- 只有后面有 hole 的节点才会被加入 hole 相关的数据结构
-
flags:状态位
DRM_MM_NODE_ALLOCATED_BIT:节点是否已分配DRM_MM_NODE_SCANNED_BIT:用于驱逐扫描(eviction scan)
2.3 enum drm_mm_insert_mode:分配策略
enum drm_mm_insert_mode {
DRM_MM_INSERT_BEST = 0, /* 最佳适配:选择最小的 hole */
DRM_MM_INSERT_LOW, /* 低地址优先:选择地址最低的 hole */
DRM_MM_INSERT_HIGH, /* 高地址优先:选择地址最高的 hole */
DRM_MM_INSERT_EVICT, /* 驱逐优先:选择最近释放的 hole */
DRM_MM_INSERT_ONCE = BIT(31), /* 只检查一个 hole,快速失败 */
/* 组合策略 */
DRM_MM_INSERT_HIGHEST = DRM_MM_INSERT_HIGH | DRM_MM_INSERT_ONCE,
DRM_MM_INSERT_LOWEST = DRM_MM_INSERT_LOW | DRM_MM_INSERT_ONCE,
};
策略详解:
| 策略 | 搜索方式 | 分配位置 | 适用场景 |
|---|---|---|---|
| BEST | 遍历所有 hole,选最小的 | 从 hole 底部分配 | 减少碎片化,通用场景 |
| LOW | 按地址从低到高搜索 | 从 hole 底部分配 | 需要低地址的资源(如 32 位设备) |
| HIGH | 按地址从高到低搜索 | 从 hole 顶部分配 | 避免低地址区域,减少碰撞 |
| EVICT | 从最近释放的 hole 开始 | 从 hole 底部分配 | 驱逐后立即重新分配,提高局部性 |
| ONCE | 只检查一个候选 hole | 取决于组合策略 | 快速失败,避免长时间搜索 |
注意:DRM_MM_INSERT_HIGH 从 hole 顶部分配,这与其他策略不同,原因是为了在高地址区域紧密排列节点。
3. 核心接口函数实现与功能
3.1 初始化与清理
3.1.1 drm_mm_init()
函数原型:
void drm_mm_init(struct drm_mm *mm, u64 start, u64 size);
功能:初始化 drm_mm 分配器,管理 [start, start+size) 范围。
实现分析(源自 drivers/gpu/drm/drm_mm.c):
void drm_mm_init(struct drm_mm *mm, u64 start, u64 size)
{
DRM_MM_BUG_ON(start + size <= start); /* 溢出检查 */
mm->color_adjust = NULL;
/* 初始化各种数据结构 */
INIT_LIST_HEAD(&mm->hole_stack);
mm->interval_tree = RB_ROOT_CACHED;
mm->holes_size = RB_ROOT_CACHED;
mm->holes_addr = RB_ROOT;
/* 关键技巧:使用哨兵节点避免特殊情况处理 */
INIT_LIST_HEAD(&mm->head_node.node_list);
mm->head_node.flags = 0;
mm->head_node.mm = mm;
mm->head_node.start = start + size; /* 末尾位置 */
mm->head_node.size = -size; /* 负数标记 */
add_hole(&mm->head_node); /* 将整个范围标记为 hole */
mm->scan_active = 0;
#ifdef CONFIG_DRM_DEBUG_MM
stack_depot_init();
#endif
}
关键设计:
- 哨兵节点技巧:将整个范围表示为哨兵节点前的一个大 hole
- 初始状态:整个范围都是空闲的,作为一个 hole 挂在
head_node上 - 无内存分配:只初始化已有结构,不分配额外内存
使用示例:
struct drm_mm mm;
memset(&mm, 0, sizeof(mm));
drm_mm_init(&mm, 0, 0x100000000ULL); /* 管理 4GB 地址空间 */
3.1.2 drm_mm_takedown()
函数原型:
void drm_mm_takedown(struct drm_mm *mm);
功能:清理 drm_mm 分配器,检查是否有泄漏。
实现:
void drm_mm_takedown(struct drm_mm *mm)
{
if (WARN(!drm_mm_clean(mm),
"Memory manager not clean during takedown.\n"))
show_leaks(mm); /* 调试模式下显示泄漏的节点 */
}
注意:调用此函数前,必须确保所有节点都已释放(drm_mm_clean(mm) 返回 true)。
3.2 分配与释放
3.2.1 drm_mm_insert_node_in_range()
函数原型:
int drm_mm_insert_node_in_range(struct drm_mm * const mm,
struct drm_mm_node * const node,
u64 size, u64 alignment,
unsigned long color,
u64 range_start, u64 range_end,
enum drm_mm_insert_mode mode);
功能:在指定范围内搜索合适的 hole 并插入节点。
参数说明:
mm:分配器对象node:预分配的节点结构(必须清零)size:请求的大小alignment:对齐要求(0 或 1 表示无对齐)color:颜色标记,用于驱动特定的限制range_start/range_end:允许的地址范围mode:分配策略
返回值:
0:成功-ENOSPC:没有足够的空间
实现流程分析(源自 drivers/gpu/drm/drm_mm.c:514):
int drm_mm_insert_node_in_range(...)
{
struct drm_mm_node *hole;
u64 remainder_mask;
bool once;
/* 1. 基本检查 */
DRM_MM_BUG_ON(range_start > range_end);
if (unlikely(size == 0 || range_end - range_start < size))
return -ENOSPC;
/* 快速失败:如果最大 hole 都不够大 */
if (rb_to_hole_size_or_zero(rb_first_cached(&mm->holes_size)) < size)
return -ENOSPC;
/* 2. 处理对齐 */
if (alignment <= 1)
alignment = 0;
once = mode & DRM_MM_INSERT_ONCE;
mode &= ~DRM_MM_INSERT_ONCE;
remainder_mask = is_power_of_2(alignment) ? alignment - 1 : 0;
/* 3. 遍历 hole */
for (hole = first_hole(mm, range_start, range_end, size, mode);
hole;
hole = once ? NULL : next_hole(mm, hole, size, mode)) {
u64 hole_start = __drm_mm_hole_node_start(hole);
u64 hole_end = hole_start + hole->hole_size;
u64 adj_start, adj_end;
u64 col_start, col_end;
/* 3.1 策略相关的早期退出 */
if (mode == DRM_MM_INSERT_LOW && hole_start >= range_end)
break;
if (mode == DRM_MM_INSERT_HIGH && hole_end <= range_start)
break;
/* 3.2 应用 color_adjust 回调 */
col_start = hole_start;
col_end = hole_end;
if (mm->color_adjust)
mm->color_adjust(hole, color, &col_start, &col_end);
/* 3.3 计算调整后的范围 */
adj_start = max(col_start, range_start);
adj_end = min(col_end, range_end);
if (adj_end <= adj_start || adj_end - adj_start < size)
continue;
/* 3.4 高地址分配从顶部开始 */
if (mode == DRM_MM_INSERT_HIGH)
adj_start = adj_end - size;
/* 3.5 处理对齐 */
if (alignment) {
u64 rem;
if (likely(remainder_mask))
rem = adj_start & remainder_mask;
else
div64_u64_rem(adj_start, alignment, &rem);
if (rem) {
adj_start -= rem;
if (mode != DRM_MM_INSERT_HIGH)
adj_start += alignment;
/* 重新检查边界 */
if (adj_start < max(col_start, range_start) ||
min(col_end, range_end) - adj_start < size)
continue;
}
}
/* 4. 找到合适的位置,插入节点 */
node->mm = mm;
node->size = size;
node->start = adj_start;
node->color = color;
node->hole_size = 0;
__set_bit(DRM_MM_NODE_ALLOCATED_BIT, &node->flags);
list_add(&node->node_list, &hole->node_list);
drm_mm_interval_tree_add_node(hole, node);
/* 5. 更新 hole 结构 */
rm_hole(hole); /* 移除原 hole */
if (adj_start > hole_start)
add_hole(hole); /* 前面有剩余空间 */
if (adj_start + size < hole_end)
add_hole(node); /* 后面有剩余空间 */
save_stack(node); /* 调试:保存分配栈 */
return 0;
}
return -ENOSPC;
}
关键点:
- 快速失败优化:先检查最大 hole,避免无谓遍历
- color_adjust 回调:允许驱动插入 guard pages 或其他限制
- 对齐处理优化:对 2 的幂次对齐使用位运算
- hole 分裂:分配后可能产生 0、1 或 2 个新 hole
- 区间树更新:使用增强红黑树高效维护区间
便捷包装函数:
/* 在整个范围内分配,无特殊限制 */
static inline int drm_mm_insert_node(struct drm_mm *mm,
struct drm_mm_node *node,
u64 size)
{
return drm_mm_insert_node_in_range(mm, node, size, 0, 0,
0, U64_MAX,
DRM_MM_INSERT_BEST);
}
/* 在整个范围内分配,指定对齐 */
static inline int drm_mm_insert_node_generic(struct drm_mm *mm,
struct drm_mm_node *node,
u64 size, u64 alignment,
unsigned long color,
enum drm_mm_insert_mode mode)
{
return drm_mm_insert_node_in_range(mm, node, size, alignment, color,
0, U64_MAX, mode);
}
3.2.2 drm_mm_remove_node()
函数原型:
void drm_mm_remove_node(struct drm_mm_node *node);
功能:从分配器中移除节点,释放其占用的地址空间。
实现分析(源自 drivers/gpu/drm/drm_mm.c:627):
void drm_mm_remove_node(struct drm_mm_node *node)
{
struct drm_mm *mm = node->mm;
struct drm_mm_node *prev_node;
DRM_MM_BUG_ON(!drm_mm_node_allocated(node));
DRM_MM_BUG_ON(drm_mm_node_scanned_block(node));
prev_node = list_prev_entry(node, node_list);
/* 1. 移除当前节点可能携带的 hole */
if (drm_mm_hole_follows(node))
rm_hole(node);
/* 2. 从区间树和链表中移除 */
drm_mm_interval_tree_remove(node, &mm->interval_tree);
list_del(&node->node_list);
/* 3. 合并 hole:移除前一个节点的 hole,重新添加合并后的 hole */
if (drm_mm_hole_follows(prev_node))
rm_hole(prev_node);
add_hole(prev_node);
clear_bit_unlock(DRM_MM_NODE_ALLOCATED_BIT, &node->flags);
}
关键逻辑:
- hole 合并:删除节点后,前后的空闲空间会自动合并
- O(1) 复杂度:主要操作是链表和红黑树的节点删除
- 无需手动清理节点:节点可以立即重新用于下一次分配
示例:
struct drm_mm_node node;
memset(&node, 0, sizeof(node));
/* 分配 */
ret = drm_mm_insert_node(&mm, &node, 0x1000);
if (ret == 0) {
/* 使用节点:node.start 是分配的地址 */
printk("Allocated at 0x%llx\n", node.start);
/* 释放 */
drm_mm_remove_node(&node);
}
3.2.3 drm_mm_reserve_node()
函数原型:
int drm_mm_reserve_node(struct drm_mm *mm, struct drm_mm_node *node);
功能:在分配器中保留一个预定义位置的节点(节点的 start 和 size 必须预设)。
应用场景:
- 保留固件/BIOS 使用的内存区域
- 保留已知的硬件保留区域
- 初始化时接管现有的内存布局
实现特点:
int drm_mm_reserve_node(struct drm_mm *mm, struct drm_mm_node *node)
{
u64 end = node->start + node->size;
struct drm_mm_node *hole;
u64 hole_start, hole_end;
/* 1. 查找包含该范围的 hole */
/* 2. 验证范围完全在 hole 内 */
/* 3. 像普通分配一样插入节点 */
/* 4. 更新 hole 结构 */
return 0; /* 成功 */
}
示例:
struct drm_mm_node reserved;
memset(&reserved, 0, sizeof(reserved));
reserved.start = 0x0; /* 固件使用的地址 */
reserved.size = 0x100000; /* 1MB */
reserved.color = 0;
ret = drm_mm_reserve_node(&mm, &reserved);
if (ret == 0) {
printk("Reserved 0x%llx-0x%llx for firmware\n",
reserved.start, reserved.start + reserved.size);
}
3.3 查询与遍历
3.3.1 drm_mm_node_allocated()
函数原型:
static inline bool drm_mm_node_allocated(const struct drm_mm_node *node);
功能:检查节点是否已分配。
实现:
static inline bool drm_mm_node_allocated(const struct drm_mm_node *node)
{
return test_bit(DRM_MM_NODE_ALLOCATED_BIT, &node->flags);
}
重要性:节点必须在使用前清零,此函数用于验证节点状态。
3.3.2 drm_mm_initialized()
函数原型:
static inline bool drm_mm_initialized(const struct drm_mm *mm);
功能:检查分配器是否已初始化。
实现:
static inline bool drm_mm_initialized(const struct drm_mm *mm)
{
return READ_ONCE(mm->hole_stack.next);
}
3.3.3 drm_mm_hole_follows()
函数原型:
static inline bool drm_mm_hole_follows(const struct drm_mm_node *node);
功能:检查节点后面是否紧跟一个 hole。
实现:
static inline bool drm_mm_hole_follows(const struct drm_mm_node *node)
{
return node->hole_size;
}
用途:遍历时判断是否需要处理 hole。
3.3.4 遍历宏
drm_mm_for_each_node:遍历所有已分配节点
#define drm_mm_for_each_node(entry, mm) \
list_for_each_entry(entry, drm_mm_nodes(mm), node_list)
drm_mm_for_each_hole:遍历所有 hole
#define drm_mm_for_each_hole(pos, mm, hole_start, hole_end) \
drm_mm_for_each_node(pos, mm) \
for_each_if(drm_mm_hole_follows(pos) && \
(hole_start = drm_mm_hole_node_start(pos)) && \
(hole_end = hole_start + pos->hole_size))
示例:
struct drm_mm_node *node;
u64 hole_start, hole_end;
/* 遍历已分配节点 */
drm_mm_for_each_node(node, &mm) {
printk("Node: 0x%llx-0x%llx\n", node->start, node->start + node->size);
}
/* 遍历 hole */
drm_mm_for_each_hole(node, &mm, hole_start, hole_end) {
printk("Hole: 0x%llx-0x%llx (size: 0x%llx)\n",
hole_start, hole_end, hole_end - hole_start);
}
3.4 高级功能
3.4.1 驱逐扫描(Eviction Scan)
用途:当内存不足时,找出需要驱逐的节点以腾出空间。
核心函数:
-
drm_mm_scan_init():初始化扫描上下文
void drm_mm_scan_init(struct drm_mm_scan *scan, struct drm_mm *mm, u64 size, u64 alignment, unsigned long color, u64 range_start, u64 range_end, enum drm_mm_insert_mode mode); -
drm_mm_scan_add_block():将候选节点添加到扫描
bool drm_mm_scan_add_block(struct drm_mm_scan *scan, struct drm_mm_node *node);返回
true表示找到足够的空间。 -
drm_mm_scan_remove_block():移除不需要驱逐的节点
bool drm_mm_scan_remove_block(struct drm_mm_scan *scan, struct drm_mm_node *node);
工作流程:
struct drm_mm_scan scan;
struct drm_mm_node *node, *tmp;
/* 1. 初始化扫描 */
drm_mm_scan_init(&scan, &mm, size, alignment, 0,
0, U64_MAX, DRM_MM_INSERT_EVICT);
/* 2. 添加候选节点直到找到足够空间 */
drm_mm_for_each_node(node, &mm) {
if (drm_mm_scan_add_block(&scan, node))
break; /* 找到了 */
}
/* 3. 驱逐标记的节点 */
list_for_each_entry_safe(node, tmp, &evict_list, evict_link) {
drm_mm_remove_node(node);
/* 驱动特定的驱逐逻辑 */
}
/* 4. 现在可以分配了 */
ret = drm_mm_insert_node(&mm, &new_node, size);
3.4.2 Color Adjust 回调
函数签名:
void (*color_adjust)(const struct drm_mm_node *node,
unsigned long color,
u64 *start, u64 *end);
功能:允许驱动在不同颜色的节点之间插入 guard pages 或其他限制。
典型用途:Intel i915 驱动用于在不同缓存域之间插入 guard pages:
static void i915_mm_color_adjust(const struct drm_mm_node *node,
unsigned long color,
u64 *start, u64 *end)
{
/* 如果相邻节点的缓存属性不同,插入 guard page */
if (node->allocated && node->color != color) {
*start += I915_GUARD_PAGE_SIZE;
}
/* 检查下一个节点 */
struct drm_mm_node *next = list_next_entry(node, node_list);
if (next->allocated && next->color != color) {
*end -= I915_GUARD_PAGE_SIZE;
}
}
设置回调:
mm.color_adjust = i915_mm_color_adjust;
3.5 调试与统计
3.5.1 drm_mm_print()
函数原型:
void drm_mm_print(const struct drm_mm *mm, struct drm_printer *p);
功能:打印分配器的完整状态,包括所有已分配节点和 hole。
输出示例:
0x0000000000000000-0x0000000000100000: 1048576: free
0x0000000000100000-0x0000000000200000: 1048576: used
0x0000000000200000-0x0000000000400000: 2097152: free
total: 4194304, used 1048576 free 3145728
使用示例:
struct drm_printer p = drm_info_printer(dev->dev);
drm_mm_print(&mm, &p);
3.5.2 调试模式(CONFIG_DRM_DEBUG_MM)
启用此选项后,drm_mm 会:
-
栈跟踪:记录每个节点的分配调用栈
static noinline void save_stack(struct drm_mm_node *node) { unsigned long entries[STACKDEPTH]; unsigned int n; n = stack_trace_save(entries, ARRAY_SIZE(entries), 1); node->stack = stack_depot_save(entries, n, GFP_NOWAIT); } -
泄漏检测:在
drm_mm_takedown()时显示未释放的节点static void show_leaks(struct drm_mm *mm) { struct drm_mm_node *node; char *buf = kmalloc(BUFSZ, GFP_KERNEL); list_for_each_entry(node, drm_mm_nodes(mm), node_list) { if (!node->stack) { DRM_ERROR("node [%08llx + %08llx]: unknown owner\n", node->start, node->size); continue; } stack_depot_snprint(node->stack, buf, BUFSZ, 0); DRM_ERROR("node [%08llx + %08llx]: inserted at\n%s", node->start, node->size, buf); } kfree(buf); } -
BUG_ON 检查:严格的断言检查
#ifdef CONFIG_DRM_DEBUG_MM #define DRM_MM_BUG_ON(expr) BUG_ON(expr) #else #define DRM_MM_BUG_ON(expr) BUILD_BUG_ON_INVALID(expr) #endif
4. AMDGPU 驱动中的应用案例
4.1 GTT(Graphics Translation Table)管理
AMDGPU 驱动使用 drm_mm 管理 GTT 地址空间。GTT 是 GPU 用来访问系统内存的地址转换表。
4.1.1 数据结构
源文件:drivers/gpu/drm/amd/amdgpu/amdgpu_gtt_mgr.c
/**
* struct amdgpu_gtt_mgr - GTT manager
*/
struct amdgpu_gtt_mgr {
struct ttm_resource_manager manager; /* TTM 资源管理器基类 */
struct drm_mm mm; /* drm_mm 分配器 */
spinlock_t lock; /* 保护 mm 的自旋锁 */
};
/**
* struct ttm_range_mgr_node - TTM 范围管理节点
*/
struct ttm_range_mgr_node {
struct ttm_resource base; /* TTM 资源基类 */
struct drm_mm_node mm_nodes[1]; /* drm_mm 节点数组 */
};
4.1.2 初始化
函数:amdgpu_gtt_mgr_init()(源自 amdgpu_gtt_mgr.c)
int amdgpu_gtt_mgr_init(struct amdgpu_device *adev, uint64_t gtt_size)
{
struct amdgpu_gtt_mgr *mgr = &adev->mman.gtt_mgr;
struct ttm_resource_manager *man = &mgr->manager;
uint64_t start, size;
man->use_tt = true;
man->func = &amdgpu_gtt_mgr_func;
ttm_resource_manager_init(man, &adev->mman.bdev, gtt_size);
/* 计算可用的 GTT 范围 */
start = AMDGPU_GTT_MAX_TRANSFER_SIZE * AMDGPU_GTT_NUM_TRANSFER_WINDOWS;
size = (adev->gmc.gart_size >> PAGE_SHIFT) - start;
/* 初始化 drm_mm 管理 GTT 地址空间 */
drm_mm_init(&mgr->mm, start, size);
spin_lock_init(&mgr->lock);
ttm_set_driver_manager(&adev->mman.bdev, TTM_PL_TT, &mgr->manager);
ttm_resource_manager_set_used(man, true);
return 0;
}
关键点:
- 地址空间划分:预留前面一部分给传输窗口,剩余部分用 drm_mm 管理
- 页面单位:size 以页面为单位(
>> PAGE_SHIFT) - 线程安全:使用自旋锁保护 drm_mm 操作
4.1.3 分配 GTT 地址
函数:amdgpu_gtt_mgr_new()(源自 amdgpu_gtt_mgr.c)
static int amdgpu_gtt_mgr_new(struct ttm_resource_manager *man,
struct ttm_buffer_object *tbo,
const struct ttm_place *place,
struct ttm_resource **res)
{
struct amdgpu_gtt_mgr *mgr = to_gtt_mgr(man);
uint32_t num_pages = PFN_UP(tbo->base.size);
struct ttm_range_mgr_node *node;
int r;
/* 1. 分配节点结构 */
node = kzalloc(struct_size(node, mm_nodes, 1), GFP_KERNEL);
if (!node)
return -ENOMEM;
ttm_resource_init(tbo, place, &node->base);
/* 2. 检查是否超出总大小 */
if (!(place->flags & TTM_PL_FLAG_TEMPORARY) &&
ttm_resource_manager_usage(man) > man->size) {
r = -ENOSPC;
goto err_free;
}
/* 3. 如果指定了范围限制,使用 drm_mm 分配地址 */
if (place->lpfn) {
spin_lock(&mgr->lock);
r = drm_mm_insert_node_in_range(&mgr->mm, &node->mm_nodes[0],
num_pages, tbo->page_alignment,
0, place->fpfn, place->lpfn,
DRM_MM_INSERT_BEST);
spin_unlock(&mgr->lock);
if (unlikely(r))
goto err_free;
/* 分配成功,记录起始地址 */
node->base.start = node->mm_nodes[0].start;
} else {
/* 4. 无范围限制,只记录大小,不分配实际地址 */
node->mm_nodes[0].start = 0;
node->mm_nodes[0].size = PFN_UP(node->base.size);
node->base.start = AMDGPU_BO_INVALID_OFFSET;
}
*res = &node->base;
return 0;
err_free:
ttm_resource_fini(man, &node->base);
kfree(node);
return r;
}
关键点:
- 条件分配:只有指定了
lpfn(last page frame number)才实际分配 GTT 地址 - 对齐支持:使用
tbo->page_alignment指定对齐要求 - 最佳适配:使用
DRM_MM_INSERT_BEST策略减少碎片 - 延迟分配:无范围限制时,延迟到真正需要时才分配
4.1.4 释放 GTT 地址
函数:amdgpu_gtt_mgr_del()(源自 amdgpu_gtt_mgr.c)
static void amdgpu_gtt_mgr_del(struct ttm_resource_manager *man,
struct ttm_resource *res)
{
struct ttm_range_mgr_node *node = to_ttm_range_mgr_node(res);
struct amdgpu_gtt_mgr *mgr = to_gtt_mgr(man);
spin_lock(&mgr->lock);
if (drm_mm_node_allocated(&node->mm_nodes[0]))
drm_mm_remove_node(&node->mm_nodes[0]);
spin_unlock(&mgr->lock);
ttm_resource_fini(man, res);
kfree(node);
}
关键点:
- 检查后删除:使用
drm_mm_node_allocated()检查节点是否实际分配了地址 - 线程安全:持有自旋锁保护 drm_mm 操作
4.1.5 恢复 GTT 映射
函数:amdgpu_gtt_mgr_recover()(源自 amdgpu_gtt_mgr.c)
void amdgpu_gtt_mgr_recover(struct amdgpu_gtt_mgr *mgr)
{
struct ttm_range_mgr_node *node;
struct drm_mm_node *mm_node;
struct amdgpu_device *adev;
adev = container_of(mgr, typeof(*adev), mman.gtt_mgr);
spin_lock(&mgr->lock);
/* 遍历所有已分配的 GTT 节点 */
drm_mm_for_each_node(mm_node, &mgr->mm) {
node = container_of(mm_node, typeof(*node), mm_nodes[0]);
amdgpu_ttm_recover_gart(node->base.bo);
}
spin_unlock(&mgr->lock);
}
用途:GPU 复位后,重新建立所有 GTT 映射。使用 drm_mm_for_each_node 遍历所有节点。
4.1.6 调试输出
函数:amdgpu_gtt_mgr_debug()(源自 amdgpu_gtt_mgr.c)
static void amdgpu_gtt_mgr_debug(struct ttm_resource_manager *man,
struct drm_printer *printer)
{
struct amdgpu_gtt_mgr *mgr = to_gtt_mgr(man);
spin_lock(&mgr->lock);
drm_mm_print(&mgr->mm, printer); /* 使用 drm_mm 的打印函数 */
spin_unlock(&mgr->lock);
}
输出示例(通过 debugfs):
$ cat /sys/kernel/debug/dri/0/amdgpu_gtt_mm
0x0000000000001000-0x0000000000002000: 4096: used
0x0000000000002000-0x0000000000100000: 1040384: free
...
total: 4294967296, used 1073741824 free 3221225472
4.2 应用总结
AMDGPU GTT 管理器的 drm_mm 使用模式:
| 方面 | 实现方式 | 优势 |
|---|---|---|
| 初始化 | drm_mm_init() 管理页面范围 | 简单高效 |
| 分配策略 | DRM_MM_INSERT_BEST | 减少碎片化 |
| 同步保护 | spinlock 保护所有操作 | 确保线程安全 |
| 节点嵌入 | 嵌入到 ttm_range_mgr_node | 零额外开销 |
| 遍历支持 | drm_mm_for_each_node | 恢复时批量处理 |
| 调试支持 | drm_mm_print() | 便于问题诊断 |
4.3 其他 AMDGPU 应用
虽然 VRAM 管理器(amdgpu_vram_mgr.c)现在主要使用 drm_buddy 分配器,但 drm_mm 的设计理念和接口模式在 AMDGPU 中被广泛采用:
- 地址空间管理:无论是 GTT 还是其他地址空间,都可以用 drm_mm 统一管理
- 分配策略灵活:根据不同场景选择不同的插入模式
- 与 TTM 集成:drm_mm 作为 TTM 资源管理器的底层实现
5. 性能分析与优化技巧
5.1 时间复杂度分析
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 初始化 | O(1) | 仅初始化数据结构 |
| 分配(最坏) | O(num_holes) | 遍历所有 hole |
| 分配(平均) | O(log n) | 红黑树查找 + 少量 hole 检查 |
| 分配(快速失败) | O(1) | 检查最大 hole 即可 |
| 释放 | O(log n) | 主要是红黑树操作 |
| 遍历所有节点 | O(n) | 线性遍历链表 |
| 查找区间重叠 | O(log n + m) | 区间树查找,m 为重叠数 |
5.2 空间开销
- drm_mm:约 200 字节(主要是红黑树根节点和链表头)
- drm_mm_node:约 120 字节(包含多个红黑树节点)
- 总开销:非常低,适合嵌入驱动数据结构
5.3 优化技巧
5.3.1 选择合适的分配策略
/* 通用场景:减少碎片 */
drm_mm_insert_node_generic(&mm, &node, size, 0, 0, DRM_MM_INSERT_BEST);
/* 需要低地址(如 32 位 DMA)*/
drm_mm_insert_node_generic(&mm, &node, size, 0, 0, DRM_MM_INSERT_LOW);
/* 大块分配:避免低地址区域 */
drm_mm_insert_node_generic(&mm, &node, size, 0, 0, DRM_MM_INSERT_HIGH);
/* 驱逐后立即分配:提高局部性 */
drm_mm_insert_node_generic(&mm, &node, size, 0, 0, DRM_MM_INSERT_EVICT);
/* 快速失败:避免长时间搜索 */
drm_mm_insert_node_generic(&mm, &node, size, 0, 0,
DRM_MM_INSERT_HIGHEST);
5.3.2 对齐优化
/* 2 的幂次对齐:使用位运算优化 */
u64 alignment = 0x1000; /* 4KB,自动优化为 (addr & 0xFFF) */
/* 非 2 的幂次:使用除法,较慢 */
u64 alignment = 0x3000; /* 12KB,使用 div64_u64_rem() */
建议:尽量使用 2 的幂次对齐(页面大小、cache line 大小等)。
5.3.3 减少碎片化
- 使用 BEST 策略:优先填充小 hole
- 定期整理:在空闲时移动对象,合并 hole
- 预留区域:使用
drm_mm_reserve_node()避免碎片 - 限制范围:尽量使用整个范围,而不是频繁的小范围分配
5.3.4 并发控制
drm_mm 本身不是线程安全的,需要外部加锁:
/* 方案 1:互斥锁(睡眠上下文) */
struct mutex lock;
mutex_init(&lock);
mutex_lock(&lock);
ret = drm_mm_insert_node(&mm, &node, size);
mutex_unlock(&lock);
/* 方案 2:自旋锁(原子上下文) */
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
ret = drm_mm_insert_node(&mm, &node, size);
spin_unlock(&lock);
/* 方案 3:RCU(读多写少场景) */
/* 需要额外的同步机制,drm_mm 不直接支持 */
AMDGPU 选择:GTT 管理器使用自旋锁,因为操作快速且可能在中断上下文中调用。
5.4 调试技巧
5.4.1 启用调试模式
# 内核配置
CONFIG_DRM_DEBUG_MM=y
启用后可以:
- 追踪每个节点的分配栈
- 检测内存泄漏
- 使用 BUG_ON 严格检查
5.4.2 使用 drm_mm_print()
/* 定期输出状态 */
struct drm_printer p = drm_debug_printer("GTT");
drm_mm_print(&mgr->mm, &p);
5.4.3 分析碎片化
u64 total_free = 0;
u64 largest_hole = 0;
struct drm_mm_node *node;
u64 hole_start, hole_end;
drm_mm_for_each_hole(node, &mm, hole_start, hole_end) {
u64 size = hole_end - hole_start;
total_free += size;
if (size > largest_hole)
largest_hole = size;
}
/* 碎片化指标 */
float fragmentation = 1.0 - (float)largest_hole / total_free;
printk("Fragmentation: %.2f%% (largest: %llu, total: %llu)\n",
fragmentation * 100, largest_hole, total_free);
6. 总结
6.1 核心要点
-
drm_mm 是通用范围分配器
- 管理连续的地址空间(不是物理内存),适用于 VRAM、GTT、虚拟地址等各种范围
-
高效的数据结构
- 区间树:O(log n) 区间查询,按大小和地址组织的红黑树:快速找到合适的 hole
-
灵活的分配策略
- BEST/LOW/HIGH/EVICT 等多种模式,支持对齐、范围限制、颜色标记
-
零开销抽象
- 不做内存分配,节点可嵌入驱动结构,无内部锁,由调用者控制同步
-
完善的调试支持
- 栈跟踪、泄漏检测,状态打印、统计信息
6.2 适用场景
适合:
- GPU 地址空间管理(VRAM、GTT)
- 驱动内部的范围分配
- 需要灵活策略的分配器
- 嵌入式系统(低开销)
不适合:
- 需要复杂策略的内存管理(用 TTM)
- 需要内部线程安全的场景
- 需要自动碎片整理的场景
6.3 最佳实践
- 初始化前清零:
memset(&mm, 0, sizeof(mm)) - 节点清零:
memset(&node, 0, sizeof(node)) - 外部加锁:根据上下文选择合适的锁
- 选择策略:根据分配模式选择合适的插入模式
- 检查返回值:始终检查
-ENOSPC错误 - 调试模式开发:开发时启用
CONFIG_DRM_DEBUG_MM - 清理检查:销毁前确保所有节点已释放
6.4 与其他分配器对比
| 特性 | drm_mm | Linux 内核 gen_pool | TTM |
|---|---|---|---|
| 复杂度 | 中等 | 简单 | 复杂 |
| 灵活性 | 高 | 低 | 非常高 |
| 性能 | O(log n) | O(n) | O(log n) + 额外开销 |
| GPU 优化 | 是 | 否 | 是 |
| 内存管理 | 否 | 否 | 是 |
| 适用范围 | 地址空间 | 通用范围 | GPU 内存全栈 |
drm_mm 在性能、灵活性和简洁性之间取得了良好的平衡,是 DRM 子系统中不可或缺的基础设施。本节代码量比较大,对于不感兴趣实现的读者可以略过,也可以先理解接口的功能,然后看过后面章节后,在来细读。
技术交流,欢迎加入社区:GPUers。
943

被折叠的 条评论
为什么被折叠?



