第六章:异步访问的同步:6.1 dma-fence机制

1. 背景与问题域

前面讲了BO共享的需求,共享必然带来同步的问题,在计算机这个既复杂又高速的的系统里共享,更是一个挑战。

1.1 异构计算时代的同步挑战

现代计算系统早已超越了单一CPU处理的模式,进入了CPU、GPU、视频编解码器、显示控制器、DSP等多种专用硬件协同工作的异构计算时代。这些硬件设备往往:

  • 异步执行:提交命令后立即返回,实际执行在硬件端异步完成
  • 跨设备协作:一块内存可能被多个设备依次或并发访问
  • 流水线处理:前一操作未完成时,后续操作就已提交

这带来了核心问题:如何安全、高效地协调多设备对共享内存的访问?

1.2 传统同步机制的局限

传统的内核同步原语(如mutex、semaphore、spinlock)主要为CPU间同步设计,面对硬件异步操作存在明显短板:

  • 轮询低效:CPU不断查询硬件状态浪费计算资源
  • 阻塞粗糙:简单的sleep无法精确表达设备间的依赖关系
  • 缺乏跨设备语义:无法在GPU、显示器、视频编解码器等不同驱动间传递同步信息

1.3 dma-fence的诞生

dma-fence机制诞生于2012年,由Canonical和德州仪器联合开发,最初作为dma-buf框架的配套同步原语。它的核心理念是:

将"异步操作的未来完成点"对象化,提供统一的跨驱动同步抽象。


2. dma-fence的核心作用

2.1 异步操作的抽象与标准化

dma-fence将"一个尚未完成的异步操作"抽象为内核对象(struct dma_fence),提供标准接口:

  • 创建:驱动提交异步任务时创建fence
  • 信号:硬件完成操作后,驱动调用dma_fence_signal()标记完成
  • 等待:消费者通过dma_fence_wait()阻塞等待完成
  • 回调:注册dma_fence_add_callback()实现异步通知

这种标准化使得不同厂商的GPU驱动、显示驱动、视频驱动能用同一套语义交换同步信息。

2.2 解耦生产者与消费者

传统同步往往要求生产者和消费者紧密配合,dma-fence实现了彻底解耦:

  • 生产者:只管创建fence,完成时signal,无需知道谁在等待
  • 消费者:只管获取fence并等待/注册回调,无需知道谁创建的

中间通过dma-buf的dma_resv(reservation object)管理fence集合,实现完全的松耦合架构。

2.3 显式与隐式同步的统一基础

Linux内核支持两种fence使用模式:

显式同步(Explicit Fencing)
用户态通过文件描述符传递fence(如sync_file),应用明确控制同步点,适合Vulkan等现代API。例如:Vulkan的VkSemaphore、Android的SyncFence

隐式同步(Implicit Fencing)
fence隐式附加在dma-buf上(通过dma_resv),驱动自动管理同步,对应用透。传统OpenGL、X11的缓冲区管理

两种模式的底层都依赖dma-fence机制,只是使用方式不同。我们后续的内容的组织,就按这个分类展开细化。


3. 核心机制解析

struct dma_fence是整个机制的核心数据结构,其完整定义如下:

struct dma_fence {
	 // 保护fence状态的自旋锁指针
    spinlock_t *lock;
    // 驱动特定操作的回调函数集
    const struct dma_fence_ops *ops;
    
    // 三合一union:根据生命周期阶段使用不同字段
    union {
        struct list_head cb_list;// 未完成时:回调函数链表
        ktime_t timestamp;       // 已完成时:信号完成的时间戳
        struct rcu_head rcu;     // 释放时:RCU延迟释放
    };
     // 执行上下文ID(由dma_fence_context_alloc分配)
    u64 context;                         
    u64 seqno;             // 上下文内的序列号(用于排序)
    unsigned long flags;   // 原子标志位(见下方enum)
    struct kref refcount;  // 引用计数
    int error;             // 错误码(<0表示异常完成)
};

关键设计解析

1. 自旋锁指针 (lock)

  • 指针而非嵌入锁:允许多个fence共享同一把锁(如同一驱动的多个fence)
  • 使用spinlock_t *而非mutex:支持在中断上下文操作fence
  • 保护对cb_listflags等的并发访问

2. 操作接口 (ops)
指向struct dma_fence_ops,定义驱动特定的行为:

