在这里插入图片描述

[TOC]

Linux 内核 dma-buf 核心实现(drivers/dma-buf/dma-buf.c)全面解析

[drivers/dma-buf/dma-buf.c] [dma-buf 共享缓冲区框架核心] [跨子系统/设备共享 DMA 缓冲区 + 同步与 CPU 访问支持的核心对象与文件接口]

介绍

dma-buf 子系统提供一种跨多个驱动/子系统共享同一块底层缓冲区的内核框架,并配套处理异步硬件访问的同步(基于 dma-resv/dma-fence)以及CPU 访问一致性(begin/end CPU access、mmap、poll/notify 等)。官方文档把其定位为:共享用于硬件(DMA)访问的缓冲区,并同步异步硬件访问。
drivers/dma-buf/dma-buf.c 则是**dma-buf 核心对象(struct dma_buf)与其“文件描述符语义”**的主要实现:全局跟踪、fd 导出、attachment 管理、map/unmap、CPU access、poll、ioctl 等。


历史与背景

这项技术是为了解决什么特定问题而诞生的?

在图形、多媒体、相机、编解码等场景中,常见“一个子系统分配 buffer,另一个子系统/设备使用同一 buffer 做 DMA”。若各驱动各自定义共享方式,会导致:

  • 缓冲区传递机制碎片化(每对驱动一套接口)
  • 同步机制不统一(谁负责等待/信号?)
  • 缓冲区生命周期难以管理(引用计数/关闭 fd/释放顺序)

dma-buf 用统一的文件描述符(fd)载体来传递 buffer,并用 dma-resv/dma-fence 体系做隐式同步,降低跨子系统互操作成本。

发展里程碑或版本迭代(从代码/文档可直接看到的能力点)

不在这里强行给出“某版本引入某特性”的时间线(那需要进一步查 git log 才严谨),但从当前主线实现可确认这些关键能力已经稳定存在:

  • 全局 dma-buf 列表与迭代接口dmabuf_list + dma_buf_iter_begin()/dma_buf_iter_next(),并强调 list mutex 不保护 refcount,需要 file_ref_get() 方式安全拿引用。)
  • 明确的锁定约定:文档化说明“哪些 API 调用方必须/必须不持有 reservation lock”,将 deadlock 风险前置到接口契约层。)
  • map_attachment 路径集成 pin 语义dma_buf_map_attachment()might_sleep()dma_resv_assert_held(),并在需要时走 ops->pin(),对导出者错误行为做 WARN。)
  • CPU 访问路径显式等待隐式 fence:在 dma_buf_begin_cpu_access() 相关逻辑里,会等待隐式渲染 fences(通过 dma_resv_wait_timeout)。)
  • 官方文档持续更新(发布时间为近一周),说明仍是活跃维护的主流框架。

社区活跃度与主流应用情况

dma-buf 是 Linux 图形/多媒体栈里的核心基础设施,DRM、V4L2 等广泛依赖它进行 buffer 交换与同步(官方文档也明确点到这些典型使用)。


核心原理与设计

核心工作原理是什么?

可以按“对象模型 + 文件语义 + 同步模型 + 映射模型”来理解:

  1. 对象模型:struct dma_buf 作为共享对象
  • 导出者(exporter)实现 struct dma_buf_ops,决定底层内存如何分配、如何 map 成 sg_table、如何处理 CPU access 等。
  • 导入者(importer/user)通过 attachment 把该 dma-buf 绑定到某个设备,再把它映射成该设备可 DMA 的形式。
  1. 文件语义:dma-buf 作为 fd 传递
  • dma-buf 通过匿名 inode/伪文件系统的 file 对象呈现为 fd,从而在进程/子系统之间传递。代码中可以看到创建 pseudo file 的路径(alloc_file_pseudo),并设置 inode size 等信息。)
  • 代码维护全局 dmabuf 列表,并提供迭代:迭代时需要安全地“提升引用计数以防销毁”。)
  1. 同步模型:dma-resv / dma-fence 隐式同步
  • dma-buf 的 resv(reservation object)聚合 fences:读/写访问顺序由 fences 表达。
  • CPU 访问路径会等待隐式 fences,确保 CPU 读写看到一致数据(至少在指定范围/方向语义内)。)
  1. 映射模型:attachment + map/unmap
  • dma_buf_map_attachment() 要求调用方持有 reservation lock(代码里 dma_resv_assert_held),并可能 sleep。)
  • 它通过 exporter 的 ops->map_dma_buf() 返回 sg_table,供设备 DMA 使用;并在需要时结合 pin 语义提升“底层存储不可迁移/可访问性”约束。)
  1. 锁定约定(非常关键)
    源码中专门列出导入者约定:例如 dma_buf_map_attachment()/vmap() 这类需要持有 reservation lock,而 dma_buf_attach()/export()/fd/get/put 等则要求不持有。这个约定是排查死锁的第一优先级依据。)

主要优势体现在哪些方面?

  • 跨子系统统一共享模型:fd 传递 + 统一 attachment/map 语义,降低耦合(官方文档描述的核心目标)。
  • 统一的隐式同步基础设施:围绕 dma-resv/dma-fence 组织同步,CPU 访问路径也能通过等待 fences 保证一致性。)
  • 明确的调用方锁约束:把“谁持锁调用什么”写进契约,降低系统集成时的不可控并发问题。)

劣势、局限性或不适用性

  • 复杂度高:必须正确处理 exporter/importer 角色、reservation lock、fence 语义、CPU access begin/end、mmap/ioctl/poll 等一整套机制;错误通常表现为隐式同步失效或死锁。)
  • 不是“通用共享内存”接口:它面向 DMA buffer 共享与同步;若只是 CPU 侧共享数据结构,可能更适合其他 IPC/共享内存机制(取决于需求)。这一点需要你基于目标场景做判断。
  • 关键路径可能 sleep:例如 dma_buf_map_attachment() 明确 might_sleep(),因此调用上下文要满足可睡眠条件。)

使用场景

