06 - SVM范围管理

难度 : 🟡🔴 进阶到高级
预计学习时间 : 2-2.5小时
前置知识: 前面章节内容、红黑树和区间树基础


📋 概述

SVM范围管理是SVM功能的核心,负责创建、查找、分割、合并和删除虚拟内存范围。这些操作需要在保持数据一致性的同时处理复杂的边界情况。想象一下,管理SVM范围就像管理一本预约日历:

  • 📅 添加预约可能与现有预约重叠
  • ✂️ 需要分割现有预约来腾出空间
  • 🔍 需要快速查找某个时间段的预约
  • 🗑️ 取消预约时需要合并相邻的空闲时段

本章将深入这些核心操作的实现细节。


6.1 范围的创建

6.1.1 svm_range_add 函数

这是SVM范围创建的入口函数,处理复杂的重叠情况:

c 复制代码
int svm_range_add(struct kfd_process *p, 
                  uint64_t start, uint64_t size,
                  uint32_t nattr, 
                  struct kfd_ioctl_svm_attribute *attrs,
                  struct list_head *update_list,
                  struct list_head *insert_list,
                  struct list_head *remove_list,
                  struct list_head *remap_list)

功能:在指定地址范围创建SVM范围,处理与现有范围的重叠。

参数详解

  • start: 起始地址(页号)
  • size: 大小(页数)
  • nattr: 属性数量
  • attrs: 属性数组
  • update_list: 输出,需要更新的范围
  • insert_list: 输出,需要插入的新范围
  • remove_list: 输出,需要删除的旧范围
  • remap_list: 输出,需要重新映射的范围

6.1.2 创建流程

复制代码
用户请求创建新范围 [A, B]
    ↓
检查现有范围的重叠情况
    ↓
┌─────────────────────────────────┐
│ 情况1: 无重叠                    │
│   → 直接创建新范围               │
├─────────────────────────────────┤
│ 情况2: 完全包含现有范围           │
│   → 更新现有范围的属性            │
│   → 分割新范围                   │
├─────────────────────────────────┤
│ 情况3: 部分重叠                  │
│   → 克隆现有范围                 │
│   → 分割克隆                     │
│   → 更新分割后的部分              │
├─────────────────────────────────┤
│ 情况4: 有间隙                    │
│   → 在间隙中创建新范围            │
└─────────────────────────────────┘
    ↓ 
事务性提交(全成功或全失败)

详细示例

场景1:无重叠,直接创建

复制代码
现有: [无]
请求: [1000-2000]

结果:
    insert_list: [1000-2000] (新创建)
    update_list: [1000-2000] (需要设置属性)

场景2:完全包含(请求范围是现有范围的子集)

复制代码
现有: [1000-3000] (属性A)
请求: [1500-2500] (属性B)

步骤:
1. 克隆 [1000-3000] → [1000-3000]'
2. 分割头部: [1000-1499] (属性A)
3. 分割尾部: [2501-3000] (属性A)
4. 中间部分: [1500-2500] (属性B)

结果:
    remove_list: [1000-3000] (原始)
    insert_list: [1000-1499], [1500-2500], [2501-3000] (三个新范围)
    update_list: [1500-2500] (需要更新属性)

场景3:部分重叠

复制代码
现有: [1000-2000], [3000-4000]
请求: [1500-3500]

步骤:
1. 处理 [1000-2000]:
   - 克隆并分割头部: [1000-1499]
   - 重叠部分: [1500-2000] (更新)
2. 创建间隙: [2001-2999]
3. 处理 [3000-4000]:
   - 克隆并分割: [3000-3500] (更新)
   - 尾部: [3501-4000]

结果:
    remove_list: [1000-2000], [3000-4000] (原始)
    insert_list: [1000-1499], [1500-2000], [2001-2999], 
                 [3000-3500], [3501-4000]
    update_list: [1500-2000], [2001-2999], [3000-3500]

6.1.3 代码解析