struct dma_fence_ops {
    const char *(*get_driver_name)(struct dma_fence *fence);
    const char *(*get_timeline_name)(struct dma_fence *fence);
    bool (*enable_signaling)(struct dma_fence *fence);  // 启用硬件中断
    bool (*signaled)(struct dma_fence *fence);           // 快速查询状态
    signed long (*wait)(struct dma_fence *fence, bool intr, signed long timeout);
    void (*release)(struct dma_fence *fence);            // 释放时的清理
    void (*set_deadline)(struct dma_fence *fence, ktime_t deadline);
};

3. 三合一union的设计

这是fence结构中最巧妙的内存优化:

[未完成阶段]  使用 cb_list
    ↓
    struct list_head cb_list  // 回调链表(16字节)
    
[signal时]  list_replace保存cb_list → 替换为timestamp
    ↓
    ktime_t timestamp  // 完成时间(8字节)
    
[释放时]  替换为rcu
    ↓
    struct rcu_head rcu  // RCU回收(16字节)

生命周期保证

  • cb_list仅在未signal时有效
  • timestamp仅在signal后、释放前有效
  • rcu仅在引用计数归零后使用
  • 通过flags中的位标志判断当前阶段

4. 上下文与序列号 (context & seqno)

// 驱动初始化时分配context(全局唯一)
u64 ctx = dma_fence_context_alloc(1);

// 每次创建fence递增seqno
dma_fence_init(fence, &my_ops, &my_lock, ctx, seqno++);

排序语义

  • 同一context内的fence通过seqno严格排序
  • seqno较大的fence必然晚于seqno较小的fence完成
  • 不同context的fence之间无序

5. 原子标志位 (flags)

enum dma_fence_flag_bits {
    DMA_FENCE_FLAG_SEQNO64_BIT,        // 使用完整64位seqno比较(否则仅用低32位)
    DMA_FENCE_FLAG_SIGNALED_BIT,       // 已完成标志(原子设置)
    DMA_FENCE_FLAG_TIMESTAMP_BIT,      // 时间戳已记录
    DMA_FENCE_FLAG_ENABLE_SIGNAL_BIT,  // 已调用enable_signaling
    DMA_FENCE_FLAG_USER_BITS,          // 用户可扩展位起始
};

原子操作

// 设置完成标志(防止重复signal)
if (test_and_set_bit(DMA_FENCE_FLAG_SIGNALED_BIT, &fence->flags))
    return -EINVAL;  // 已经signal过了

// 检查是否完成(无需加锁)
if (test_bit(DMA_FENCE_FLAG_SIGNALED_BIT, &fence->flags))
    return 0;  // 快速路径

总结:设计哲学

dma_fence结构体现了Linux内核的三大设计原则:

  1. 内存高效:union设计将不同阶段的数据复用同一内存空间
  2. 并发安全:原子位操作 + 自旋锁 + RCU机制保证多核安全
  3. 灵活可扩展ops回调允许驱动定制行为,USER_BITS支持私有扩展

整个结构仅占72字节(64位系统),却承载了复杂的同步语义,是内核"零成本抽象"理念的典范。

3.2 fence的生命周期

阶段一:初始化

驱动调用 dma_fence_init(fence, ops, lock, context, seqno)
   ↓
设置基本信息,引用计数为1
   ↓
返回给调用者,fence处于未完成状态

阶段二:传递与等待

fence被放入dma_resv或作为sync_file导出
   ↓
消费者获取fence并增加引用计数
   ↓
消费者选择同步方式:
  - dma_fence_wait() → 阻塞等待
  - dma_fence_add_callback() → 注册回调

阶段三:完成信号

硬件完成操作,触发中断
   ↓
驱动在中断处理或工作队列中调用 dma_fence_signal()
   ↓
设置 SIGNALED_BIT,记录时间戳
   ↓
遍历 cb_list,依次调用所有注册的回调
   ↓
唤醒所有阻塞等待的进程

阶段四:释放

所有持有者调用 dma_fence_put() 减少引用计数
   ↓
引用计数归零
   ↓
通过RCU机制延迟释放内存(保证并发读安全)

3.3 dma-fence的同步方式

dma-fence提供了两种异步操作的同步方式:

  • 阻塞等待 (dma_fence_wait):进程主动休眠,等待fence信号
  • 注册回调 (dma_fence_add_callback):注册函数,fence完成时异步调用
3.3.1 阻塞等待机制

dma_fence_wait()提供同步等待语义,核心流程:

1. 注册临时回调 dma_fence_default_wait_cb
   - 回调函数保存当前进程的task_struct指针
   
2. 设置进程状态
   - TASK_INTERRUPTIBLE(可被信号中断)
   - TASK_UNINTERRUPTIBLE(不可中断)
   
