1. GPU KMD 内存管理概述
在GPU内核模式驱动(Kernel Mode Driver, KMD)开发中,内存管理是最核心也是最复杂的模块之一。不同于CPU侧相对统一的内存管理方式,GPU内存管理需要同时兼顾设备特性和操作系统约束。现代GPU通常采用统一内存架构(Unified Memory Architecture, UMA),这使得GPU虚拟地址(GPU Virtual Address, GPUVA)的管理变得尤为关键。
GPUVA空间可以看作GPU视角下的"虚拟内存",它通过页表映射到物理内存(可能是设备专用显存或系统共享内存)。在Linux KMD中,这个机制通常通过DRM(Direct Rendering Manager)子系统的GPUVA管理器来实现。与传统的CPU虚拟内存管理相比,GPUVA有三大显著特征:
- 地址空间独立性:GPU拥有完全独立的虚拟地址空间,不受进程CPU虚拟地址空间限制
- 映射粒度灵活:支持从4KB到2MB甚至1GB不等的页面大小
- 同步要求严格:需要显式管理CPU与GPU之间的地址空间同步
2. GPUVA分配核心机制解析
2.1 GPUVA空间组织结构
现代GPU通常采用48位虚拟地址空间,理论可寻址范围达256TB。以NVIDIA的Hopper架构为例,其GPUVA空间主要划分为以下几个关键区域:
| 地址范围 | 区域用途 | 映射特性 |
|---|---|---|
| 0x0000_0000_0000 | 保留区(不可用) | - |
| 0x0000_0100_0000 | 用户态分配区 | 按需分页,支持迁移 |
| 0x0000_8000_0000 | 内核驱动专用区 | 固定映射,不可换出 |
| 0xFFFF_FFFF_F000 | 保护页(防溢出) | - |
在Linux KMD实现中,这个布局通过drm_gpuva_manager结构体管理:
c复制struct drm_gpuva_manager {
struct drm_mm mm; // 底层区间管理器
u64 start; // 地址空间起始
u64 end; // 地址空间结束
struct list_head regions; // 已分配区域链表
};
2.2 分配算法实现细节
GPUVA分配主要面临两个技术挑战:避免地址碎片化和快速查找空闲区域。主流驱动采用改进的伙伴系统(Buddy System)结合红黑树(RB-Tree)的方案:
- 初始分配阶段:使用size-segregated free lists快速匹配最接近2^n大小的块
- 碎片整理阶段:通过RB-Tree查找相邻空闲区域进行合并
- 回退机制:当连续空间不足时,尝试执行GPUVA压缩(类似内存紧缩)
以AMDGPU驱动为例的核心分配逻辑:
c复制int amdgpu_vm_alloc_pts(struct amdgpu_device *adev,
struct amdgpu_vm *vm,
uint64_t size,
uint64_t *addr) {
// 尝试快速分配
err = drm_mm_insert_node_in_range(&vm->mm, &node,
size, alignment,
0, start, end,
DRM_MM_INSERT_BEST);
if (err) {
// 触发碎片整理
amdgpu_vm_defragment(vm);
err = drm_mm_insert_node_in_range(...);
}
*addr = node.start;
return err;
}
关键提示:在实现自定义分配器时,必须考虑GPU硬件的TLB特性。例如NVIDIA GPU的TLB条目有限,过小的分配粒度会导致频繁的TLB刷新,建议最小分配尺寸不低于64KB。
3. 实战:实现自定义GPUVA分配器
3.1 环境准备与基础结构
我们基于Linux 6.1内核和DRM框架构建一个简易GPUVA管理器。首先定义核心数据结构:
c复制struct my_gpuva_allocator {
struct drm_gpuva_manager base;
spinlock_t lock;
atomic64_t alloc_count;
struct kmem_cache *slab_cache;
};
struct my_gpuva_region {
struct drm_gpuva va;
u32 flags;
struct list_head user_ptrs;
};
初始化过程需要特别注意GPU架构特性:
c复制int my_gpuva_init(struct my_gpuva_allocator *alloc,
u64 start, u64 size)
{
// 确保起始地址对齐GPU页大小(通常2MB)
start = ALIGN(start, SZ_2M);
size = ALIGN_DOWN(size, SZ_2M);
drm_gpuva_manager_init(&alloc->base, start, s
