5.2.4 结尾留下了一个悬念:导入方通过 dma_buf_attach → dma_buf_map_attachment 拿到 sg_table 之后,这张表里的 DMA 地址就固定指向了 buffer 当前所在的物理位置。可如果导出方是 GPU,它的显存随时可能因为 TTM eviction、显存吃紧而把这块 BO 从 VRAM 搬到 GTT、甚至换出。一旦 buffer 搬了家,导入方手里那张旧 sg_table 就变成了指向错误位置的「野地址」。
「静态导入方」的解法简单粗暴:map 时 pin 住后端存储,映射存在期间谁也别想动它。代价是显存被长期pin住——一块被 RDMA 网卡映射走的 GPU BO,哪怕 GPU 自己显存告急也无法迁移或换出。对渲染、AI 这类要反复共享大量 buffer 的场景,这种「钉死」是不可接受的。
Dynamic dma-buf(动态 dma-buf) 就是为化解这对矛盾而生:导入方不再要求 pin,而是向导出方注册一个 move_notify 回调;导出方每次要搬动 buffer 前,先逐个通知所有动态导入方「我要动了,请把你的旧映射作废、稍后重建」。于是显存重新变得可迁移,共享也不再以pin为代价。
1. 静态 vs 动态
回顾 5.2.4:导入方是否「动态」,完全取决于 dma_buf_dynamic_attach() 时传入的 importer_ops 是否为 NULL。
struct dma_buf_attachment *dma_buf_attach(struct dma_buf *dmabuf,
struct device *dev)
{
return dma_buf_dynamic_attach(dmabuf, dev, NULL, NULL); /* 静态:importer_ops = NULL */
}
传入非 NULL 的 importer_ops(其中带 move_notify),就成了动态导入方。二者的行为差异可以浓缩成一张表:
| 判定函数 | dma_buf_attachment_is_dynamic() = false | = true |
| map 时是否 pin 后端 | 是(pin_on_map 成立) | 否 |
| buffer 可否被迁移 | 映射期间不可 | 可——迁移前经 move_notify 通知 |
| 迁移后旧 sg_table | 不存在此问题 | 立即失效,须重新 map |
| 同步责任 | core 帮忙等 KERNEL fence | 导入方自己遵守 write fence |
| 典型使用者 | 简单 scanout、不支持 ODP 的 RDMA | GPU↔GPU 互导、支持 move 的现代驱动 |
一句话:静态用「pin 住不许动」换简单,动态用「随时准备重映射」换显存不被 pin。
2. move_notify:动态机制的心脏
动态机制的全部精髓,就在导出方搬移 buffer 前调用的这一个函数:
void dma_buf_move_notify(struct dma_buf *dmabuf)
{
struct dma_buf_attachment *attach;
dma_resv_assert_held(dmabuf->resv);
list_for_each_entry(attach, &dmabuf->attachments, node)
if (attach->importer_ops)
attach->importer_ops->move_notify(attach);
}
struct dma_buf_attach_ops 的定义印证了这套契约:
struct dma_buf_attach_ops {
bool allow_peer2peer; /* 导入方能否处理无 struct page 的对端资源 */
void (*move_notify)(struct dma_buf_attachment *attach); /* [可选] buffer 正在移动 */
};
内核头文件对 move_notify 的语义有极精确的描述,值得逐条拆解:
- “Mappings stay valid and are not directly affected by this callback”——回调触发的瞬间,旧映射在物理上仍然有效,move_notify 只是一个「预警」,不是「已经搬完」。
- “But the DMA-buf can now be in a different physical location, so all mappings should be destroyed and re-created as soon as possible”——导入方收到通知后,应尽快作废旧 sg_table 并在下次使用前重建。
- “New mappings can be created after this callback returns, and will point to the new location”——回调返回后新建的映射,指向的就是搬移后的新位置。
3. pin / unpin:动态导入方的「临时钉住」
动态机制并非完全否定 pin,而是把 pin 从「map 时强制」降级为「按需临时」。dma_buf_pin() / dma_buf_unpin() 专供动态导入方在确实需要一段稳定物理地址(如把 buffer 拿去做 scanout 上屏)时使用:
int dma_buf_pin(struct dma_buf_attachment *attach)
{
struct dma_buf *dmabuf = attach->dmabuf;
int ret = 0;
WARN_ON(!attach->importer_ops); /* 只有动态导入方能调用 */
dma_resv_assert_held(dmabuf->resv);
if (dmabuf->ops->pin)
ret = dmabuf->ops->pin(attach);
return ret;
}
void dma_buf_unpin(struct dma_buf_attachment *attach)
{
struct dma_buf *dmabuf = attach->dmabuf;
WARN_ON(!attach->importer_ops);
dma_resv_assert_held(dmabuf->resv);
if (dmabuf->ops->unpin)
dmabuf->ops->unpin(attach);
}
三个约束值得留意:
于是三者构成一个清晰的层次:move_notify 是常态(可迁移),pin/unpin 是临时的例外(短暂钉住做 scanout),静态导入方的 map 时 pin 才是永久钉死。
4. amdgpu 实战:move_notify 里到底做了什么
抽象讲完,落到 amdgpu 看 GPU 驱动如何实现这套契约。amdgpu 作为动态导入方时注册的回调表是:
static const struct dma_buf_attach_ops amdgpu_dma_buf_attach_ops = {
.allow_peer2peer = true,
.move_notify = amdgpu_dma_buf_move_notify
};
allow_peer2peer = true 表示 amdgpu 能处理「对端 VRAM 直连(P2P)、没有 struct page」的资源——这也是它在 attach 时协商 P2P 能力的前提。核心是 move_notify 的实现:
static void
amdgpu_dma_buf_move_notify(struct dma_buf_attachment *attach)
{
struct drm_gem_object *obj = attach->importer_priv;
struct amdgpu_bo *bo = gem_to_amdgpu_bo(obj);
struct ttm_operation_ctx ctx = { false, false };
struct ttm_placement placement = {};
struct amdgpu_vm_bo_base *bo_base;
int r;
/* 1. 让本地这块「导入 BO」失效 */
amdgpu_vm_bo_invalidate(bo, false);
if (!bo->tbo.resource || bo->tbo.resource->mem_type == TTM_PL_SYSTEM)
return;
/* 2. 用空 placement 校验:把它踢回 SYSTEM,即作废旧的物理映射 */
r = ttm_bo_validate(&bo->tbo, &placement, &ctx);
...
/* 3. 遍历所有把该 BO 映射进 GPU 虚拟地址空间的 VM,更新页表 */
for (bo_base = bo->vm_bo; bo_base; bo_base = bo_base->next) {
struct amdgpu_vm *vm = bo_base->vm;
struct dma_resv *resv = vm->root.bo->tbo.base.resv;
...
r = amdgpu_vm_handle_moved(adev, vm, NULL); /* 重建 GPU 页表映射 */
...
}
}
对照第 2 节的头文件语义,这段代码把「作废 + 重建」落到了实处:
- amdgpu_vm_bo_invalidate + 空 placement 的 ttm_bo_validate:把这块导入 BO 的旧映射作废(推回 SYSTEM 域)。这正是响应「buffer 换了物理位置,旧映射要销毁」。
- 遍历 bo->vm_bo 更新 GPU 页表:amdgpu 不只是作废 CPU/DMA 侧的 sg_table,它把「BO 已移动」的事实一路传导到自己的 GPUVM 页表——所有引用这块 BO 的 GPU 虚拟地址映射都要跟着更新。这一步呼应了第九章 GPUVM 的 amdgpu_vm_handle_moved。
- 全程在持锁下操作 VM 的 resv:与 dma_buf_move_notify 持 dmabuf->resv 锁的约定一脉相承,避免与命令提交并发。
而 amdgpu 作为导出方时的 pin/unpin(amdgpu_dma_buf_pin / amdgpu_dma_buf_unpin)则展示了 move_notify 能力如何反向影响 pin 策略:
static int amdgpu_dma_buf_pin(struct dma_buf_attachment *attach)
{
struct amdgpu_bo *bo = gem_to_amdgpu_bo(attach->dmabuf->priv);
u32 domains = bo->allowed_domains;
/* 只有开启 move_notify 时,才允许 pin 进 VRAM 做 P2P;
* 否则退回 GTT,避免 GPU 与 RDMA 在无 move 通知时的钉死冲突 */
if (!IS_ENABLED(CONFIG_DMABUF_MOVE_NOTIFY)) {
domains &= ~AMDGPU_GEM_DOMAIN_VRAM;
} else {
list_for_each_entry(attach, &dmabuf->attachments, node)
if (!attach->peer2peer)
domains &= ~AMDGPU_GEM_DOMAIN_VRAM;
}
...
return amdgpu_bo_pin(bo, domains);
}
这段逻辑的判断极具代表性:只有在编译开启 CONFIG_DMABUF_MOVE_NOTIFY(即动态机制可用)时,amdgpu 才敢把 buffer pin 进 VRAM 供对端 P2P 直连;否则一律退回 GTT(系统内存)中转。原因正是注释所说——没有 move 通知时,一旦 pin 进 VRAM 就可能与 RDMA 产生「谁也不能动」的钉死冲突。
5. 一次完整的动态搬移时序
把上述环节串成一条时间线,动态 dma-buf 的价值就一目了然:
动态导入方(另一 GPU / 网卡)
dma-buf core
导出方 GPU(TTM)
动态导入方(另一 GPU / 网卡)
dma-buf core
导出方 GPU(TTM)
#mermaid-svg-lyRWMiM57OJZS8cn{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lyRWMiM57OJZS8cn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lyRWMiM57OJZS8cn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lyRWMiM57OJZS8cn .error-icon{fill:#552222;}#mermaid-svg-lyRWMiM57OJZS8cn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lyRWMiM57OJZS8cn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lyRWMiM57OJZS8cn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lyRWMiM57OJZS8cn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lyRWMiM57OJZS8cn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lyRWMiM57OJZS8cn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lyRWMiM57OJZS8cn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lyRWMiM57OJZS8cn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lyRWMiM57OJZS8cn .marker.cross{stroke:#333333;}#mermaid-svg-lyRWMiM57OJZS8cn svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lyRWMiM57OJZS8cn p{margin:0;}#mermaid-svg-lyRWMiM57OJZS8cn .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lyRWMiM57OJZS8cn text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-lyRWMiM57OJZS8cn .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lyRWMiM57OJZS8cn .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-lyRWMiM57OJZS8cn .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-lyRWMiM57OJZS8cn .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-lyRWMiM57OJZS8cn #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-lyRWMiM57OJZS8cn .sequenceNumber{fill:white;}#mermaid-svg-lyRWMiM57OJZS8cn #sequencenumber{fill:#333;}#mermaid-svg-lyRWMiM57OJZS8cn #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-lyRWMiM57OJZS8cn .messageText{fill:#333;stroke:none;}#mermaid-svg-lyRWMiM57OJZS8cn .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lyRWMiM57OJZS8cn .labelText,#mermaid-svg-lyRWMiM57OJZS8cn .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-lyRWMiM57OJZS8cn .loopText,#mermaid-svg-lyRWMiM57OJZS8cn .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-lyRWMiM57OJZS8cn .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lyRWMiM57OJZS8cn .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-lyRWMiM57OJZS8cn .noteText,#mermaid-svg-lyRWMiM57OJZS8cn .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-lyRWMiM57OJZS8cn .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lyRWMiM57OJZS8cn .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lyRWMiM57OJZS8cn .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lyRWMiM57OJZS8cn .actorPopupMenu{position:absolute;}#mermaid-svg-lyRWMiM57OJZS8cn .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-lyRWMiM57OJZS8cn .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lyRWMiM57OJZS8cn .actor-man circle,#mermaid-svg-lyRWMiM57OJZS8cn line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-lyRWMiM57OJZS8cn :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
导入阶段(不 pin)
用 sg_table 驱动 DMA,buffer 仍可迁移
显存告急,TTM 决定把这块 BO 从 VRAM 搬到 GTT
下次要用时重新映射
dma_buf_dynamic_attach(dmabuf, dev, importer_ops)
1
dma_buf_map_attachment() → sg_table(指向 VRAM)
2
dma_resv_lock(dmabuf->>resv)
3
dma_buf_move_notify(dmabuf)
4
importer_ops->>move_notify(attach)
5
作废旧 sg_table / 更新自身页表
6
真正搬移 buffer(VRAM → GTT)
7
dma_resv_unlock()
8
dma_buf_map_attachment() → 新 sg_table(指向 GTT)
9
对比静态路径——静态导入方在第一次 map 时就把 buffer pin 死在 VRAM,导出方的 TTM 根本无法执行上图中间那次搬移。动态机制通过一次 move_notify 回调,把「先通知、后搬移、再重映射」三拍拆开,既让共享得以继续,又把迁移的自由还给了导出方。
6. 小结与延伸
动态 dma-buf 的全部要义可以收束为三句话:
至此,5.2 节从 anon_inode、dma-buf 机制、dma-buf exporter 实现、attach/map/sg_table的importer 实现 到本节的动态机制,已经把「内核内部两个驱动如何共享一块 buffer」讲清楚了。但又自然而然引出另一个方向:用户态能不能直接分配一块可共享、零拷贝的 dma-buf,而不必经过某个具体 GPU 驱动? 这个问题可以上升到一个更大的主题:如何把内核态一些机制让用户态也可用,这个主题同样令人兴奋。那就让我们继续兴奋地前行吧,这正是 5.2.6 dma-buf heaps 与 udmabuf 要回答的——它把 dma-buf 的分配权从内核驱动下放到用户态。
我们这里的分析只是共享下的迁移涉及的冰山一角,背后涉及的逻辑很多,下面是相关的主题:
相关阅读:
- move_notify 里对 GPU 页表的更新(amdgpu_vm_handle_moved)属于第九章 GPUVM 的范畴;
- dma_resv 锁与 fence 的配合详见 6.3 隐式同步 dma_resv。


