难度 : 🟡 进阶
预计学习时间 : 1.5-2小时
前置知识: C语言结构体、链表、红黑树基础
📋 概述
理解数据结构是掌握SVM实现的关键。AMDGPU SVM的核心数据结构复杂,承载了虚拟内存范围管理、页面状态跟踪、GPU映射信息等关键功能。本章将深入剖析四个核心数据结构,理解它们的设计意图和使用方式。
想象一下:每个SVM内存范围就像图书馆的一本书,我们需要:
- 📚 记录它的位置(起始地址、大小)
- 🏷️ 标记它的属性(权限、位置偏好)
- 🔗 维护它的关系(与其他范围的关系)
- 📊 跟踪它的状态(是否有效、是否已映射)
4.1 svm_range - 内存范围表示
结构体定义
变量有点多,一个方法是先理解range这个概念所代表的地址范围相关属性;然后在理解因share所带来的属性;最后理解因管理的需求所需要的属性。
c
// 文件: kfd_svm.h
struct svm_range {
// === 组织管理 ===
struct svm_range_list *svms; // 所属的范围列表
struct interval_tree_node it_node; // 区间树节点
struct list_head list; // 链表节点
struct list_head update_list; // 更新队列链表
struct list_head deferred_list; // 延迟处理链表
struct list_head child_list; // 子范围列表
// === 地址范围 ===
unsigned long start; // 起始地址(页号)
unsigned long last; // 结束地址(页号)
uint64_t npages; // 页面数量
// === 同步保护 ===
struct mutex migrate_mutex; // 迁移互斥锁
struct mutex lock; // 范围锁
unsigned int saved_flags; // 保存的内存分配标志
// === 内存位置 ===
uint32_t preferred_loc; // 偏好位置 (0=CPU, GPU ID)
uint32_t prefetch_loc; // 预取位置
uint32_t actual_loc; // 实际位置
uint64_t vram_pages; // VRAM页面数
// === 物理映射 ===
dma_addr_t *dma_addr[MAX_GPU_INSTANCE]; // DMA地址数组
struct ttm_resource *ttm_res; // TTM资源(VRAM)
uint64_t offset; // VRAM内偏移
struct svm_range_bo *svm_bo; // 关联的BO
struct list_head svm_bo_list; // BO的范围列表节点
// === 访问控制 ===
uint32_t flags; // 标志位
uint8_t granularity; // 迁移粒度(log2页数)
DECLARE_BITMAP(bitmap_access, MAX_GPU_INSTANCE); // 访问位图
DECLARE_BITMAP(bitmap_aip, MAX_GPU_INSTANCE); // AIP位图
bool mapped_to_gpu; // 是否已映射到GPU
// === 状态跟踪 ===
atomic_t invalid; // 无效标志
ktime_t validate_timestamp; // 验证时间戳
atomic_t queue_refcount; // 队列引用计数
// === MMU通知 ===
struct mmu_interval_notifier notifier; // MMU通知器
// === 工作队列 ===
struct svm_work_list_item work_item; // 工作项
};
关键字段详解
1. 地址范围字段
c
unsigned long start; // 起始地址(页号)
unsigned long last; // 结束地址(页号)
uint64_t npages; // 页面数量
重要 :start 和 last 是页号,不是字节地址!
示例:
start = 0x7f8a2c001 (虚拟地址 0x7f8a2c001000 的页号)
last = 0x7f8a2c100 (虚拟地址 0x7f8a2c100000 的页号)
npages = last - start + 1 = 0x100 = 256 页
覆盖地址范围:
0x7f8a2c001000 - 0x7f8a2c100FFF (1MB)
为什么用页号
- 节省存储空间(页号比字节地址小12位)
- 页面对齐是自然的
- 与MMU的工作单位一致
2. 位置标记字段
c
uint32_t preferred_loc; // 偏好位置
uint32_t prefetch_loc; // 预取位置
uint32_t actual_loc; // 实际位置
位置编码:
0: CPU(系统内存)1, 2, 3, ...: GPU ID
c
// 示例:范围偏好在GPU 0
prange->preferred_loc = 0; // GPU 0
prange->actual_loc = 0; // 当前在系统内存
// GPU访问后可能迁移
prange->actual_loc = 0; // 现在在GPU 0的VRAM
三个位置的区别:
- preferred_loc: 用户指定的偏好(通过IOCTL设置)
- prefetch_loc: 上次预取到的位置
- actual_loc: 当前实际所在位置
3. 物理映射字段
c
dma_addr_t *dma_addr[MAX_GPU_INSTANCE]; // 每个GPU的DMA地址数组
struct ttm_resource *ttm_res; // VRAM的TTM资源
uint64_t offset; // VRAM内的偏移
DMA地址数组:
prange->dma_addr[0] → [页0的DMA地址, 页1的DMA地址, ..., 页N的DMA地址]
prange->dma_addr[1] → [页0的DMA地址, 页1的DMA地址, ..., 页N的DMA地址]
...
每个GPU都需要独立的DMA映射,因为:
- 不同GPU有不同的MMU
- DMA地址可能不同
示例:256页的范围
c
// 系统内存情况
prange->npages = 256;
prange->dma_addr[0] = kvcalloc(256, sizeof(dma_addr_t), ...);
prange->dma_addr[0][0] = 0x8000_1000; // 页0的DMA地址
prange->dma_addr[0][1] = 0x8000_2000; // 页1的DMA地址
// ...
// VRAM情况
prange->ttm_res = (TTM资源描述VRAM块);
prange->offset = 0x1000; // 在VRAM块内的偏移
4. 访问控制字段
c
DECLARE_BITMAP(bitmap_access, MAX_GPU_INSTANCE); // 哪些GPU可访问
DECLARE_BITMAP(bitmap_aip, MAX_GPU_INSTANCE); // 哪些GPU可就地访问
位图使用:
c
// GPU 0和GPU 2可以访问这个范围
set_bit(0, prange->bitmap_access);
set_bit(2, prange->bitmap_access);
// 检查GPU 1是否可以访问
if (test_bit(1, prange->bitmap_access)) {
// 可以访问
}
// AIP: Access In Place(无需迁移即可访问)
// 通常通过PCIe或XGMI直接访问其他GPU的VRAM
set_bit(1, prange->bitmap_aip); // GPU 1可以直接访问
可视化:
8个GPU系统:
bitmap_access: [1][0][1][1][0][0][0][0]
GPU0可访问 GPU2和3可访问
bitmap_aip: [1][1][0][0][0][0][0][0]
GPU0和1可以就地访问(无需迁移)
5. 同步字段
c
struct mutex migrate_mutex; // 迁移互斥锁
struct mutex lock; // 范围锁
锁的层次:
c
// migrate_mutex: 保护迁移操作(粗粒度)
mutex_lock(&prange->migrate_mutex);
// 执行页面迁移操作
svm_migrate_ram_to_vram(...);
mutex_unlock(&prange->migrate_mutex);
// lock: 保护范围的元数据(细粒度)
svm_range_lock(prange); // 也会保存内存分配标志
prange->start = new_start;
prange->last = new_last;
svm_range_unlock(prange);
为什么需要两个锁
migrate_mutex: 迁移操作耗时长,需要独立的锁lock: 快速访问/修改元数据,不能被迁移阻塞
4.2 svm_range_list - 范围管理
结构体定义
c
// 文件: kfd_priv.h
struct svm_range_list {
// === 组织结构 ===
struct mutex lock; // 保护整个列表
struct rb_root_cached objects; // 区间树根(红黑树)
struct list_head list; // 链表头
// === 延迟工作 ===
struct work_struct deferred_list_work; // 延迟工作
struct list_head deferred_range_list;// 延迟范围列表
spinlock_t deferred_list_lock; // 延迟列表锁
// === 状态跟踪 ===
atomic_t evicted_ranges; // 被驱逐的范围数
atomic_t drain_pagefaults; // 排空缺页标志
struct delayed_work restore_work; // 恢复工作
// === GPU支持 ===
DECLARE_BITMAP(bitmap_supported, MAX_GPU_INSTANCE); // 支持SVM的GPU
// === 缺页处理 ===
struct task_struct *faulting_task; // 缺页任务
uint64_t checkpoint_ts[MAX_GPU_INSTANCE]; // 检查点时间戳
// === 配置 ===
uint8_t default_granularity;// 默认迁移粒度
// === CRIU支持 ===
struct list_heea criu_svm_metadata_list; // CRIU元数据
};
数据组织方式
svm_range_list 同时使用了两种数据结构:
1. 区间树(Interval Tree)
基于红黑树实现,用于快速查找地址范围:
查询:给定地址0x7f8a2c005000,找到包含它的范围
[0x1000-0x2000]
/ \
[0x500-0x800] [0x5000-0x8000] ← 找到了!
/ \
[0x100-0x200] [0x9000-0xA000]
时间复杂度:O(log N)
使用示例:
c
// 查找包含地址的范围
struct svm_range *prange;
unsigned long addr = 0x7f8a2c005; // 页号
prange = svm_range_from_addr(&p->svms, addr, NULL);
if (prange) {
pr_debug("Found range [0x%lx-0x%lx]\n",
prange->start, prange->last);
}
2. 链表(Linked List)
用于顺序遍历所有范围:
svms->list → [range1] → [range2] → [range3] → NULL
优势:
- 顺序遍历所有范围
- 插入/删除 O(1)
- 适合"对所有范围执行操作"的场景
使用示例:
c
// 遍历所有SVM范围
struct svm_range *prange;
list_for_each_entry(prange, &p->svms.list, list) {
pr_debug("Range [0x%lx-0x%lx]\n",
prange->start, prange->last);
}
为什么需要两种结构
区间树:擅长查找
- "这个地址属于哪个范围?" → O(log N)
链表:擅长遍历
- "遍历所有范围并驱逐" → O(N),但常数小
- "插入/删除" → O(1)
组合使用:各取所长!
工作队列机制
c
struct work_struct deferred_list_work; // 延迟工作
struct delayed_work restore_work; // 恢复工作
延迟工作的用途:
c
// GPU缺页时,不在中断上下文处理,而是延迟
svm_range_add_list_work(
&p->svms, prange, mm, SVM_OP_ADD_RANGE_AND_MAP);
// 稍后在工作队列上下文执行
// → svm_range_deferred_list_work()
// → 处理实际的迁移和映射
恢复工作:
c
// VRAM被驱逐后,延迟恢复
schedule_delayed_work(&svms->restore_work,
msecs_to_jiffies(AMDGPU_SVM_RANGE_RESTORE_DELAY_MS));
// 稍后执行恢复
// → svm_range_restore_work()
4.3 svm_range_bo - Buffer Object管理
结构体定义
c
// 文件: kfd_svm.h
struct svm_range_bo {
struct amdgpu_bo *bo; // TTM Buffer Object
struct kref kref; // 引用计数
struct list_head range_list; // 共享此BO的范围列表
spinlock_t list_lock; // 列表锁
struct amdgpu_amdkfd_fence *eviction_fence;// 驱逐fence
struct work_struct eviction_work; // 驱逐工作
uint32_t evicting; // 驱逐中标志
struct work_struct release_work; // 释放工作
struct kfd_node *node; // 所属的KFD节点
};
为什么需要svm_range_bo
SVM范围可能很大,不适合分配一整块VRAM。解决方案:
大范围分割成多个小块:
原始SVM范围: [0x1000 - 0x10000] (60MB)
↓ 分割
BO1[4MB] BO2[4MB] BO3[4MB] ... BO15[4MB]
↓ ↓ ↓
Range1 Range2 Range3 ...
多个svm_range可以共享一个svm_range_bo
引用计数管理
c
struct kref kref; // 引用计数
使用模式:
c
// 增加引用
struct svm_range_bo *svm_bo = svm_range_bo_ref(prange->svm_bo);
// 减少引用(可能释放)
svm_range_bo_unref(svm_bo);
// 如果引用计数为0 → 释放BO
为什么需要引用计数
BO可以被多个范围共享:
svm_bo ← range1
← range2
← range3
只有当所有范围都释放后,BO才能被释放
驱逐机制
c
struct amdgpu_amdkfd_fence *eviction_fence; // 驱逐fence
struct work_struct eviction_work; // 驱逐工作
uint32_t evicting; // 驱逐状态
驱逐流程:
1. VRAM不足,TTM需要驱逐
↓
2. 触发eviction_fence信号
↓
3. 调度eviction_work
↓ svm_range_evict_svm_bo_worker()
4. 处理驱逐:
- 取消GPU映射
- 标记范围为无效
- 释放VRAM
↓
5. 标记evicting状态
代码示例:
c
// 文件: kfd_svm.c
static void svm_range_evict_svm_bo_worker(struct work_struct *work)
{
struct svm_range_bo *svm_bo;
svm_bo = container_of(work, struct svm_range_bo, eviction_work);
// 驱逐所有使用此BO的范围
list_for_each_entry(prange, &svm_bo->range_list, svm_bo_list) {
// 取消映射
svm_range_unmap_from_gpu(...);
// 标记无效
atomic_set(&prange->invalid, 1);
}
// 释放VRAM BO
amdgpu_bo_unref(&svm_bo->bo);
}
4.4 svm_work_list_item - 异步工作项
结构体定义
c
// 文件: kfd_svm.h
enum svm_work_list_ops {
SVM_OP_NULL,
SVM_OP_UNMAP_RANGE,
SVM_OP_UPDATE_RANGE_NOTIFIER,
SVM_OP_UPDATE_RANGE_NOTIFIER_AND_MAP,
SVM_OP_ADD_RANGE,
SVM_OP_ADD_RANGE_AND_MAP
};
struct svm_work_list_item {
enum svm_work_list_ops op; // 操作类型
struct mm_struct *mm; // 进程内存描述符
};
使用场景
某些SVM操作不能在当前上下文执行(如中断上下文),需要延迟:
GPU Page Fault (中断上下文)
↓ 不能直接处理(不能睡眠)
创建work_item
↓
添加到deferred_list
↓ schedule_work()
工作队列上下文
↓ 可以睡眠,可以分配内存
执行实际操作
操作类型详解
c
// SVM_OP_ADD_RANGE_AND_MAP: 添加范围并映射到GPU
work_item.op = SVM_OP_ADD_RANGE_AND_MAP;
work_item.mm = current->mm;
// 稍后在工作队列执行
// → 分配物理页面
// → 建立GPU页表映射
常见操作:
-
SVM_OP_UNMAP_RANGE: 取消GPU映射
- CPU修改了页面 → 需要使GPU映射失效
-
SVM_OP_UPDATE_RANGE_NOTIFIER: 更新MMU notifier
- 范围大小改变 → 需要重新注册notifier
-
SVM_OP_ADD_RANGE_AND_MAP: 添加范围并映射
- GPU缺页 → 需要迁移页面并建立映射
4.5 数据结构关系图
让我们把这些结构串联起来:
┌─────────────────────────────────────────────────────┐
│ kfd_process (进程) │
│ ┌───────────────────────────────────────────────┐ │
│ │ svm_range_list (svms) │ │
│ │ ┌────────────────────────────────────────┐ │ │
│ │ │ objects (区间树) │ │ │
│ │ │ ┌─────────────────────────────┐ │ │ │
│ │ │ │ svm_range (范围1) │ │ │ │
│ │ │ │ • start=0x1000 │ │ │ │
│ │ │ │ • last=0x2000 │ │ │ │
│ │ │ │ • actual_loc=0 (CPU) │ │ │ │
│ │ │ │ • dma_addr[0] → [地址数组] │ │ │ │
│ │ │ │ • svm_bo → ────────────────│─┐ │ │ │
│ │ │ └───────────────┼─────────────┘ │ │ │ │
│ │ │ ┌───────────────┼─────────────┐ │ │ │ │
│ │ │ │ svm_range (范围2) │ │ │ │ │
│ │ │ │ • start=0x3000 │ │ │ │ │
│ │ │ │ • actual_loc=1 │ │ │ │ │
│ │ │ │ • ttm_res → VRAM │ │ │ │ │
│ │ │ │ • svm_bo → ─────────────┐ │ │ │ │ │
│ │ │ └──────────────────────────│──┘ │ │ │ │
│ │ └────────────────────────────────┼────┼──┘ │ │
│ │ │ │ │ │
│ │ list (链表): range1 → range2 → │ │ │ │
│ └───────────────────────────────────┼────┼──────┘ │
└──────────────────────────────────────┼────┼─────────┘
↓ ↓
┌──────────────────────────────┐
│ svm_range_bo (BO1) │
│ • bo → amdgpu_bo (VRAM 4MB) │
│ • kref = 2 (两个范围共享) │
│ • range_list: [range1, │
│ range2] │
│ • eviction_fence │
└──────────────────────────────┘
典型场景演示
场景1:CPU分配内存
c
// 用户调用malloc
void *ptr = malloc(8MB);
// 内核创建svm_range
struct svm_range *prange = kzalloc(...);
prange->start = 0x7f8a2c000; // 起始页号
prange->last = 0x7f8a2d000; // 结束页号
prange->npages = 0x1000; // 4096页 = 16MB
prange->actual_loc = 0; // 在CPU(系统内存)
prange->preferred_loc = 0; // 偏好CPU
prange->svms = &process->svms; // 属于进程的svms
// 添加到svms
svm_range_add_to_svms(prange); // 加入区间树和链表
场景2:GPU首次访问
c
// GPU访问ptr → Page Fault
// → svm_range_restore_pages()
// 1. 查找范围
prange = svm_range_from_addr(&p->svms, fault_addr, NULL);
// 2. 决定是否迁移(假设迁移到GPU 0)
if (should_migrate_to_vram(prange)) {
// 3. 分配VRAM
svm_range_vram_node_new(node, prange, false);
// 创建svm_range_bo和amdgpu_bo
// 4. 迁移页面
svm_migrate_ram_to_vram(...);
// 5. 更新状态
prange->actual_loc = 1; // GPU 0
prange->vram_pages = prange->npages;
}
// 6. 建立GPU页表映射
svm_range_map_to_gpu(prange, ...);
set_bit(0, prange->bitmap_access); // GPU 0可访问
prange->mapped_to_gpu = true;
场景3:CPU修改页面
c
// CPU写入ptr[0] = 42;
// → MMU Notifier触发
// → svm_range_cpu_invalidate_pagetables()
atomic_set(&prange->invalid, 1); // 标记无效
// 创建工作项
prange->work_item.op = SVM_OP_UNMAP_RANGE;
svm_range_add_list_work(&p->svms, prange, mm, SVM_OP_UNMAP_RANGE);
// 稍后在工作队列:
// → svm_range_unmap_from_gpu(prange, ...);
clear_bit(0, prange->bitmap_access); // GPU 0不再可访问
💡 重点提示
-
页号vs字节地址 :
start和last是页号,需要左移12位才是字节地址。 -
两种组织方式:区间树用于查找,链表用于遍历,各有优势。
-
引用计数 :
svm_range_bo使用kref管理生命周期,可被多个范围共享。 -
异步处理:很多操作通过工作队列异步执行,避免阻塞。
-
位图的威力 :
bitmap_access和bitmap_aip用一个字就能表示8个GPU的状态。
⚠️ 常见误区
❌ 误区1:"一个svm_range对应一个物理块"
- ✅ 正确理解:一个范围可能跨越系统内存和VRAM,或者多个物理块。
❌ 误区2:"actual_loc改变时物理页面立即移动"
- ✅ 正确理解:actual_loc是状态记录,实际迁移是独立的操作。
❌ 误区3:"bitmap_access和mapped_to_gpu是重复的"
- ✅ 正确理解:bitmap记录哪些GPU可访问,mapped_to_gpu记录是否已建立映射。
❌ 误区4:"所有字段都需要svms->lock保护"
- ✅ 正确理解:不同字段有不同的保护机制(svms->lock、prange->lock、atomic等)。
📝 实践练习
-
结构体计算 :
计算
struct svm_range的大小:bashpahole drivers/gpu/drm/amd/amdkfd/kfd_svm.o -C svm_range -
代码追踪:
bash# 查找创建svm_range的位置 grep -n "kzalloc.*svm_range" drivers/gpu/drm/amd/amdkfd/kfd_svm.c # 查找区间树的使用 grep -n "interval_tree_" drivers/gpu/drm/amd/amdkfd/kfd_svm.c -
思考题:
- 为什么需要
migrate_mutex和lock两个锁? - 区间树的键值是什么?
- 如果多个GPU访问同一个范围,DMA地址如何管理?
- 为什么需要
-
画图练习 :
画出3个SVM范围共享2个BO的关系图。
📚 本章小结
- svm_range: 核心结构,表示一个虚拟内存范围,包含地址、位置、映射等信息
- svm_range_list: 管理所有范围,使用区间树+链表双重组织
- svm_range_bo: 管理VRAM Buffer Object,支持共享和引用计数
- svm_work_list_item: 异步工作项,支持延迟执行操作
这四个结构是SVM实现的骨架,后续的所有操作都围绕它们展开。
📖 扩展阅读
➡️ 下一步
理解了核心数据结构后,我们将在下一章探讨进程如何管理这些SVM范围,以及进程生命周期中SVM的初始化和清理。
🔗 导航
- 上一章:03 - AMDGPU驱动架构概览
- 下一章: 审核中...
- 返回目录: AMD ROCm-SVM技术的实现与应用深度分析目录