首选场景(举例)

  • DRM ↔ V4L2 / 编解码器 / ISP / 显示:图形与多媒体流水线中,一个组件产出帧缓冲,另一个组件消费;dma-buf 用 fd 传递 buffer,fence 做隐式同步,是典型路径(官方文档点名 DRM、V4L2 等互通)。
  • 跨设备共享同一底层存储:例如 GPU 渲染结果直接给显示/视频编码设备 DMA 使用,通过 attachment/map 取 sg_table。

不推荐使用的场景(原因)

  • 不涉及 DMA 的纯 CPU 数据共享:dma-buf 引入的同步/映射语义与实现复杂度可能是负担。
  • 无法接受隐式同步模型的系统:如果你需要显式控制每一步同步、或系统中同步策略与 dma-resv 机制冲突,使用 dma-buf 可能会增加调试难度(需要你对 fence/resv 语义非常熟)。

对比分析

这里选 3 类“相似但定位不同”的技术路线做对比(你学习时容易混淆的点):

1) dma-buf vs “普通共享内存/文件(memfd/shm/tmpfs)”

  • 实现方式

    • dma-buf:面向 DMA 缓冲区共享,核心是 exporter ops + attachment + sg_table + fence/resv。
    • memfd/shm:面向 CPU 地址空间共享,不提供统一的设备 DMA 映射与隐式同步语义。
  • 性能开销

    • dma-buf:在 map/unmap、同步等待、poll/notify 上有额外机制成本,但换来跨设备共享与正确同步。)
  • 资源占用

    • dma-buf:底层存储由 exporter 决定(可能是 pages、carveout、IOMMU 映射等),并伴随 resv/fence 元数据。
  • 隔离级别

    • dma-buf:以 fd 为句柄,权限由 fd 传递控制;设备访问能力通过 attachment 绑定设备。
  • 启动速度

    • dma-buf:建立共享关系通常要 export→fd→get→attach→map,多步骤;但这是为了明确控制共享与同步。

2) dma-buf vs “每设备独立分配 + 自定义传递”

  • dma-buf 的主要优势就是避免 N×M 的私有接口组合;并统一同步。
  • 代价是所有参与者都要遵循锁定约定与同步模型,否则系统性问题更隐蔽。)

3) dma-buf vs dma-heap(补充理解边界)

  • dma-heap 更像“面向用户态申请 dma-buf 的分配入口/内存池策略”,而 dma-buf.c 是“共享对象与语义核心”。两者经常一起出现,但职责不同。你在继续读源码时会很快感受到这一点(drivers/dma-buf/dma-heap.c 是另一条线)。

总结

关键特性

  • dma_buf 作为可通过 fd 传递的共享 DMA 缓冲区对象,并支持跨设备 attachment、映射为 sg_table。
  • dma-resv/dma-fence 为核心的隐式同步,CPU 访问路径会等待相关 fences。)
  • 明确的锁定约定,是正确使用与排障的第一要点。)
  • 核心实现还包含全局列表迭代、poll 支持、命名与统计等工程性能力。

dma-heapdma-buf 的职责关系:分配入口/内存池策略 vs 共享对象/语义核心

1) dma-buf 是“共享对象与语义核心”

dma-buf.c 这类代码主要在做两件事:

  • 定义共享对象 struct dma_buf 的生命周期与规则
    例如:引用计数、release 路径、attachments 管理、fence/同步语义、统计与 debug 导出等。
  • 提供跨驱动共享的通用接口
    例如:导出(export)、导入(attach/map/detach)、文件描述符关联、dentry/vfs 相关回调等。

也就是说:
dma-buf 不关心“这块内存来自哪个池子、怎么分配出来的”,它关心的是:
一旦你给我一个 buffer,我怎么把它变成一个可共享、可同步、可回收的‘共享对象’


2) dma-heap 更像“面向用户态申请 dma-buf 的分配入口/内存池策略”

dma-heap 的定位更偏“上游入口 + 策略”:

  • 它提供给用户态一个标准入口(通常是 /dev/dma_heap/<heap> 这样的设备节点)
    用户态通过 ioctl 之类请求“给我分配一个 dma-buf”。
  • 它把“分配策略”抽象成不同 heap
    例如:system heap、cma heap、carveout heap(具体有哪些取决于内核实现/配置)。
  • 它最终会创建一个 dma-buf(或者包装成 dma-buf 导出的对象)返回给用户态。

也就是说:
dma-heap 的职责是:
决定从哪里分配、用什么策略分配、给用户态一个可用的 dma-buf fd


3) 两者如何“经常一起出现”

典型链路(概念上)是:

  1. 用户态:打开某个 heap 设备节点,请求分配
  2. dma-heap:选择对应 heap 的分配器,分配物理/页面/SG 列表等底层内存
  3. dma-heap:把分配结果“导出”为一个 dma-buf 对象(绑定 ops,形成共享语义)
  4. 返回给用户态:一个 dma-buf 的 fd
  5. 后续共享:别的驱动/子系统通过 dma-buf attach/map 等使用同一份底层内存

这里的分工就是:

  • dma-heap:分配与池化策略(入口、选择、分配)
  • dma-buf:共享与生命周期语义(对象、同步、引用、回收)

DMA-BUF 初始化与生命周期管理:dma_buf_init / dmabuffs_dname / dma_buf_release / dma_buf_file_release

先说明适用边界(与你要求的 STM32H750 单核无 MMU 视角直接相关):
这段代码属于 Linux 内核 dma-buf 子系统 + VFS(挂载、dentry、文件释放)+ sysfs/debugfs。它依赖内核的挂载体系、dentry 生命周期、模块引用计数、以及通常与虚拟内存映射相关的约束(例如 vmapping_counter)。因此在 STM32H750(ARMv7-M、无 MMU、裸机/RTOS) 场景中无法直接运行;但其中的“资源生命周期一致性”“引用计数与不变量检查”“并发保护(spinlock)”思想仍可用于你在嵌入式驱动/中间件里设计资源管理。


dma_buf_init: 在子系统初始化阶段建立 dma-buf 的 sysfs/debugfs 与伪文件系统挂载

