第三章:GEM分析: 3.3 drm_mm 范围分配器

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 的设计目标包括:

  1. 简单高效:提供基础的范围分配功能,算法简单但实用
  2. 可扩展性:作为基础类,可被更复杂的内存管理器(如 TTM)扩展
  3. GPU 特化:针对 GPU 的特殊需求进行优化,如支持对齐、颜色标记等
  4. 零开销抽象:不做实际物理内存分配,由驱动管理实际的数据结构嵌入

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;             /* 驱逐扫描状态标志 */
};

关键字段说明

  1. interval_tree:使用区间树(Interval Tree)存储所有已分配的节点

    • 支持快速查找某个地址范围内的节点
    • 基于 Linux 内核的 INTERVAL_TREE_DEFINE 宏实现
    • 时间复杂度:O(log n + m),m 为重叠节点数
  2. holes_size:按 hole 大小组织的缓存红黑树

    • 用于 DRM_MM_INSERT_BEST 策略(最佳适配)
    • 最大的 hole 位于树的最左侧
    • 时间复杂度:O(log n)
  3. holes_addr:按 hole 地址组织的增强红黑树

    • 用于 DRM_MM_INSERT_LOWDRM_MM_INSERT_HIGH 策略
    • 使用 subtree_max_hole 进行增强,支持快速查找最大 hole
    • 时间复杂度:O(log n)
  4. hole_stack:最近释放的 hole 链表栈

    • 用于 DRM_MM_INSERT_EVICT 策略
    • 优化局部性,减少碎片化
    • 访问复杂度:O(num_holes)
  5. 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
};

关键字段说明

  1. color:颜色标记

    • 用途:标识不兼容的内存域(如不同的缓存策略)
    • 示例:Intel i915 驱动用它在不同缓存域之间插入 guard pages
    • 配合 color_adjust 回调使用
  2. hole_sizesubtree_max_hole

    • hole_size:当前节点后面紧跟的空闲空间大小
    • subtree_max_hole:子树中最大的 hole 大小(用于优化搜索)
    • 只有后面有 hole 的节点才会被加入 hole 相关的数据结构
  3. 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;
}

关键点

  1. 快速失败优化:先检查最大 hole,避免无谓遍历
  2. color_adjust 回调:允许驱动插入 guard pages 或其他限制
  3. 对齐处理优化:对 2 的幂次对齐使用位运算
  4. hole 分裂:分配后可能产生 0、1 或 2 个新 hole
  5. 区间树更新:使用增强红黑树高效维护区间

便捷包装函数

/* 在整个范围内分配,无特殊限制 */
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);
}

关键逻辑

  1. hole 合并:删除节点后,前后的空闲空间会自动合并
  2. O(1) 复杂度:主要操作是链表和红黑树的节点删除
  3. 无需手动清理节点:节点可以立即重新用于下一次分配

示例

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);

功能:在分配器中保留一个预定义位置的节点(节点的 startsize 必须预设)。

应用场景

  • 保留固件/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)

用途:当内存不足时,找出需要驱逐的节点以腾出空间。

核心函数

  1. 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);
    
  2. drm_mm_scan_add_block():将候选节点添加到扫描

    bool drm_mm_scan_add_block(struct drm_mm_scan *scan,
                               struct drm_mm_node *node);
    

    返回 true 表示找到足够的空间。

  3. 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 会:

  1. 栈跟踪:记录每个节点的分配调用栈

    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);
    }
    
  2. 泄漏检测:在 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);
    }
    
  3. 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;
}

关键点

  1. 条件分配:只有指定了 lpfn(last page frame number)才实际分配 GTT 地址
  2. 对齐支持:使用 tbo->page_alignment 指定对齐要求
  3. 最佳适配:使用 DRM_MM_INSERT_BEST 策略减少碎片
  4. 延迟分配:无范围限制时,延迟到真正需要时才分配
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 中被广泛采用:

  1. 地址空间管理:无论是 GTT 还是其他地址空间,都可以用 drm_mm 统一管理
  2. 分配策略灵活:根据不同场景选择不同的插入模式
  3. 与 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 减少碎片化
  1. 使用 BEST 策略:优先填充小 hole
  2. 定期整理:在空闲时移动对象,合并 hole
  3. 预留区域:使用 drm_mm_reserve_node() 避免碎片
  4. 限制范围:尽量使用整个范围,而不是频繁的小范围分配
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 核心要点

  1. drm_mm 是通用范围分配器

    • 管理连续的地址空间(不是物理内存),适用于 VRAM、GTT、虚拟地址等各种范围
  2. 高效的数据结构

    • 区间树:O(log n) 区间查询,按大小和地址组织的红黑树:快速找到合适的 hole
  3. 灵活的分配策略

    • BEST/LOW/HIGH/EVICT 等多种模式,支持对齐、范围限制、颜色标记
  4. 零开销抽象

    • 不做内存分配,节点可嵌入驱动结构,无内部锁,由调用者控制同步
  5. 完善的调试支持

    • 栈跟踪、泄漏检测,状态打印、统计信息

6.2 适用场景

适合

  • GPU 地址空间管理(VRAM、GTT)
  • 驱动内部的范围分配
  • 需要灵活策略的分配器
  • 嵌入式系统(低开销)

不适合

  • 需要复杂策略的内存管理(用 TTM)
  • 需要内部线程安全的场景
  • 需要自动碎片整理的场景

6.3 最佳实践

  1. 初始化前清零memset(&mm, 0, sizeof(mm))
  2. 节点清零memset(&node, 0, sizeof(node))
  3. 外部加锁:根据上下文选择合适的锁
  4. 选择策略:根据分配模式选择合适的插入模式
  5. 检查返回值:始终检查 -ENOSPC 错误
  6. 调试模式开发:开发时启用 CONFIG_DRM_DEBUG_MM
  7. 清理检查:销毁前确保所有节点已释放

6.4 与其他分配器对比

特性drm_mmLinux 内核 gen_poolTTM
复杂度中等简单复杂
灵活性非常高
性能O(log n)O(n)O(log n) + 额外开销
GPU 优化
内存管理
适用范围地址空间通用范围GPU 内存全栈

drm_mm 在性能、灵活性和简洁性之间取得了良好的平衡,是 DRM 子系统中不可或缺的基础设施。本节代码量比较大,对于不感兴趣实现的读者可以略过,也可以先理解接口的功能,然后看过后面章节后,在来细读。


技术交流,欢迎加入社区:GPUers

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DeeplyMind

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值