0. 引言

本节我们重点关注dma_fence.ops.enable_signaling的设计哲学和用法。我们先来看下内核中对该回调的注释:

@enable_signaling:启用 fence 的软件信号通知。

对于具备硬件到硬件(hw→hw)信号能力的 fence 实现,可以通过实现此操作来启用必要的中断、
或向命令流中插入命令等,从而在仅需要 hw→hw 同步的常见情况下避免这些高开销的操作。
此回调在首次调用 dma_fence_wait() 或 dma_fence_add_callback() 的路径中被调用,
用于通知 fence 实现:有另一个驱动正在等待该信号(即 hw→sw 的情况)。

此回调在 IRQ 禁用的状态下被调用,因此在此回调之外的代码中,只能使用会禁用 IRQ 的自旋锁。

返回 false 表示 fence 已经完成(passed),或者发生了某些错误导致无法启用信号通知。返回 true 表示成功启用。

可以在 enable_signaling 中设置 dma_fence.error,但仅限于返回 false 时。

由于许多实现可以在 enable_signaling 被调用之前就调用 dma_fence_signal(),
因此存在一个竞争窗口:dma_fence_signal() 可能导致 fence 的最后一个引用被释放,
从而其内存被释放。为避免这种情况,此回调的实现应使用 dma_fence_get() 获取自己的引用,
并在 fence 被 signal 时释放该引用(例如通过中断处理程序)。

此回调是可选的。如果未提供此回调,则驱动必须始终保持信号通知处于启用状态。

单看这个详尽的注释,真得有可能越看越糊涂,本节试图讲清楚这个设计和用法。

1. 设计思想:Lazy Enablement(延迟启用)

Linux 内核的 dma-fence 框架采用了一个精妙的优化哲学:不用就不开,用时才开

在 GPU 驱动中,大量的 fence 仅用于 硬件到硬件(hw→hw)同步——GPU A 的输出作为 GPU B 的输入,整个过程不需要 CPU 参与。如果每个 fence 都默认开启中断通知 CPU,会带来不必要的性能开销:

  • 中断上下文切换成本
  • ISR 执行开销
  • 缓存污染
  • 功耗增加

因此,fence 框架将"启用软件信号通知"这一行为延迟到真正有软件消费者时才执行。这就是 enable_signaling 回调的设计初衷。

核心原则:

只有当有人真正需要软件通知时(dma_fence_wait()dma_fence_add_callback()),才启用信号机制。


2. 回调的作用

bool (*enable_signaling)(struct dma_fence *fence);

enable_signalingstruct dma_fence_ops 中的一个可选回调,它的职责是:

告知 fence 的实现者:“有软件在等待这个 fence,请确保 fence 完成时能调用 dma_fence_signal()。”

具体含义取决于实现者:

  • 对于硬件 fence:开启中断,使 GPU 完成时能触发 ISR → dma_fence_signal()
  • 对于软件 fence:可以作为事件触发点,启动某些异步工作流
  • 如果不实现(设为 NULL):框架认为信号机制始终启用

返回值语义

返回值 含义
true 成功启用信号机制,fence 尚未完成
false fence 已经 signaled,或发生错误无法启用

3. 工作流程

3.1 触发路径

enable_signaling 不会被直接调用,而是由以下 fence 框架 API 间接触发:

dma_fence_add_callback()  ───────┐
dma_fence_wait()           ──────┼──→ enable_signaling()
dma_fence_enable_sw_signaling() ─┘

3.2 调用时序(以 dma_fence_add_callback 为例)

int dma_fence_add_callback(struct dma_fence *fence, ...)
{
    spin_lock_irqsave(fence->lock, flags);

    // Step 1: 检查是否已经启用过
    if (!test_bit(DMA_FENCE_FLAG_ENABLE_SIGNAL_BIT, &fence->flags)) {
        // Step 2: 设置标志位
        set_bit(DMA_FENCE_FLAG_ENABLE_SIGNAL_BIT, &fence->flags);

        // Step 3: 调用驱动实现
        if (!fence->ops->enable_signaling(fence)) {
            // 返回 false: fence 已完成或错误
            return -ENOENT;
        }
    }

    // Step 4: 再次检查 signaled(防止竞争)
    if (test_bit(DMA_FENCE_FLAG_SIGNALED_BIT, &fence->flags))
        return -ENOENT;

    // Step 5: 挂载 callback
    list_add_tail(&cb->node, &fence->cb_list);

    spin_unlock_irqrestore(fence->lock, flags);
}