此函数是dma-buf 子系统的启动入口,通过 subsys_initcall 在内核子系统初始化阶段执行。它的核心动作是:

  1. 初始化统计 sysfs 节点dma_buf_init_sysfs_statistics() 负责把 dma-buf 的统计信息以 sysfs 形式对外发布(失败即直接退出,避免半初始化状态)。
  2. 挂载 dma-buf 专用伪文件系统kern_mount(&dma_buf_fs_type) 创建并挂载一个仅供内核使用的 mount,用于承载 dma-buf 的匿名文件/dentry 管理(这是后续 dentry 回调能够生效的前提)。
  3. 初始化 debugfsdma_buf_init_debugfs() 用于调试信息导出;此处不影响核心功能但方便定位问题。

该函数体现的关键技巧是:把一个核心资源(dma_buf_mnt 挂载点)作为全局根对象建立起来,后续所有 dma-buf 文件对象的 dentry 生命周期都依赖它。


dmabuffs_dname: 为 dma-buf dentry 动态生成可读的路径名称

此函数是 dentry 的 .d_dname 回调,用于按需生成 dentry 的显示名称。其核心机制是:

  1. dentry->d_fsdata 取回 struct dma_buf:驱动/子系统把私有对象指针挂在 dentry 上,VFS 回调时再取回,这是典型的“对象挂接”模式。
  2. spin_lock 保护 name 读取dmabuf->name 可能被并发更新或释放,因此通过 dmabuf->name_lock 在读取期间保持一致性。
  3. 使用 dynamic_dname 输出格式化名称:返回值不是静态缓冲区,而是通过 dynamic_dname 在 VFS 需要时格式化输出(避免长期保存字符串导致额外生命周期管理复杂度)。

dma_buf_release: dentry 释放时销毁 dma-buf 对象并回收所有关联资源

此函数是 dentry 的 .d_release 回调,是 dma-buf 对象最终销毁路径 的关键部分。它的核心原则是:在释放前强制验证不变量,防止“仍在使用的对象被释放”

关键点(只讲关键技巧,不讲“代码怎么跑”的基础):

  1. 不变量检查(BUG_ON)

    • BUG_ON(dmabuf->vmapping_counter);:要求不存在仍在进行的 vmap 映射计数,否则释放会导致地址空间/引用悬挂。
    • BUG_ON(dmabuf->cb_in.active || dmabuf->cb_out.active);:要求不存在仍活跃的 fence 回调状态,否则说明异步完成路径存在引用不平衡或状态机错误。
  2. 先拆统计与驱动资源,再拆通用资源

    • dma_buf_stats_teardown(dmabuf):撤销统计信息,避免 sysfs/debugfs 仍引用已释放对象。
    • dmabuf->ops->release(dmabuf):调用导出方提供的 release,释放与底层 exporter 相关的资源。
  3. 处理内嵌 resv 对象的析构

    • dmabuf->resv 指向 &dmabuf[1](即紧随主结构体的内嵌对象),则需要 dma_resv_fini() 做对象析构。
  4. 确保附件链表为空并维护模块引用计数

    • WARN_ON(!list_empty(&dmabuf->attachments));:释放时不应仍有 attachment。
    • module_put(dmabuf->owner);:归还 exporter 模块引用,保证模块可卸载性与引用一致性。
  5. 最终释放内存:释放 name 字符串与 dmabuf 本体。


dma_buf_file_release: dma-buf 文件关闭路径中的全局列表摘除

此函数是 file 的释放路径(通常由 file_operations->release 触发),它的核心动作很集中:

  1. is_dma_buf_file(file):确认该 file 确实是 dma-buf 文件,避免误删。
  2. __dma_buf_list_del(file->private_data):从 dma-buf 的全局跟踪链表中移除该对象(属于“框架侧 bookkeeping”)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
* @brief dma-buf 子系统初始化入口函数
*
* 通过 subsys_initcall 在内核子系统初始化阶段执行,用于:
* - 初始化 sysfs 统计导出
* - 挂载 dma-buf 伪文件系统并保存挂载点
* - 初始化 debugfs 调试导出
*
* @return 成功返回 0,失败返回负错误码
*/
static int __init dma_buf_init(void) /* 子系统初始化函数,仅在 init 阶段使用 */
{
int ret; /* 保存各初始化步骤的返回值 */

/* 初始化 dma-buf 统计的 sysfs 导出;失败则终止,避免进入半初始化状态 */
ret = dma_buf_init_sysfs_statistics(); /* 建立 sysfs 统计节点 */
if (ret) /* 若失败则直接返回错误码 */
return ret; /* 向上层传播错误 */

/* 挂载 dma-buf 的伪文件系统;该挂载点承载 dma-buf 文件对象的 dentry 生命周期 */
dma_buf_mnt = kern_mount(&dma_buf_fs_type); /* 创建并挂载内核私有 mount */
if (IS_ERR(dma_buf_mnt)) /* 若挂载返回错误指针 */
return PTR_ERR(dma_buf_mnt); /* 返回对应负错误码 */

/* 初始化 debugfs 导出;用于调试观测,不影响核心功能语义 */
dma_buf_init_debugfs(); /* 建立 debugfs 节点 */
return 0; /* 初始化成功 */
}
subsys_initcall(dma_buf_init); /* 指定在 subsys 初始化阶段调用 dma_buf_init */