3. 调用 schedule_timeout(timeout)
   - 进程让出CPU,进入休眠
   - 被移出运行队列
   
4. fence完成时回调被触发
   - wake_up_state(task, TASK_NORMAL)
   - 进程恢复TASK_RUNNING,重新进入运行队列
   
5. 清理回调节点,返回剩余超时时间

这种实现避免了忙等待,进程休眠期间不占用CPU资源。

3.3.2 异步回调机制

dma_fence_add_callback()提供非阻塞的异步通知:

注册阶段

int dma_fence_add_callback(fence, cb, func) {
    // 快速检查:已完成则返回错误,不调用回调
    if (test_bit(SIGNALED_BIT, &fence->flags))
        return -ENOENT;
    
    // 启用硬件中断(如果需要)
    __dma_fence_enable_signaling(fence);
    
    // 将回调加入链表
    cb->func = func;
    list_add_tail(&cb->node, &fence->cb_list);
    
    return 0;  // 立即返回,不阻塞
}

触发阶段

int dma_fence_signal_timestamp_locked(fence, timestamp) {
    // 设置完成标志(原子操作,避免重复signal)
    test_and_set_bit(SIGNALED_BIT, &fence->flags);
    
    // 保存回调链表(即将被timestamp替换)
    list_replace(&fence->cb_list, &cb_list);
    
    // 记录时间戳
    fence->timestamp = timestamp;
    
    // 依次调用所有回调(可能在中断上下文)
    list_for_each_entry_safe(cur, tmp, &cb_list, node) {
        cur->func(fence, cur);
    }
}

3.5 使能信号机制

许多硬件支持"硬件到硬件"同步(hw→hw),无需CPU介入。但当需要"硬件到软件"通知(hw→sw)时,必须显式启用中断:

bool (*enable_signaling)(struct dma_fence *fence) {
    // 驱动特定实现
    // 1. 启用硬件中断
    // 2. 或插入命令到GPU队列触发回调
    // 3. 返回true表示成功,false表示fence已完成
}

这种设计的优势:

  • 性能优化:纯硬件同步无需CPU开销
  • 按需启用:只有调用wait或add_callback时才启用中断
  • 避免风暴:减少不必要的中断

首次调用dma_fence_wait()dma_fence_add_callback()时,框架会自动调用enable_signaling()


4. 与dma-buf的集成

dma_fence和dma-buf是同时提出的,两者协作完成了共享buffer的同步访问。

dma-buf通过struct dma_resv 管理关联的fence集合:

struct dma_buf {
    struct dma_resv *resv;  // 指向reservation object
    // ...其他字段
};

struct dma_resv {
    struct ww_mutex lock;          // 等待-受伤互斥锁
    struct dma_resv_list *fences;  // fence数组(RCU保护)
};

5. 总结

dma-fence作为Linux内核异步操作同步的基础设施,其价值在于:

  1. 抽象统一:为GPU、显示、视频、相机等不同硬件提供统一的同步语义
  2. 性能优化:支持硬件直接同步,最大化并行度,减少CPU开销
  3. 灵活强大:支持阻塞/非阻塞、显式/隐式等多种模式

本文从异步访问的需求引出dma_fence的设计,需要重点理解的几个概念有:

  1. 异构系统的执行方式;
  2. 显式同步与隐式同步的概念;
  3. 阻塞与非阻塞式的同步方式;

下面的章节我们先来理解阻塞与非阻塞同步方式的具体实现和差异;然后在来看显式与隐式的使用方法。

