欢迎光临
我们一直在努力

第五章:BO的共享:5.2.5 dynamic dma-buf:共享下的迁移

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),就成了动态导入方。二者的行为差异可以浓缩成一张表:

静态导入方(importer_ops == NULL)动态导入方(importer_ops != NULL)
判定函数 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);
}

  • 遍历 dmabuf->attachments 链表:导出方无需知道对端是谁,照着链表逐个通知即可。
  • 只通知动态导入方(if (attach->importer_ops)):静态导入方没有 move_notify,也不需要——它们的 buffer 本就被 pin 住不会移动。
  • 必须在持 resv 锁时调用(dma_resv_assert_held):这保证了「通知导入方」与「实际搬移」之间不会有并发的新映射插进来。回调本身也在持锁状态下执行。
  • 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);
    }

    三个约束值得留意:

  • WARN_ON(!attach->importer_ops):这两个 API 只允许动态导入方调用。静态导入方的 pin 是在 map 内部自动完成的(5.2.4 的 dma_buf_pin_on_map),不走这条路。
  • 内核注释明确限制用途:dma_buf_pin 的注释写道「only for limited use cases like scanout and not for temporary pin operations」「不得让用户态通过此接口 pin 住任意数量的 buffer」——防止动态机制被滥用成变相的「无限钉死」。
  • unpin 之后 buffer 重新可迁移:注释点明 unpin「allows the exporter to move any mapping of @attach again and inform the importer through move_notify」——即 unpin 让 buffer 回到「可搬移、搬移时发 move_notify」的常态。
  • 于是三者构成一个清晰的层次: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 的全部要义可以收束为三句话:

  • move_notify 是核心:导出方搬移前遍历 attachments 逐个通知动态导入方作废旧映射;它把「共享」与「可迁移」这对矛盾拆解为「先预警、再搬移、后重建」的时序协作。
  • pin/unpin 是临时例外:仅动态导入方可调用,用于 scanout 等确需稳定地址的短暂场景,用完即 unpin 让 buffer 回归可迁移常态——绝不可退化为变相的永久钉死。
  • 静态与动态的分水岭在 attach:是否传 importer_ops(含 move_notify)一次决定人格;静态以 pin 换简单,动态以重映射换灵活。
  • 至此,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。
    赞(0)
    未经允许不得转载:171主机测评 » 第五章:BO的共享:5.2.5 dynamic dma-buf:共享下的迁移
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址