/**
* @brief 生成 dma-buf dentry 的动态显示名称
*
* 该函数被 VFS 在需要展示 dentry 名称时调用,通过 dentry->d_fsdata 取回 dmabuf,
* 并在 name_lock 保护下读取 dmabuf->name,最终使用 dynamic_dname 格式化输出。
*
* @param dentry 需要生成名称的目录项
* @param buffer 输出缓冲区
* @param buflen 输出缓冲区长度
* @return 返回由 dynamic_dname 生成的名称指针
*/
static char *dmabuffs_dname(struct dentry *dentry, char *buffer, int buflen)
{
struct dma_buf *dmabuf; /* dma-buf 核心对象指针(挂在 dentry->d_fsdata) */
char name[DMA_BUF_NAME_LEN]; /* 临时名称缓冲,用于拷贝 dmabuf->name */
ssize_t ret = 0; /* strscpy 返回值:>0 表示成功拷贝长度,<=0 表示失败或空 */

/* 从 dentry 私有数据取回 dmabuf 对象 */
dmabuf = dentry->d_fsdata; /* d_fsdata 由创建 dentry 时设置,指向 struct dma_buf */

/* 读取 dmabuf->name 需要并发保护,避免与更新/释放竞争 */
spin_lock(&dmabuf->name_lock); /* 自旋锁保护 name 指针与其内容的一致性 */
if (dmabuf->name) /* 若名称存在则拷贝到本地缓冲 */
ret = strscpy(name, dmabuf->name, sizeof(name)); /* 拷贝名称,避免直接暴露指针生命周期 */
spin_unlock(&dmabuf->name_lock); /* 结束保护区 */

/* 生成形如 "/<dentry-name>:<dmabuf-name>" 的动态名称;若无 dmabuf->name 则使用空串 */
return dynamic_dname(buffer, buflen, "/%s:%s",
dentry->d_name.name, ret > 0 ? name : ""); /* ret>0 才认为 name 有效 */
}

/**
* @brief dentry 释放回调:销毁 dma-buf 对象并回收资源
*
* 该回调在 dentry 生命周期结束时触发。释放前必须满足关键不变量:
* - 不存在仍在进行的 vmap 计数
* - 不存在仍活跃的回调状态(cb_in/cb_out)
* - attachments 列表应为空(否则说明仍被外部持有)
*
* @param dentry 被释放的目录项
*/
static void dma_buf_release(struct dentry *dentry)
{
struct dma_buf *dmabuf; /* 待释放的 dma-buf 对象 */

/* 从 dentry 私有数据取回 dmabuf */
dmabuf = dentry->d_fsdata; /* d_fsdata 指向 struct dma_buf */
if (unlikely(!dmabuf)) /* 若为空则无需处理(防御性检查) */
return; /* 直接返回 */

/* 强制要求不存在仍在进行的 vmap 映射引用 */
BUG_ON(dmabuf->vmapping_counter); /* 若非 0,说明映射引用不平衡,释放会导致严重错误 */

/* 强制要求不存在仍活跃的 fence 回调状态 */
BUG_ON(dmabuf->cb_in.active || dmabuf->cb_out.active); /* 若为真,说明异步状态机不一致 */

/* 拆除统计信息导出,避免外部观测接口继续引用该对象 */
dma_buf_stats_teardown(dmabuf); /* 释放与统计相关的资源 */

/* 调用 exporter 提供的 release,用于释放底层导出方持有的资源 */
dmabuf->ops->release(dmabuf); /* exporter 释放:通常涉及底层 buffer/fence/映射等 */

/* 若 resv 对象为内嵌(紧随 dmabuf 分配),则需要在此执行析构 */
if (dmabuf->resv == (struct dma_resv *)&dmabuf[1]) /* 判断 resv 是否指向内嵌区域 */
dma_resv_fini(dmabuf->resv); /* 完成 resv 对象析构 */

/* 释放时不应仍存在 attachment;若存在,提示严重生命周期错误 */
WARN_ON(!list_empty(&dmabuf->attachments)); /* attachment 非空意味着仍有外部引用关系未解除 */

/* 归还 exporter 模块引用,维护模块卸载语义一致性 */
module_put(dmabuf->owner); /* owner 为 exporter 模块,释放对应引用 */

/* 释放名称字符串(若存在) */
kfree(dmabuf->name); /* dmabuf->name 由 kmalloc 分配 */

/* 释放 dmabuf 主对象内存 */
kfree(dmabuf); /* dmabuf 本体释放,生命周期结束 */
}

/**
* @brief file release 回调:从 dma-buf 全局列表中摘除该文件关联对象
*
* @param inode 文件 inode
* @param file 文件对象
* @return 成功返回 0;若 file 不是 dma-buf 文件则返回 -EINVAL
*/
static int dma_buf_file_release(struct inode *inode, struct file *file)
{
/* 确认该 file 为 dma-buf file,避免误删 */
if (!is_dma_buf_file(file)) /* 检查 file 类型 */
return -EINVAL; /* 非 dma-buf 文件,拒绝处理 */

/* 从 dma-buf 全局列表中移除与该 file 关联的对象 */
__dma_buf_list_del(file->private_data); /* private_data 持有列表节点或关联对象指针 */

return 0; /* 释放流程完成 */
}

/**
* @brief dma-buf dentry 操作集
*
* 将 dentry 的动态命名与释放回调绑定到 dma-buf 的 dentry 上:
* - d_dname:用于动态生成显示名称
* - d_release:用于在 dentry 释放时销毁 dmabuf
*/
static const struct dentry_operations dma_buf_dentry_ops = {
.d_dname = dmabuffs_dname, /* 动态名称生成回调 */
.d_release = dma_buf_release, /* dentry 释放回调 */
};

/** @brief dma-buf 伪文件系统的挂载点,全局唯一,用于承载 dma-buf 的 dentry/文件对象 */
static struct vfsmount *dma_buf_mnt; /* 由 dma_buf_init 中 kern_mount 初始化 */

drivers/dma-buf/dma-fence.c DMA Fence(DMA 栅栏)同步机制

[功能概述] 为异步 DMA 硬件操作提供内核内的统一同步原语(可跨驱动、可跨进程边界通过用户态载体传递),并与 dma-buf/dma-resv 形成显式/隐式同步体系。

介绍

dma-fence 用 struct dma_fence 表示,是 Linux 内核中面向异步硬件工作(GPU 渲染、视频编解码、显示等)的同步原语:通过 dma_fence_init() 初始化,通过 dma_fence_signal()(或带时间戳的变体)标记完成;同一 context 上的 fence 具有全序关系,并可用于显式 fencing(sync_file)、子系统封装(如 DRM syncobj),以及隐式 fencing(挂在 dma_buf 的 dma_resv 上)。


历史与背景