3.3 竞争条件与防护

存在一个关键竞争窗口:

时间线:
  T1: set_bit(ENABLE_SIGNAL_BIT)
  T2: [此时另一个 CPU 上 dma_fence_signal() 被调用]
  T3: enable_signaling() 执行

如果 dma_fence_signal() 在 T2 发生,它会检查 ENABLE_SIGNAL_BIT

  • 已设置 → 遍历 cb_list 通知所有 callback
  • 未设置 → 无需通知(没有软件消费者)

框架通过 先设置 ENABLE_SIGNAL_BIT,再检查 SIGNALED_BIT 来关闭这个竞争窗口,确保不会漏掉信号。

3.4 调用环境约束

┌─────────────────────────────────────────────────┐
│ enable_signaling() 的调用环境                    │
├─────────────────────────────────────────────────┤
│ • IRQ 已禁用(spin_lock_irqsave)                │
│ • 持有 fence->lock                              │
│ • 只能使用 spin_lock 类型的锁                    │
│ • 不能睡眠                                       │
│ • 不能调用可能睡眠的内存分配                      │
└─────────────────────────────────────────────────┘

4. 常规使用场景:硬件 Fence

这是 enable_signaling 最典型的用途——按需开启 GPU 完成中断

4.1 场景描述

GPU Ring Buffer:
  [cmd1] [cmd2] [cmd3] ... [cmdN]
                              ^
                              fence seqno = N

正常情况(hw→hw):GPU B 通过 semaphore 等待 GPU A,不需要中断
特殊情况(hw→sw):CPU 要等 fence N 完成,需要中断通知

4.2 典型实现模式

static bool gpu_fence_enable_signaling(struct dma_fence *f)
{
    struct gpu_fence *fence = to_gpu_fence(f);
    struct gpu_ring *ring = fence->ring;

    // 1. 快速检查:fence 是否已完成
    if (gpu_fence_completed(ring, fence->seqno))
        return false;

    // 2. 获取 fence 引用(防止中断到来前 fence 被释放)
    dma_fence_get(f);

    // 3. 启用该 ring 的完成中断
    gpu_irq_enable(ring->irq_src);

    // 4. 再次检查(防止在开中断过程中 fence 已完成)
    if (gpu_fence_completed(ring, fence->seqno)) {
        gpu_irq_disable(ring->irq_src);
        dma_fence_put(f);
        return false;
    }

    return true;
}

// 对应的中断处理函数
static irqreturn_t gpu_fence_isr(int irq, void *data)
{
    struct gpu_ring *ring = data;

    // 遍历该 ring 上所有 pending fence,signal 已完成的
    while (fence = next_pending_fence(ring)) {
        if (gpu_fence_completed(ring, fence->seqno)) {
            dma_fence_signal(&fence->base);
            dma_fence_put(&fence->base);  // 释放 enable_signaling 中获取的引用
        }
    }

    return IRQ_HANDLED;
}

4.3 为什么要 dma_fence_get()

enable_signaling 的文档明确要求:

实现应 grab 自己的引用 (dma_fence_get()),在 fence 被 signal 时释放。

原因:dma_fence_signal() 可能在 enable_signaling 返回之前被调用,导致最后一个引用被释放、fence 被释放。如果 ISR 此时访问 fence → use-after-free

竞争场景(不加引用):
  CPU0                          CPU1
  enable_signaling(fence)
    gpu_irq_enable()
                                gpu_fence_isr()
                                  dma_fence_signal(fence)
                                  // 最后一个引用释放
                                  // fence 被 kfree
    fence->seqno  ← UAF!

5. 特殊使用场景:事件触发器

这里给出一个特殊的使用场景,AMDGPU KFD中的eviction fence用法。

5.1 KFD Eviction Fence

AMD KFD 驱动对 enable_signaling 有一个创造性的用法——把它当作"有人想驱逐我的 BO"的事件触发器

背景

KFD(Kernel Fusion Driver)为每个进程的所有 BO 共享一个 eviction fence或者每个SVM BO设置一个eviction fence。这个 fence 的含义是:

  • 进程共享的fence表示这个进程的用户队列还在运行,BO 不能被搬走。
  • SVM BO的fence在非signal时,表示不能被搬走,为signal时表示可以搬走。

当 TTM 需要腾出 VRAM 空间时,它会尝试等待 eviction fence → 触发 enable_signaling