<think>我们正在讨论Linux内核中的dma-fence.c文件。根据用户的问题,需要解释该文件的作用及工作原理。 根据引用[1]的内容,dma-fence在内核中被创建,可以通过文件描述符在用户层进行操作,并且可以在内核和用户层之间传递。这为我们提供了理解dma-fence的基础。 首先,dma-fenceLinux内核中用于同步DMA(直接内存访问)操作的一种机制。它主要用于图形和多媒体领域,例如在GPU操作、显示处理、视频编解码等场景中,用于同步不同设备或不同进程之间的内存访问。 作用: 1. 同步机制dma-fence提供了一种机制,使得一个或多个执行单元(如CPU、GPU)可以等待某个异步操作(如DMA传输、GPU渲染)完成。 2. 跨进程同步:通过将dma-fence导出为文件描述符,可以在不同进程间传递,实现跨进程同步。 3. 信号机制:当异步操作完成时,dma-fence会发出信号,唤醒等待该fence的实体。 工作原理: 1. 创建fence:在内核驱动中,当启动一个异步操作(如GPU命令提交)时,会创建一个dma_fence对象,该对象与这个异步操作关联。 2. 等待fence:其他操作如果需要等待该异步操作完成,可以在这个fence上等待(通过dma_fence_wait()或类似的函数)。等待可以是阻塞的,也可以是非阻塞的(轮询)。 3. 信号通知:当异步操作完成时,驱动程序会调用dma_fence_signal()函数来标记fence为已完成状态。这会唤醒所有等待该fence的等待者,并执行任何注册的回调函数。 4. 文件描述符导出:通过dma_fence_get_fd()函数,可以将dma_fence对象导出为一个文件描述符。这个文件描述符可以被传递到用户空间,也可以通过UNIX域套接字传递给其他进程。 5. 用户空间操作:在用户空间,这个文件描述符可以像普通文件描述符一样使用。例如,可以使用poll()来等待fence完成,或者通过read()来获取fence的状态(但通常使用poll或epoll来等待)。 6. 内核空间还原:当用户空间将文件描述符传递回内核(例如通过ioctl)时,内核可以通过dma_fence_get_from_fd()函数将文件描述符还原为dma_fence对象,从而在内核中继续使用。 需要注意的是,dma-fence机制Linux内核DRM(Direct Rendering Manager)子系统的核心组件之一,尤其在实现显式同步(explicit synchronization)中扮演重要角色。 引用[1]中提到的内容正是这一机制的关键:它允许dma-fence在内核和用户空间之间以文件描述符的形式传递,从而实现了跨进程的同步。 现在,根据要求生成相关问题: 1. dma-fence在内核中是如何被创建和销毁的? 2. 在用户空间如何使用dma-fence文件描述符进行同步? 3. dma-fence在图形处理中的具体应用案例有哪些? 注意:由于引用[1]是站内引用,我们在回答中引用了它,所以末尾需要添加引用标识[^1]。</think>### DMA-FenceLinux 内核中的作用及工作原理 #### 作用 `dma-fence.c` 是 Linux 内核中实现 **异步操作同步** 的核心组件,主要作用包括: 1. **跨硬件同步** 协调 CPU、GPU、DMA 控制器等异构硬件的操作顺序,确保内存访问的正确性(如 GPU 渲染完成后才启动显示)[^1]。 2. **进程间同步** 通过文件描述符(fd)在进程间传递同步状态,实现跨进程资源共享(如 Vulkan/OpenGL 显式同步)。 3. **依赖管理** 构建操作依赖链(如 `fenceA` 完成后触发 `fenceB`),避免资源竞争。 #### 工作原理 1. **内核层创建** 驱动(如 DRM 显卡驱动)创建 `dma_fence` 对象,包含: ```c struct dma_fence { spinlock_t lock; const struct dma_fence_ops *ops; unsigned long flags; // 状态标志(如 SIGNALED) struct list_head cb_list; // 回调函数链表 }; ``` 2. **用户层暴露** 通过 `dma_fence_get_fd()` 将 fence 导出为文件描述符(fd),用户层可对其操作: ```c int fd = dma_fence_get_fd(fence); // 内核导出 fd ``` - 用户层通过 `poll()` 监听 fence 状态 - 通过 `read()` 获取错误码(如超时) 3. **跨进程传递** 通过 Unix socket 传递 fd,接收方用 `dma_fence_get_from_fd()` 还原内核对象: ```c struct dma_fence *fence = dma_fence_get_from_fd(fd); // fd 转回 fence ``` 4. **同步机制** - **等待完成**:内核中调用 `dma_fence_wait()` 阻塞直到 fence 完成 - **信号通知**:硬件操作完成后,驱动调用 `dma_fence_signal()` 触发回调 - **链式依赖**:通过 `dma_fence_add_callback()` 注册回调,实现依赖触发 #### 典型应用场景 1. **GPU 渲染流水线** - 顶点着色 → 栅格化 → 像素着色,每个阶段用 fence 同步 2. **显示输出** 确保帧渲染完成后再提交给显示控制器(避免撕裂)。 3. **视频编解码** 同步解码器输出与渲染器输入。 --- ### 相关问题 1. DMA-Fence 如何避免用户层直接访问内核数据结构? 2. 在多 GPU 场景下,DMA-Fence 如何协调不同设备间的同步? 3. DMA-FenceLinux Futex 在同步机制上有何本质区别? 4.DMA-Fence 出现信号丢失时,内核有哪些调试手段? [^1]: DMA-Fence 通过文件描述符实现用户层与内核的交互,确保安全隔离。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DeeplyMind

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值