前言
drm_gpuvm是 DRM 里的「GPU 地址空间管理器」,替驱动维护整段 GPU 虚拟地址空间。它遵循一条贯穿整个 DRM 子系统的设计原则:框架只实现与硬件无关的通用逻辑,凡是涉及具体硬件行为、驱动私有内存布局的动作,都通过struct drm_gpuvm_ops的回调交给驱动完成。这个「机制与策略分离」的取向,在前几章的设计里也反复出现。
这篇把drm_gpuvm_ops的 9 个回调讲清楚:
- 这 9 个回调怎么分组、每组负责什么;
- 为什么框架不亲自做这些事,而是交给驱动。
按「服务谁」归类,这 9 个回调恰好是三组,本文按从对象生命周期到地址空间落地的顺序,分组讲清楚。
1. 9 个回调的全景
structdrm_gpuvm_ops{/* 组一:对象生命周期 & 驻留 */void(*vm_free)(structdrm_gpuvm*gpuvm);structdrm_gpuvm_bo*(*vm_bo_alloc)(void);void(*vm_bo_free)(structdrm_gpuvm_bo*vm_bo);int(*vm_bo_validate)(structdrm_gpuvm_bo*vm_bo,structdrm_exec*exec);/* 组二:op 内存分配 */structdrm_gpuva_op*(*op_alloc)(void);void(*op_free)(structdrm_gpuva_op*op);/* 组三:地址空间变更的落地 */int(*sm_step_map)(structdrm_gpuva_op*op,void*priv);int(*sm_step_remap)(structdrm_gpuva_op*op,void*priv);int(*sm_step_unmap)(structdrm_gpuva_op*op,void*priv);};| 分组 | 回调 | 服务对象 |
|---|---|---|
| 一、对象生命周期 & 驻留 | vm_free、vm_bo_alloc/vm_bo_free、vm_bo_validate | GPUVM / BO 对象本身 |
| 二、op 内存分配 | op_alloc/op_free | 描述地址空间变更的drm_gpuva_op |
| 三、地址空间变更落地 | sm_step_map/sm_step_remap/sm_step_unmap | 每一步的页表插入 / 拆分 / 删除 |
下面按这个顺序逐组说清职责。
2. 组一:对象生命周期 & 驻留管理
这一组管的是 GPUVM 和 BO 这两类对象本身的「生老病死」与「显存换入换出」,是整套回调里最基础、也最先被用到的。
| 回调 | 触发点 | 用途 | 可选性 |
|---|---|---|---|
vm_free | drm_gpuvm最后一个引用被丢弃 | 释放整个 VM | 强制 |
vm_bo_alloc/vm_bo_free | 分配 / 释放drm_gpuvm_bo | vm_bo是「GEM 对象 ↔ VM」的连接件 | 可选 |
vm_bo_validate | drm_gpuvm_validate() | 提交前把被换出(evicted)的 BO 重新验证回显存 | 用到才需 |
几个要点:
vm_free是整个 ops 里唯一强制的回调。框架在引用归零时会drm_WARN_ON(!ops->vm_free),然后调ops->vm_free(gpuvm)。因为drm_gpuvm通常内嵌在驱动更大的结构体里,框架只拿到内嵌字段的指针,根本不知道该怎么释放外层对象。vm_bo_alloc/vm_bo_free定制vm_bo的内存布局。drm_gpuvm_bo是连接一个 GEM 对象与一个 VM 的中间结构,很多驱动想把它内嵌进自己的结构体,于是框架分配时先看驱动有没有实现:if(ops&&ops->vm_bo_alloc)vm_bo=ops->vm_bo_alloc();elsevm_bo=kzalloc(sizeof(*vm_bo),GFP_KERNEL);不实现就走默认
kzalloc,实现了就用驱动定制版。vm_bo_validate是驻留路径。它通常内部调用驱动版的ttm_bo_validate(),把被驱逐的 BO 搬回显存。这管的是「物理显存驻留」,和「虚拟地址布局」是正交的两件事。
3. 组二:op 列表的内存分配配套
op_alloc/op_free只做一件小事:分配和释放drm_gpuva_op。
drm_gpuva_op是框架描述「一次地址空间变更需要哪些步骤」的数据结构。和vm_bo同理,很多驱动希望把drm_gpuva_op内嵌进自己的结构体里携带私有字段,于是框架在需要分配 op 时先看驱动有没有实现op_alloc,没有就用默认kzalloc。
这两个回调也是可选的,纯粹是内存布局定制,不涉及任何页表语义。
4. 组三:地址空间变更的「落地三件套」
这三个是把「地址空间该怎么变」真正写进驱动页表的回调。框架内部会遍历地址空间、算出一次请求需要做哪些插入、拆分、删除,然后逐步回调给驱动落地:
| 回调 | 语义动作 |
|---|---|
sm_step_map | 插入一条新映射 |
sm_step_remap | 把一条已有映射拆开(原映射被新请求部分覆盖时) |
sm_step_unmap | 删除一条已有映射 |
一个容易忽略的点:这三步不是「一个请求只触发一步」。一条新映射若覆盖了旧映射,框架必须先 remap / unmap 掉旧的,再 map 新的,因此一次请求可能连续回调好几步。
框架对这三者有强制要求,入口会检查回调是否齐全:
if(unlikely(!(ops&&ops->sm_step_map&&ops->sm_step_remap&&ops->sm_step_unmap)))return-EINVAL;也就是说:要用框架的地址空间变更引擎,就必须提供这组落地回调——框架只出「该做哪些步骤」的决策,驱动出「怎么写进硬件页表」的执行。
5. 为什么框架不自己做,而是交给驱动
这是本文的核心问题。框架明明已经算出了「该做哪些步骤」,为什么不干脆把页表也一起改了?答案可以归纳成四条边界。
5.1 硬件差异:页表格式框架根本不认识
不同 GPU 的页表结构、页大小、权限位编码、TLB 失效方式各不相同(AMD 的 GPUVM、Intel、Nouveau、Panthor 各有各的 MMU)。框架若要亲自写页表,就得内置所有厂商的 MMU 知识——这既不现实,也违背分层设计。
所以框架只做厂商无关的部分:地址区间的 split/merge 决策(这部分逻辑对所有 GPU 都一样);把厂商相关的落地动作(sm_step_*)留给驱动。这是最经典的「机制与策略分离」。
5.2 内存布局:对象由驱动拥有,框架不知如何分配/释放
drm_gpuvm、drm_gpuvm_bo、drm_gpuva_op在实际驱动里几乎总是内嵌在更大的驱动私有结构体中。框架手里只有一个内嵌字段的指针,无法知道外层结构体的大小、也无法知道额外字段该怎么初始化和回收。
这正是vm_free、vm_bo_alloc/free、op_alloc/free存在的原因:谁分配、谁定义布局,谁就得负责释放。框架只在「需要一个对象」和「对象该没了」的时机点回调,具体怎么分配/释放由拥有者决定。
5.3 时机与上下文:只有驱动知道何时真正写硬件
改页表往往不能立刻同步执行,而要挂到 fence、排进 DMA/SDMA 队列、和调度器与 dma_resv 锁协同。什么时候刷 TLB、是否要等某个 fence signalled、批处理还是逐条提交——这些都是驱动的调度语义。
框架把sm_step_*设计成「回调 + 驱动传入的priv指针」,本质是把执行时机的控制权交还给驱动:框架给你一步步的「要做什么」,你自己决定「什么时候、以什么并发方式做」。
5.4 驻留是驱动的独立子系统
vm_bo_validate背后是 TTM/BO 的驱逐与回迁体系,这套东西和地址空间管理是正交的两个子系统。框架没有理由、也没有能力去替驱动决定一个 BO 该不该被搬回显存、搬到哪个内存域。它只在drm_gpuvm_validate()需要时,把「这个被驱逐的 BO 需要验证」这一信号回调出去。
6. 小结
drm_gpuvm_ops的 9 个回调,按职责分三组:- 组一:对象生命周期 & 驻留:
vm_free(强制)、vm_bo_alloc/vm_bo_free、vm_bo_validate——管 GPUVM/BO 对象的分配释放与显存驻留; - 组二:op 内存分配配套:
op_alloc/op_free——可选,定制drm_gpuva_op的内存布局; - 组三:地址空间变更落地:
sm_step_map/sm_step_remap/sm_step_unmap——框架强制要求,把每一步写进硬件页表。
- 组一:对象生命周期 & 驻留:
框架不自己动手,而是交给驱动,根本原因是机制与策略分离:框架掌握厂商无关的「算法」(对象生命周期时机、地址区间怎么切),驱动掌握厂商相关的「执行」(对象内存布局、页表长什么样、何时写硬件、BO 怎么驻留)。