这项技术为了解决什么问题而诞生

核心问题是:共享 buffer 的多设备/多驱动异步访问需要统一的“完成信号”语义,否则容易出现数据竞争、重复写、读到未完成结果等问题。dma-buf 文档明确把 dma-fence 定义为“指示异步硬件操作何时完成”的机制,并与 dma-resv 一起实现隐式同步。

重要里程碑或迭代点(从源码注释与近期讨论归纳)

  • 2012 年左右进入主线生态:源码头部版权与作者信息显示该机制至少在 2012 年已形成并由多方维护。
  • 跨驱动契约(cross-driver contract)与 lockdep 注解体系:源码文档强调“跨驱动一致规则”、超时/恢复、以及用 dma_fence_begin_signalling()/dma_fence_end_signalling() 标注可能影响 dma_fence_signal() 路径的临界区,以便 lockdep 检查死锁风险。
  • 显式同步用户态载体 sync_file 成熟:sync_file 文档把它定义为 fences 的载体,用于驱动与用户态之间传递同步点,从而实现跨进程边界的显式 fencing。
  • 新增“deadline hint”提示机制(优化调度/等待体验):源码中专门描述了可通过 dma_fence_set_deadline(以及用户态 ioctl 间接路径)提供“紧迫性提示”。
  • 2025 年关于 dma_fence 模块卸载/RCU 保护的持续改进:LWN 转发的补丁讨论指出:fence 可能泄漏到外部驱动并在 signaled 后仍延迟释放,导致发行者模块卸载后 dma_fence_ops 不可用而崩溃,补丁集尝试用 RCU 保护 ops 并允许 fence 更“自包含”。

社区活跃度与主流应用情况

  • 主流应用:dma-buf 文档点名 DRM 大量使用 dma-buf 交换 buffer,并与 V4L2 等子系统交互;其中同步体系的三大原语就包含 dma-fence。
  • 活跃度:内核官方文档页面近期仍在更新(docs.kernel.org 页面显示近期发布/抓取),并且 2025 年仍有围绕 dma_fence 的设计问题在 dri-devel 等社区持续讨论。

核心原理与设计

核心工作原理

  1. 对象模型struct dma_fence 表示一个同步点,具有“未完成/已完成”状态;初始化用 dma_fence_init(),完成用 dma_fence_signal()(或 dma_fence_signal_timestamp() 记录完成时间戳)。
  2. context + seqno 的全序语义:通过 dma_fence_context_alloc() 分配 context,同一 context 上 fence 全序。源码用一个全局 atomic64 计数器分配 context。
  3. 等待与唤醒路径dma_fence_wait_timeout() 会进行可睡眠检查,并在等待前调用 dma_fence_enable_sw_signaling() 触发“尽快完成”的软件信号支持(内部会调用 dma_fence_ops.enable_signaling)。
  4. 回调机制:fence 支持注册 callback;源码也明确警告取消 callback 风险高(容易引入死锁/竞态),通常仅用于硬件卡死恢复等场景。
  5. 跨驱动契约约束:源码文档强调“必须在合理时间内完成”“需要 hang recovery”“等待场景下的锁/内存分配限制”等,目的是避免跨子系统组合时产生不可控死锁与回收路径问题。

主要优势

  • 跨驱动同步契约统一:把“异步硬件完成”抽象成跨驱动可理解的对象,降低多子系统协作成本。
  • 显式/隐式同步都能承载:既能通过 sync_file 走显式 fencing(跨进程传递),也能通过 dma_resv 走隐式 fencing(buffer 附带同步点)。
  • 调试与可观测性:源码导出 tracepoint(如 dma_fence_emit/dma_fence_signaled 等),便于跟踪同步行为。

已知劣势、局限性与不适用点

  • 设计上强依赖“发行者必须可靠完成/恢复”:如果硬件/驱动无法保证 fence 在合理时间完成,或缺失 hang recovery,系统整体等待链条会放大风险。
  • 回调取消与复杂锁层级容易出错:源码对取消 callback 的风险做了强提示;此外还需要遵循锁注解与 dma_resv 锁层级规则。
  • 模块卸载边界问题:共享 buffer 会让 fence “流入外部驱动”,signaled 后仍可能延迟释放,导致发行者模块卸载带来 ops 指针失效风险;社区仍在通过 RCU 等手段改进。

使用场景

首选场景(举例)

  • 图形/显示管线:DRM 在进程/上下文之间交换 buffer,需要用 fence 表达“GPU 渲染完成/scanout 完成”等同步点。
  • 媒体处理:与 V4L2 等协作时,在 producer/consumer 间传递 buffer,并用 fence 保证读写顺序。
  • 用户态显式同步(Vulkan/OpenGL/媒体栈互操作):用 sync_file 把 fence 作为 fd 传递,实现跨进程显式 fencing。

不推荐使用的场景

  • 纯驱动内部、无跨设备共享需求的简单同步:优先用 completion/waitqueue 等更直接的内核同步原语(更少契约约束、更少与 dma_resv/用户态交互复杂度)。
  • 无法提供可靠超时与恢复路径的硬件任务:因为跨驱动等待链条会把“不可完成 fence”的风险放大到系统层面。

对比分析

维度 dma_fence completion / waitqueue(典型内核同步) sync_file
定位 异步 DMA 同步点(跨驱动契约) 通用内核同步(多用于同一子系统/驱动内部) fence 的用户态载体(fd 传递)
实现方式 struct dma_fence + ops + callback + wait waitqueue/completion 结构 + 唤醒 file 对象包装一个/多个 fence,并提供导入导出流程
性能开销 创建轻量,但启用 signalling/等待会触发 ops 与回调管理;强调可观测性与契约约束 通常更直接、更少跨子系统约束 增加 file/fd 管理与跨进程传递成本
资源占用 fence 对象 + 回调链 + 可能的追踪/注解;共享场景下生命周期更复杂 一般更少对象间外溢 额外的 file 结构与 fd 生命周期
隔离级别 可跨驱动、可跨进程边界(借助载体) 通常局限在内核内部调用关系 面向用户态边界的显式同步
“启动速度”(可理解为创建/投入使用的时延) dma_fence_init() 初始化即可发布;但要遵循发布时机与 lockdep 注解规则 通常更快更直接 需要创建 file、安装 fd 等步骤