c 复制代码
// 主循环:处理所有重叠的范围
node = interval_tree_iter_first(&svms->objects, start, last);
while (node) {
    prange = container_of(node, struct svm_range, it_node);
    next = interval_tree_iter_next(node, start, last);
    
    // 检查是否属性相同
    if (svm_range_is_same_attrs(p, prange, nattr, attrs) &&
        prange->mapped_to_gpu) {
        // 属性相同,无需操作
    } 
    // 部分重叠:需要分割
    else if (node->start < start || node->last > last) {
        // 1. 克隆现有范围
        struct svm_range *old = prange;
        prange = svm_range_clone(old);
        
        // 2. 添加到相应列表
        list_add(&old->update_list, remove_list);
        list_add(&prange->list, insert_list);
        list_add(&prange->update_list, update_list);
        
        // 3. 分割头部(如果需要)
        if (node->start < start)
            svm_range_split_head(prange, start, 
                                insert_list, remap_list);
        
        // 4. 分割尾部(如果需要)
        if (node->last > last) 
            svm_range_split_tail(prange, last, 
                                insert_list, remap_list);
    }
    // 完全包含:直接更新
    else
        list_add(&prange->update_list, update_list);
    
    // 处理间隙
    if (node->start > start) 
        svm_range_split_new(svms, start, node->start - 1,
                           ..., &new_list, update_list);
    
    node = next;
    start = next_start;
}

// 处理尾部间隙
if (start <= last)
    svm_range_split_new(svms, start, last, 
                       ..., &new_list, update_list);

6.1.4 事务性保证

c 复制代码
// 如果任何操作失败,回滚所有更改
if (r) {
    // 释放所有新创建的范围
    list_for_each_entry_safe(prange, tmp, insert_list, list)
        svm_range_free(prange, false);
    list_for_each_entry_safe(prange, tmp, &new_list, list)
        svm_range_free(prange, true);
} else {
    // 成功,合并新范围到插入列表
    list_splice(&new_list, insert_list);
}

为什么需要事务性?

  • 避免部分完成导致的不一致状态
  • 失败时无需复杂的清理逻辑
  • 保证调用者看到的是原子操作

6.1.5 VMA跨越的约束

SVM范围可以跨越多个VMA,但有严格要求。

svm_range_is_valid() 函数的实现可以看出,SVM范围允许跨越多个VMA

c 复制代码
// 文件: kfd_svm.c
static int
svm_range_is_valid(struct kfd_process *p, uint64_t start, uint64_t size)
{
	const unsigned long device_vma = VM_IO | VM_PFNMAP | VM_MIXEDMAP;
	struct vm_area_struct *vma;
	unsigned long end;

	start <<= PAGE_SHIFT;
	end = start + (size << PAGE_SHIFT);
	do {
		vma = vma_lookup(p->mm, start);
		if (!vma || (vma->vm_flags & device_vma))
			return -EFAULT;
		start = min(end, vma->vm_end);
	} while (start < end);  // 循环检查所有VMA
	
	return svm_range_check_vm(...);
}

VMA必须满足一下要求:

1. VMA必须连续存在

c 复制代码
vma = vma_lookup(p->mm, start);
if (!vma || ...)
	return -EFAULT;
  • SVM范围覆盖的整个地址空间必须都有对应的VMA
  • 中间不能有空洞(unmapped区域)
  • 如果任何部分没有VMA,创建将失败

2. 禁止特殊设备映射

c 复制代码
const unsigned long device_vma = VM_IO | VM_PFNMAP | VM_MIXEDMAP;
if (vma->vm_flags & device_vma)
	return -EFAULT;