实现
static bool amdkfd_fence_enable_signaling(struct dma_fence *f)
{
    struct amdgpu_amdkfd_fence *fence = to_amdgpu_amdkfd_fence(f);

    if (dma_fence_is_signaled(f))
        return true;

    if (!fence->svm_bo) {
        // 调度 evict & restore 工作项
        kgd2kfd_schedule_evict_and_restore_process(fence->mm, ...);
    } else {
        // SVM BO eviction
        svm_range_schedule_evict_svm_bo(fence);
    }
    return false;  // 关键:返回 false
}
流程图
TTM 需要 VRAM 空间
  │
  ▼
ttm_bo_evict()
  │
  ▼
amdgpu_bo_move() → 设置 GPU scheduler job
  │
  ▼
GPU scheduler 发现 eviction fence 未 signaled
  │
  ▼
dma_fence_add_callback(eviction_fence)
  │
  ▼
enable_signaling()
  │
  ├─→ schedule_evict_and_restore_process()
  │     │
  │     ▼ (异步 work item)
  │   暂停用户队列(quiesce)
  │     │
  │     ▼
  │   dma_fence_signal(eviction_fence)  ← 软件主动 signal
  │     │
  │     ▼
  │   TTM 收到通知,搬移 BO
  │     │
  │     ▼
  │   恢复用户队列(restore)
  │
  └─→ return false(告诉框架:fence 还没好,等 signal)
设计巧妙之处
方面 说明
不需要硬件中断 纯软件驱动的 signal
enable_signaling = 事件触发器 "有人想驱逐我"这个事件的通知点
返回 false 不是表示失败,而是"我已经知道了,会异步 signal"
延迟执行 只有真正需要驱逐时才暂停队列,平时不影响性能

5.2 与常规用法的对比

常规(硬件 fence) KFD eviction fence
谁 signal 硬件中断 → ISR 驱动 work item
enable_signaling 做什么 开中断 启动 eviction 流程
返回值 true(已启用) false(会异步处理)
语义 “请通知我 fence 完成” “有人要驱逐我的内存”

6. 不实现 enable_signaling 的情况

如果驱动的 fence ops 中 enable_signaling = NULL

// dma-fence.c 框架代码中的处理
if (!fence->ops->enable_signaling ||
    fence->ops->enable_signaling(fence)) {
    // NULL 被视为"始终启用"
}

适用场景:

  • 中断始终开启的简单驱动
  • 纯软件 fence(如 drm_sched_fence),驱动自己知道何时 signal
  • 不需要优化 hw→hw 路径的情况

7. 常见陷阱与最佳实践

7.1 陷阱

  1. 忘记 dma_fence_get():导致 use-after-free
  2. enable_signaling 中睡眠:它在 spin_lock_irqsave 中被调用
  3. 忽略竞争:enable 期间 fence 可能已被 signal
  4. 返回 true 但不确保能 signal:callback 永远不会触发,造成死锁

7.2 最佳实践

static bool my_fence_enable_signaling(struct dma_fence *f)
{
    // 1. 先检查是否已完成
    if (hw_fence_completed(f))
        return false;

    // 2. 拿引用
    dma_fence_get(f);

    // 3. 启用通知机制(中断/轮询/其他)
    enable_notification(f);

    // 4. 再次检查(关闭竞争窗口)
    if (hw_fence_completed(f)) {
        disable_notification(f);
        dma_fence_put(f);
        return false;
    }

    return true;
}

8. 总结

enable_signaling 体现了内核设计中"按需付费"的哲学:

┌─────────────────────────────────────────────────────────────┐
│                    enable_signaling 全景                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  设计思想: Lazy Enablement - 不用就不开,用时才开              │
│                                                             │
│  触发条件: 有软件消费者需要知道 fence 何时完成                 │
│            (wait / callback / enable_sw_signaling)          │
│                                                             │
│  常规用途: 开启硬件中断,让 ISR 能调用 dma_fence_signal        │
│                                                             │
│  特殊用途: 作为事件触发器,启动异步工作流                      │
│            (如 KFD eviction fence)                          │
│                                                             │
│  不实现:   框架视为信号始终启用                                │
│                                                             │
│  核心约束: IRQ disabled / 不能睡眠 / 必须防竞争               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

理解了 enable_signaling,就理解了 dma-fence 框架中软件通知路径的入口点——它是连接"硬件完成"与"软件感知"的桥梁,也是驱动对"何时付出通知成本"做出决策的控制点。但很多驱动里都不实现这个回调,默认就是可通知的。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