补充:drm_syncobj 属于 DRM 子系统提供的显式 fencing 原语,相比 sync_file,其特点是允许底层 fence 被更新(源码在 overview 中点到这一差异)。


总结

关键特性

  • struct dma_fence 作为异步 DMA 完成信号的统一抽象;context 保证同一执行序列全序。
  • 支持等待/回调;等待前可触发软件 signalling;取消回调需极谨慎。
  • 面向跨驱动组合的严格契约:超时与恢复、锁与内存分配上下文约束、lockdep 注解。
  • 与 dma-buf/dma-resv 形成显式(sync_file)与隐式同步体系。

dma_fence_init_stub: 初始化并立即完成一个全局 stub fence,用于提供可用的默认同步对象

在内核子系统初始化阶段,构造一个全局的“stub fence”,并把它立即 signal(完成)。这样做的目的不是表示真实的异步 DMA 工作,而是提供一个始终已完成、且具备合法 dma_fence 语义的默认对象,供框架在某些“需要 fence 但当前没有真实 fence”或“需要一个占位且永不阻塞”的路径中使用。

这段代码背后的关键点是 dma_fence 的跨驱动契约:

  • fence 必须可等待、可 signal、可识别(driver/timeline 名称)
  • fence 必须遵循统一的锁与回调规则,以便跨设备、跨子系统进行同步
    stub fence 的存在可以让框架在缺省情况下仍保持这些规则成立。

核心原理概述

  1. 提供最小可用的 dma_fence_ops
    通过 dma_fence_stub_ops 提供 .get_driver_name.get_timeline_name,确保该 fence 在调试/追踪时具备可识别的名称。

  2. 初始化 fence 的核心字段与锁
    dma_fence_init() 会把 fence 与 ops、lock、context、seqno 绑定,建立 fence 的基础身份与并发保护策略。

  3. 打开 signal 能力位并立即 signal
    DMA_FENCE_FLAG_ENABLE_SIGNAL_BIT 允许 fence 走 signal 路径;随后立刻 dma_fence_signal(),使该 fence 永远处于“已完成”状态。

  4. 在 subsys_initcall 阶段完成初始化
    通过 subsys_initcall(dma_fence_init_stub) 保证该 stub 在大量子系统使用 fence 之前就已就绪,避免早期路径拿到未初始化对象。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/*
* dma_fence_stub_get_name: 返回 stub fence 的固定名称
* @fence: 当前 fence 对象
*
* 该函数用于同时作为 driver name 与 timeline name 的获取回调,
* 以保证 stub fence 在调试、追踪与输出中具备可识别的名字。
*/
static const char *dma_fence_stub_get_name(struct dma_fence *fence)
{
return "stub";
}

/*
* dma_fence_stub_ops: stub fence 的操作集
*
* stub fence 并不表示真实硬件队列或真实时间线,
* 其目的只是提供最小的可识别信息,使其符合 dma_fence 的基本接口契约。
*/
static const struct dma_fence_ops dma_fence_stub_ops = {
.get_driver_name = dma_fence_stub_get_name, /* 返回驱动名称 */
.get_timeline_name = dma_fence_stub_get_name, /* 返回时间线名称 */
};

/*
* dma_fence_init_stub: 初始化并完成全局 stub fence
*
* 在内核子系统初始化阶段构造一个全局 dma_fence:
* - 使用 stub ops 与全局锁初始化其核心字段
* - 设置允许 signal 的标志位
* - 立即 signal,使其永久处于完成状态
*/
static int __init dma_fence_init_stub(void)
{
/*
* 初始化 dma_fence 核心字段:
* - &dma_fence_stub: 全局 stub fence 对象
* - &dma_fence_stub_ops: 操作集,用于获取名称等
* - &dma_fence_stub_lock: fence 内部并发保护锁
* - context=0, seqno=0: 使用固定上下文与序号作为占位身份
*
* 对单核 STM32H750 视角的关键点:
* - 单核不意味着无并发:内核仍存在抢占/中断等并发语义
* - 因此 fence 仍必须依赖锁来保证状态转换一致性
*/
dma_fence_init(&dma_fence_stub, &dma_fence_stub_ops,
&dma_fence_stub_lock, 0, 0);

/*
* 设置允许 signal 的标志位。
*
* DMA_FENCE_FLAG_ENABLE_SIGNAL_BIT 用于控制 fence 是否允许进入 signal 路径,
* 避免某些 fence 类型在未准备好回调机制时被错误地 signal。
*/
set_bit(DMA_FENCE_FLAG_ENABLE_SIGNAL_BIT,
&dma_fence_stub.flags);

/*
* 立即 signal,使该 fence 永久处于“已完成”状态。
*
* 这样任何对该 fence 的 wait 都不会阻塞,
* 可用于框架内部需要 fence 语义但不希望引入等待的缺省路径。
*/
dma_fence_signal(&dma_fence_stub);
return 0;
}
subsys_initcall(dma_fence_init_stub);

__dma_fence_init / dma_fence_init: 初始化 dma_fence 的身份、并发保护与回调容器

这段代码的核心作用是:把一个已分配的 struct dma_fence 对象初始化成“可被框架安全使用的同步原语实例”。它不负责 signal、不负责等待,只负责建立最关键的三类基础设施:

  1. 生命周期基础:初始化引用计数 refcount,让 fence 能被跨子系统安全持有与释放。
  2. 并发一致性基础:绑定 lock,规定后续所有状态转换(signal、回调链处理等)必须在该锁保护下完成。
  3. 语义比较基础:设置 contextseqno,让 fence 之间可以通过 dma_fence_later() 这类比较函数建立有序关系(同一 context 内要求完全有序)。