不允许的VMA类型

  • VM_IO:I/O设备映射(如 /dev/mem
  • VM_PFNMAP:Page Frame Number映射(如framebuffer)
  • VM_MIXEDMAP:混合页面映射

原因:这些特殊映射无法参与页面迁移和MMU通知机制,与SVM的核心机制冲突。

3. 不能与GPU BO映射冲突

c 复制代码
return svm_range_check_vm(p, start_unchg, (end - 1) >> PAGE_SHIFT, NULL, NULL);

svm_range_check_vm() 确保:

  • SVM范围不能覆盖已通过 kfd_ioctl_alloc_memory_of_gpu 分配的区域
  • 避免与传统GPU内存管理路径冲突
  • 返回 -EADDRINUSE 表示地址已被占用

4. 尊重VMA的访问权限

在页面错误处理中(svm_range_restore_pages),系统会检查VMA权限:

c 复制代码
// 文件: kfd_svm.c, 行: 1710-1730
vma = vma_lookup(mm, addr);
if (vma) {
	readonly = !(vma->vm_flags & VM_WRITE);
	
	if (!(vma->vm_flags & VM_READ)) {
		// 特殊情况:不可读但可写
		if (vma->vm_flags & VM_WRITE)
			readonly = false;
	}
}

含义

  • 跨越的多个VMA可以有不同的访问权限
  • 页面迁移时会根据VMA权限设置GPU页表权限
  • 只读VMA中的页面会在GPU侧映射为只读
实际场景示例

场景1:跨heap和stack(✅ 允许)

复制代码
VMA1: [0x1000-0x2000] (heap, RW)
VMA2: [0x3000-0x4000] (stack, RW)
SVM Range: [0x1500-0x3500]
状态: ✅ 允许(连续,都是普通内存)

场景2:跨mmap区域(✅ 允许)

复制代码
VMA1: [0x1000-0x2000] (anonymous, RW)
VMA2: [0x2000-0x3000] (anonymous, RO)
SVM Range: [0x1000-0x3000]
状态: ✅ 允许(权限会分段处理)
说明: VMA2的只读页面在GPU侧也映射为只读

场景3:包含设备映射(❌ 禁止)

复制代码
VMA1: [0x1000-0x2000] (normal, RW)
VMA2: [0x2000-0x3000] (device, VM_IO)
SVM Range: [0x1000-0x3000]
状态: ❌ 禁止(包含设备映射)
错误: -EFAULT

场景4:有空洞(❌ 禁止)

复制代码
VMA1: [0x1000-0x2000] (normal, RW)
[gap: 0x2000-0x3000]  ← 未映射区域
VMA2: [0x3000-0x4000] (normal, RW)
SVM Range: [0x1000-0x4000]
状态: ❌ 禁止(中间有未映射区域)
错误: -EFAULT

场景5:与GPU BO冲突(❌ 禁止)

复制代码
VMA1: [0x1000-0x3000] (normal, RW)
GPU BO: [0x2000-0x2500] (通过kfd_ioctl_alloc_memory_of_gpu分配)
SVM Range: [0x1500-0x2800]
状态: ❌ 禁止(与GPU BO重叠)
错误: -EADDRINUSE
为什么允许跨VMA?
  1. 灵活性 :用户可能通过多次 mmap() 调用创建连续的内存区域
  2. 简化API:应用程序不需要了解内核内部的VMA边界
  3. 动态适应:heap和stack会动态增长,导致VMA边界变化
  4. 统一管理:大块内存可能由多个VMA组成,但希望作为单一SVM范围管理
为什么有这些限制?
  1. MMU Notifier依赖

    • SVM使用 mmu_interval_notifier 跟踪页面变化
    • 设备映射(VM_IO等)不支持标准的页面操作
  2. 页面迁移要求

    • 需要能够分配/释放物理页面
    • 设备映射的页面无法迁移
  3. 避免冲突

    • 传统GPU BO和SVM使用不同的内存管理路径
    • 同一地址不能同时被两种机制管理
  4. 硬件限制

    • GPU页表需要可修改的物理页面映射
    • 特殊映射可能有固定的物理地址
边界情况处理

svm_range_get_range_boundaries() 函数展示了如何处理VMA边界:

c 复制代码
static int
svm_range_get_range_boundaries(struct kfd_process *p, int64_t addr,
			       unsigned long *start, unsigned long *last,
			       bool *is_heap_stack)
{
	struct vm_area_struct *vma;
	
	vma = vma_lookup(p->mm, addr << PAGE_SHIFT);
	if (!vma) {
		pr_debug("VMA does not exist in address [0x%llx]\n", addr);
		return -EFAULT;
	}
	
	*is_heap_stack = vma_is_initial_heap(vma) || vma_is_initial_stack(vma);
	
	// 考虑VMA边界和默认粒度
	start_limit = max(vma->vm_start >> PAGE_SHIFT,
		      ALIGN_DOWN(addr, 1UL << p->svms.default_granularity));
	end_limit = min(vma->vm_end >> PAGE_SHIFT,
		    ALIGN(addr + 1, 1UL << p->svms.default_granularity));
	
	// 避免与现有范围重叠
	// ...
}

这个函数在页面错误时自动创建范围,它会:

  • 限制范围在单个VMA内
  • 考虑默认粒度对齐
  • 避免与已存在的范围重叠
  • 特殊处理heap和stack
最佳实践建议
  1. 使用大块mmap :尽量通过单次 mmap() 创建大区域,减少VMA碎片
  2. 检查返回值 :调用SVM API后检查错误码,特别是 -EFAULT-EADDRINUSE
  3. 避免混合映射:不要在SVM区域内混合普通内存和设备映射
  4. 按粒度对齐 :使用 p->svms.default_granularity 对齐地址,减少边界问题
  5. 分离BO和SVM:传统GPU BO和SVM区域使用不同的地址范围

6.2 范围的查找 (interval tree使用)

区间树原理

区间树是基于红黑树的数据结构,支持高效的区间查询:

复制代码
普通查找:给定精确地址 → O(log N)
区间查找:给定地址范围 → O(log N + K) (K是结果数量)

数据结构

c 复制代码
struct interval_tree_node {
    struct rb_node rb;          // 红黑树节点
    unsigned long start;        // 区间起始
    unsigned long last;         // 区间结束
    unsigned long __subtree_last;  // 子树最大结束值(内部使用)
};

查找操作

1. 查找包含指定地址的范围
c 复制代码
struct svm_range *svm_range_from_addr(struct svm_range_list *svms,
                                      unsigned long addr,
                                      struct svm_range **parent)
{
    struct interval_tree_node *node;
    
    // 在区间树中查找包含addr的节点
    node = interval_tree_iter_first(&svms->objects, addr, addr);
    
    if (!node)
        return NULL;
    
    return container_of(node, struct svm_range, it_node);
}

示例

复制代码
区间树:
    [1000-2000]
    [3000-4000]
    [5000-6000]

查找地址 3500:
    → interval_tree_iter_first(&svms->objects, 3500, 3500)
    → 返回 [3000-4000]

查找地址 2500:
    → 返回 NULL(无包含此地址的范围)
2. 查找与指定范围重叠的所有范围
c 复制代码
// 查找所有与 [start, last] 重叠的范围
struct interval_tree_node *node;

node = interval_tree_iter_first(&svms->objects, start, last);
while (node) {
    struct svm_range *prange;
    prange = container_of(node, struct svm_range, it_node);
    
    // 处理prange...
    
    node = interval_tree_iter_next(node, start, last);
}

示例

复制代码
区间树:
    [1000-2000]
    [2500-3000]
    [3500-4000]

查找范围 [1500-3500]:
    第一次: 返回 [1000-2000]  (与 [1500-3500] 重叠)
    第二次: 返回 [2500-3000]  (与 [1500-3500] 重叠)
    第三次: 返回 [3500-4000]  (与 [1500-3500] 重叠)
    第四次: 返回 NULL
3. 区间树的维护
c 复制代码
// 插入
prange->it_node.start = prange->start;
prange->it_node.last = prange->last;
interval_tree_insert(&prange->it_node, &svms->objects);

// 删除
interval_tree_remove(&prange->it_node, &svms->objects);

// 更新(需要先删除再插入)
interval_tree_remove(&prange->it_node, &svms->objects);
prange->start = new_start;
prange->last = new_last;
prange->it_node.start = new_start;
prange->it_node.last = new_last;
interval_tree_insert(&prange->it_node, &svms->objects);

区间树 vs 链表的时间复杂度

复制代码
操作        区间树         链表
-------------------------------------
精确查找    O(log N)       O(N)
区间查找    O(log N + K)   O(N)
插入       O(log N)       O(1)
删除       O(log N)       O(1)
遍历所有    O(N)           O(N)

结论:区间树擅长查找,链表擅长遍历。因此SVM同时使用两者!


6.3 范围的分割与合并

分割操作

为什么需要分割?

复制代码
场景:修改范围的一部分属性

原范围: [1000-3000] (只读)
请求:   将 [1500-2500] 设为可写

解决方案:分割
    [1000-1499] (只读)
    [1500-2500] (可写) ← 修改这部分
    [2501-3000] (只读)
svm_range_split 函数
c 复制代码
// 文件: kfd_svm.c
/**
 * svm_range_split - split a range in 2 ranges
 * @prange: the svm range to split
 * @start: the remaining range start address in pages
 * @last: the remaining range last address in pages
 * @new: the result new ranges generated
 *
 * Two cases only:
 * case 1: if start == prange->start
 *         prange ==> prange[start, last]
 *         new range [last + 1, prange->last]
 *
 * case 2: if last == prange->last
 *         prange ==> prange[start, last]
 *         new range [prange->start, start - 1]
 *
 * Return:
 * 0 - OK, -ENOMEM - out of memory, -EINVAL - invalid start, last
 */
int svm_range_split(struct svm_range *prange, 
                    uint64_t start, uint64_t last,
                    struct svm_range **new)

两种分割模式:分割头部和分割尾部。

  1. 分割头部start == prange->start

    原范围: [1000-3000]
    分割: [1000-2000]
    结果:
    prange: [1000-2000] (修改原范围)
    *new: [2001-3000] (新创建)

  2. 分割尾部last == prange->last

    原范围: [1000-3000]
    分割: [1500-3000]
    结果:
    prange: [1500-3000] (修改原范围)
    *new: [1000-1499] (新创建)

分割步骤
c 复制代码
int svm_range_split(struct svm_range *prange, 
                    uint64_t start, uint64_t last,
                    struct svm_range **new)
{
    uint64_t old_start = prange->start;
    uint64_t old_last = prange->last;
    
    // 1. 参数验证
    if (old_start != start && old_last != last)
        return -EINVAL;  // 必须从头或尾分割
    
    // 2. 创建新范围
    if (old_start == start)
        *new = svm_range_new(svms, last + 1, old_last, false);
    else
        *new = svm_range_new(svms, old_start, start - 1, false);
    
    // 3. 调整属性和资源
    r = svm_range_split_adjust(*new, prange, start, last);
    
    return r;
}
svm_range_split_adjust 详解

这个函数处理分割后的资源分配:

c 复制代码
// 文件: kfd_svm.c
int svm_range_split_adjust(struct svm_range *new, 
                           struct svm_range *old,
                           uint64_t start, uint64_t last)
{
    // 1. 调整地址范围
    old->start = start;
    old->last = last;
    old->npages = last - start + 1;
    
    // 2. 分割DMA地址数组
    r = svm_range_split_pages(new, old, start, last);
    
    // 3. 分割VRAM节点(如果有)
    if (old->actual_loc && old->ttm_res)
        r = svm_range_split_nodes(new, old, start, last);
    
    return r;
}

DMA地址数组分割

复制代码
原DMA数组 (1000页):
old->dma_addr[0]: [addr0, addr1, ..., addr999]

分割为 [1000-1499] 和 [1500-2999]:

new->dma_addr[0]: [addr0, addr1, ..., addr499]    (500页)
old->dma_addr[0]: [addr500, addr501, ..., addr999] (500页)

代码实现

c 复制代码
static int svm_range_split_pages(struct svm_range *new, 
                                 struct svm_range *old,
                                 uint64_t start, uint64_t last)
{
    uint64_t npages = last - start + 1;
    int i, r;
    
    for (i = 0; i < MAX_GPU_INSTANCE; i++) {
        r = svm_range_split_array(&new->dma_addr[i], 
                                  &old->dma_addr[i],
                                  sizeof(*old->dma_addr[i]), 
                                  old->start,
                                  npages, new->start, new->npages,
                                  old->actual_loc ? &new->vram_pages : NULL);
        if (r)
            return r;
    }
    
    // 更新VRAM页面计数
    if (old->actual_loc)
        old->vram_pages -= new->vram_pages;
    
    return 0;
}

合并操作

虽然没有显式的"合并"函数,但SVM通过以下方式实现逻辑合并:

隐式合并:创建时检查相邻范围

c 复制代码
// 在svm_range_add中
if (svm_range_is_same_attrs(p, prange, nattr, attrs) &&
    prange->mapped_to_gpu) {
    // 属性相同,无需创建新范围
    // 现有范围自然"合并"了新请求
}

例子

复制代码
现有: [1000-2000] (可写)
请求: [2001-3000] (可写,相同属性)

结果: 不创建新范围,两者逻辑上是连续的

6.4 范围的删除与清理

删除流程

复制代码
删除请求
    ↓
svm_range_unlink()  ← 从数据结构移除
    ↓
svm_range_remove_notifier()  ← 移除MMU notifier
    ↓
svm_range_free()  ← 释放资源
c 复制代码
// 文件: kfd_svm.c
static void svm_range_unlink(struct svm_range *prange)
{
    // 1. 从svm_bo的范围列表移除
    if (prange->svm_bo) {
        spin_lock(&prange->svm_bo->list_lock);
        list_del(&prange->svm_bo_list);
        spin_unlock(&prange->svm_bo->list_lock);
    }
    
    // 2. 从svms的链表移除
    list_del(&prange->list);
    
    // 3. 从区间树移除
    if (prange->it_node.start != 0 && prange->it_node.last != 0)
        interval_tree_remove(&prange->it_node, &prange->svms->objects);
}
svm_range_remove_notifier
c 复制代码
// 文件: kfd_svm.c
static void svm_range_remove_notifier(struct svm_range *prange)
{
    pr_debug("remove notifier svms 0x%p prange 0x%p [0x%lx 0x%lx]\n",
             prange->svms, prange,
             prange->notifier.interval_tree.start >> PAGE_SHIFT,
             prange->notifier.interval_tree.last >> PAGE_SHIFT);
    
    if (prange->notifier.interval_tree.start != 0 &&
        prange->notifier.interval_tree.last != 0)
        mmu_interval_notifier_remove(&prange->notifier);
}
svm_range_free
c 复制代码
void svm_range_free(struct svm_range *prange, bool do_unmap)
{
    // 1. 取消GPU映射
    if (do_unmap && prange->mapped_to_gpu) {
        for (i = 0; i < MAX_GPU_INSTANCE; i++) {
            if (test_bit(i, prange->bitmap_access))
                svm_range_unmap_from_gpu(...);
        }
    }
    
    // 2. 释放DMA映射
    svm_range_dma_unmap(prange);
    for (i = 0; i < MAX_GPU_INSTANCE; i++) {
        kvfree(prange->dma_addr[i]);
        prange->dma_addr[i] = NULL;
    }
    
    // 3. 释放VRAM资源
    if (prange->svm_bo) {
        svm_range_vram_node_free(prange);
    }
    
    // 4. 销毁锁
    mutex_destroy(&prange->lock);
    mutex_destroy(&prange->migrate_mutex);
    
    // 5. 释放结构本身
    kfree(prange);
}

6.5 代码示例和流程图

完整示例:创建重叠范围

c 复制代码
// 场景:创建新范围,与现有范围部分重叠
struct kfd_process *p = ...;
struct svm_range_list *svms = &p->svms;

// 准备输出列表
LIST_HEAD(update_list);
LIST_HEAD(insert_list);
LIST_HEAD(remove_list);
LIST_HEAD(remap_list);

// 现有范围: [1000-2000]
// 请求创建: [1500-3000] with 新属性

mutex_lock(&svms->lock);

r = svm_range_add(p, 1500, 1501,  // 1501页
                 nattr, attrs,
                 &update_list, &insert_list, 
                 &remove_list, &remap_list);

if (r == 0) {
    // 成功!应用更改
    
    // 1. 移除旧范围
    list_for_each_entry_safe(prange, tmp, &remove_list, update_list) {
        svm_range_unlink(prange);
    }
    
    // 2. 插入新范围
    list_for_each_entry(prange, &insert_list, list) {
        svm_range_add_to_svms(prange);
        svm_range_add_notifier_locked(mm, prange);
    }
    
    // 3. 更新属性
    list_for_each_entry(prange, &update_list, update_list) {
        svm_range_apply_attrs(p, prange, nattr, attrs);
    }
    
    // 4. 释放旧范围
    list_for_each_entry_safe(prange, tmp, &remove_list, update_list) {
        svm_range_free(prange, true);
    }
}

mutex_unlock(&svms->lock);

流程图:svm_range_add

复制代码
┌─────────────────────────────────────────┐
│   svm_range_add(p, start, size, ...)    │
└─────────────────────────────────────────┘
            ↓
┌─────────────────────────────────────────┐
│  初始化输出列表                          │
│  update_list, insert_list, remove_list  │
└─────────────────────────────────────────┘
            ↓
┌─────────────────────────────────────────┐
│  查找重叠范围                            │
│  interval_tree_iter_first(start, last)  │
└─────────────────────────────────────────┘
            ↓
         有重叠?
      ┌────┴────┐
      是        否
      ↓         ↓
┌──────────┐  ┌──────────────┐
│处理重叠   │  │创建新范围     │
│  ├克隆    │  │svm_range_new │
│  ├分割    │  └──────────────┘
│  └更新    │        ↓
└──────────┘     加入insert_list
      ↓              ↓
   下一个重叠? ←──────┘
      ↓ 无更多
┌─────────────────────────────────────────┐
│  处理间隙:创建新范围填充                 │
└─────────────────────────────────────────┘
            ↓
        有错误?
      ┌────┴────┐
      是        否
      ↓         ↓
┌──────────┐  ┌──────────────┐
│回滚:      │  │提交:         │
│释放所有   │  │返回输出列表   │
│新创建范围 │  │供调用者应用   │
└──────────┘  └──────────────┘

💡 重点提示

  1. 事务性svm_range_add是事务性的,要么全成功,要么全失败。

  2. 克隆而非修改:重叠的范围会被克隆,原范围保持不变直到事务成功。

  3. 区间树是关键:快速查找重叠范围,避免遍历所有范围。

  4. 分割的两种模式:只能从头部或尾部分割,不能从中间。

  5. 资源管理:分割时DMA地址数组和VRAM都需要相应分割。


⚠️ 常见陷阱

陷阱1:"直接修改现有范围"

  • ✅ 正确:克隆后修改,保持原范围直到提交。

陷阱2:"忘记更新区间树"

  • ✅ 正确:范围地址改变时,必须先从区间树删除,修改后再插入。

陷阱3:"分割后忘记调整VRAM页面计数"

  • ✅ 正确:svm_range_split_pages会更新vram_pages

陷阱4:"释放范围前不取消映射"

  • ✅ 正确:svm_range_freedo_unmap参数控制是否取消映射。

📝 实践练习

  1. 代码追踪

    bash 复制代码
    # 追踪范围创建
    grep -n "svm_range_add" drivers/gpu/drm/amd/amdkfd/kfd_svm.c
    
    # 查看分割实现
    grep -A 30 "svm_range_split" drivers/gpu/drm/amd/amdkfd/kfd_svm.c
  2. 思考题

    • 为什么分割只能从头或尾,不能从中间?
    • 如果两个范围完全相同属性,为什么不合并成一个?
    • svm_range_split_array如何处理VRAM页面计数?
  3. 调试练习

    bash 复制代码
    # 启用动态调试
    echo 'file kfd_svm.c line 2133 +p' > /sys/kernel/debug/dynamic_debug/control
    
    # 触发范围创建并查看日志
    dmesg | grep "svm_range_add"
  4. 画图练习

    画出以下场景的范围变化:

    • 现有: [1000-2000], [4000-5000]
    • 创建: [1500-4500]
    • 画出所有中间步骤

📚 本章小结

  • 创建svm_range_add处理复杂重叠,克隆-分割-更新模式
  • 查找:区间树提供O(log N)查找,支持精确和范围查询
  • 分割svm_range_split从头或尾分割,调整资源分配
  • 删除:三步走:unlink → remove_notifier → free
  • 事务性:保证操作的原子性,失败时自动回滚

范围管理是SVM的基础,理解这些操作对后续学习至关重要。


➡️ 下一步

掌握了范围管理后,下一章我们将学习内存迁移机制------如何在系统内存和GPU显存之间移动页面。


🔗 导航

相关推荐
DeeplyMind1 天前
07 - SVM内存迁移机制
svm·amdgpu·rocm·kfd·rocr
啊阿狸不会拉杆2 天前
《机器学习导论》第 13 章-核机器
人工智能·python·算法·机器学习·支持向量机·svm·核机器
DeeplyMind3 天前
05 - 进程与SVM的关系
svm·amdgpu·rocm·kfd
DeeplyMind4 天前
03 - AMDGPU驱动架构概览
svm·amdgpu·rocm·kfd
DeeplyMind5 天前
ROCm rocr-libhsakmt分析系列4: HsaMemFlags分析
rocm·rocr·libhsakmt·hsamemflags
DeeplyMind7 天前
附录A:AMDGPU SVM 属性类型
kfd·amdgpu svm
DeeplyMind7 天前
04 - SVM核心数据结构详解
svm·amdgpu·kfd
DeeplyMind9 天前
02 - SVM相关的Linux内核基础
hmm·rocm·kfd·共享虚拟内存·amdgpu svm
DeeplyMind10 天前
01 - 什么是SVM
svm·amdgpu·rocm·kfd