核心原理概述

  1. 严格的前置条件检查(BUG_ON)

    • lock 必须存在:否则后续 signal/wait/callback 的并发一致性不可保证。
    • ops 必须提供 get_driver_name/get_timeline_name:这是跨驱动契约的一部分,用于诊断、跟踪与一致的接口语义。缺失会导致框架在 debug/trace/输出路径上出现不可接受的不确定性。
  2. 引用计数初始化(kref_init)

    • fence 的引用计数是其“跨模块共享”的基本机制。
    • 后续任何 attach/等待/回调注册都可能持有 fence,必须依赖 refcount 保证对象不被提前释放。
  3. 回调链表容器初始化(cb_list)

    • INIT_LIST_HEAD(&fence->cb_list) 建立回调链表头。
    • 这使得 dma_fence_add_callback() 能在 fence 未 signal 时挂回调;signal 时可遍历并执行。
  4. 并发保护锁绑定(fence->lock)

    • 该指针定义 fence 的“互斥域”。
    • signal 与 add_callback 的竞态处理依赖这把锁的语义:必须是 irqsafe,以便在可能的中断上下文中也能安全使用。
  5. context/seqno 初始化(有序语义基础)

    • context 表示执行上下文(例如某个硬件队列/时间线)。
    • seqno 在同一 context 内单调递增,用于比较“哪个 fence 更晚”。
    • 这为 dma_fence_later() 提供数学意义上的偏序关系:同一 context 可全序比较,不同 context 不保证可比。
  6. flags 与 error 初始化

    • flags 保存 fence 的状态标志(例如是否允许 enable_signaling 等)。
    • error = 0 表示初始无错误,后续可以通过 signal 带错误完成语义。
  7. trace 钩子

    • trace_dma_fence_init(fence) 用于追踪初始化事件,便于定位生命周期与竞态问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/*
* __dma_fence_init: 初始化 dma_fence 的内部字段
* @fence: 需要初始化的 fence 对象
* @ops: fence 的操作集,提供跨驱动统一的接口实现入口
* @lock: fence 的 irqsafe 自旋锁,用于保护该 fence 的状态与回调链
* @context: fence 所属执行上下文(同 context 内的 fence 必须全序)
* @seqno: 该 context 内单调递增的序列号,用于 fence 先后比较
* @flags: fence 初始标志位集合
*/
static void
__dma_fence_init(struct dma_fence *fence, const struct dma_fence_ops *ops,
spinlock_t *lock, u64 context, u64 seqno, unsigned long flags)
{
/* lock 必须存在,否则无法保证 signal/callback 路径的并发一致性 */
BUG_ON(!lock);

/*
* ops 必须存在,且至少要能提供 driver 名与 timeline 名:
* 这既是调试/追踪要求,也是跨驱动契约的一部分。
*/
BUG_ON(!ops || !ops->get_driver_name || !ops->get_timeline_name);

/* 初始化引用计数,支持 fence 在跨子系统共享时的生命周期管理 */
kref_init(&fence->refcount);

/* 绑定操作集:后续查询、打印、以及可选的 enable_signaling 等行为通过 ops 扩展 */
fence->ops = ops;

/* 初始化回调链表头:用于 dma_fence_add_callback() 注册回调 */
INIT_LIST_HEAD(&fence->cb_list);

/* 绑定并发保护锁:规定 fence 的状态转换与回调链操作必须在该锁保护下进行 */
fence->lock = lock;

/* 设置 fence 的执行上下文与序列号,用于同一 context 内的先后比较 */
fence->context = context;
fence->seqno = seqno;

/* 设置初始标志位 */
fence->flags = flags;

/* 初始错误码为 0;若后续异步任务失败,可通过 error 语义完成 */
fence->error = 0;

/* 记录初始化事件,便于追踪 fence 生命周期 */
trace_dma_fence_init(fence);
}

/*
* dma_fence_init: 初始化一个自定义 fence(对外 API)
* @fence: 需要初始化的 fence 对象
* @ops: fence 的操作集
* @lock: fence 的 irqsafe 自旋锁
* @context: fence 所属执行上下文
* @seqno: 该 context 内单调递增序列号
*
* 该函数是对 __dma_fence_init 的封装,flags 固定为 0。
*/
void
dma_fence_init(struct dma_fence *fence, const struct dma_fence_ops *ops,
spinlock_t *lock, u64 context, u64 seqno)
{
/* flags=0 表示以默认初始标志初始化 fence */
__dma_fence_init(fence, ops, lock, context, seqno, 0UL);
}
EXPORT_SYMBOL(dma_fence_init);

dma_fence_signal_timestamp_locked / dma_fence_signal_timestamp / dma_fence_signal_locked / dma_fence_signal: 以原子方式将 fence 从“未完成”转换为“已完成”,并执行回调链

这组函数实现的是 dma_fence 最核心的状态转换:signal(完成)。它们的共同语义是:

  • 将 fence 的状态从“未 signaled”变为“已 signaled”(只允许一次)
  • 设置完成时间戳(timestamp 版本)
  • 解除所有 dma_fence_wait() 的阻塞条件
  • 执行所有通过 dma_fence_add_callback() 注册的回调
  • 并且用锁与原子位操作保证:signal 与回调注册之间不会出现丢回调、重复回调或释放后访问

这段代码最关键的技巧有三个:

  1. 一次性状态转换的原子保证test_and_set_bit(SIGNALED_BIT) 保证只有第一个调用者能成功 signal,后续调用直接失败返回。
  2. 回调链“先摘链后执行”:用 list_replacefence->cb_list 原子地换成一个临时链表 cb_list,然后在锁保护外(或锁内的后半段)安全遍历执行,避免回调执行过程中破坏 fence 内部链表结构或造成死锁/递归。
  3. 锁的两层封装*_locked 版本要求调用者已持锁,非 locked 版本负责 spin_lock_irqsave/restore,保证在中断上下文与普通上下文都成立。

在 STM32H750(单核、无 MMU)视角下需要强调:

  • 单核不意味着没有并发:中断、抢占、软中断/工作队列都能产生并发交错,因此 spin_lock_irqsave 与原子位仍然必要。
  • irqsave 的意义不是“多核同步”,而是“禁止本 CPU 中断导致的重入竞态”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/*
* dma_fence_signal_timestamp_locked: 在已持 fence->lock 的条件下 signal fence 并设置时间戳
* @fence: 需要 signal 的 fence
* @timestamp: 使用 CLOCK_MONOTONIC 域的完成时间戳
*
* 返回:
* 0 - 成功 signal(第一次生效)
* -EINVAL- fence 已经 signaled(再次调用不生效)
*/
int dma_fence_signal_timestamp_locked(struct dma_fence *fence,
ktime_t timestamp)
{
struct dma_fence_cb *cur, *tmp; /* 回调节点遍历指针 */
struct list_head cb_list; /* 临时回调链表头,用于“摘链执行” */

/* 断言调用者已持有 fence->lock,用于保证状态转换与回调链操作的原子性 */
lockdep_assert_held(fence->lock);

/*
* 将 SIGNALED 位从 0 原子设置为 1,并返回旧值。
* - 若旧值为 1,说明已经 signal 过,本次返回 -EINVAL
* - 若旧值为 0,本次成为“唯一有效 signal”
*
* 这是 fence “只能从未完成->已完成一次”的硬性语义保证。
*/
if (unlikely(test_and_set_bit(DMA_FENCE_FLAG_SIGNALED_BIT,
&fence->flags)))
return -EINVAL;

/*
* 在写入 timestamp 前,先把 fence->cb_list “摘出来”。
*
* list_replace 的效果:
* - 把 fence->cb_list 的链表内容整体移交给局部变量 cb_list
* - fence->cb_list 自身被替换成一个空链表头(仍然可用)
*
* 这样做的关键目的:
* 1) 回调执行过程中不再触碰 fence->cb_list,避免回调递归/并发修改破坏内部结构
* 2) signal 与 add_callback 的竞态能被严格界定:signal 成功后,后续回调注册必须走“立即执行/失败返回”的路径
*/
list_replace(&fence->cb_list, &cb_list);

/* 写入完成时间戳,并设置“timestamp 有效”标志位 */
fence->timestamp = timestamp;
set_bit(DMA_FENCE_FLAG_TIMESTAMP_BIT, &fence->flags);

/* 记录 trace:用于性能分析与事件追踪 */
trace_dma_fence_signaled(fence);

/*
* 执行所有回调(安全遍历)。
*
* list_for_each_entry_safe 允许在遍历时删除节点;
* 这里先把 cur->node 重新 INIT_LIST_HEAD,确保该回调节点脱离链表,避免重复执行与悬挂指针。
*/
list_for_each_entry_safe(cur, tmp, &cb_list, node) {
INIT_LIST_HEAD(&cur->node); /* 标记该回调节点已从链表摘除 */
cur->func(fence, cur); /* 执行回调:由注册方提供的函数 */
}

return 0;
}
EXPORT_SYMBOL(dma_fence_signal_timestamp_locked);

/*
* dma_fence_signal_timestamp: 在未持锁条件下 signal fence,并设置指定时间戳
* @fence: 需要 signal 的 fence
* @timestamp: 完成时间戳(CLOCK_MONOTONIC 域)
*
* 该函数负责加锁(irqsave)并调用 *_locked 版本完成实际工作。
*/
int dma_fence_signal_timestamp(struct dma_fence *fence, ktime_t timestamp)
{
unsigned long flags; /* irqsave 保存的中断标志 */
int ret; /* 返回值 */

/* fence 为空属于调用者错误,返回 -EINVAL 并产生警告 */
if (WARN_ON(!fence))
return -EINVAL;

/*
* 使用 spin_lock_irqsave:
* - 自旋锁保证与其他上下文对同一 fence 的并发操作互斥
* - irqsave 防止本 CPU 中断打断导致的重入竞态
*/
spin_lock_irqsave(fence->lock, flags);
ret = dma_fence_signal_timestamp_locked(fence, timestamp); /* 真正的状态转换与回调执行 */
spin_unlock_irqrestore(fence->lock, flags);

return ret;
}
EXPORT_SYMBOL(dma_fence_signal_timestamp);

/*
* dma_fence_signal_locked: 在已持锁条件下 signal fence(使用当前时间戳)
* @fence: 需要 signal 的 fence
*
* 这是对 dma_fence_signal_timestamp_locked 的简化封装。
*/
int dma_fence_signal_locked(struct dma_fence *fence)
{
/* 使用 ktime_get() 获取当前 CLOCK_MONOTONIC 时间戳 */
return dma_fence_signal_timestamp_locked(fence, ktime_get());
}
EXPORT_SYMBOL(dma_fence_signal_locked);

/*
* dma_fence_signal: 在未持锁条件下 signal fence(使用当前时间戳),并标注 signalling 区间
* @fence: 需要 signal 的 fence
*
* 相比 dma_fence_signal_timestamp,该函数额外使用:
* - dma_fence_begin_signalling / dma_fence_end_signalling
* 用于标注“可能导致 fence 完成”的代码区间,辅助避免死锁与锁依赖问题(lockdep/规则契约)。
*/
int dma_fence_signal(struct dma_fence *fence)
{
unsigned long flags; /* irqsave 保存的中断标志 */
int ret; /* signal 返回值 */
bool tmp; /* begin_signalling 的返回值,用于 end_signalling 配对 */

/* fence 为空属于调用者错误 */
if (WARN_ON(!fence))
return -EINVAL;

/*
* 标注进入 signalling 区间。
* 该机制属于跨驱动契约的一部分,用于帮助识别“等待路径与完成路径”的锁依赖关系,降低死锁风险。
*/
tmp = dma_fence_begin_signalling();

/* 加锁并完成实际 signal 逻辑 */
spin_lock_irqsave(fence->lock, flags);
ret = dma_fence_signal_timestamp_locked(fence, ktime_get());
spin_unlock_irqrestore(fence->lock, flags);

/* 标注退出 signalling 区间,与 begin 成对 */
dma_fence_end_signalling(tmp);

return ret;
}
EXPORT_SYMBOL(dma_fence_signal);