[Linux]学习笔记系列 -- [drivers][dma]dma-buf


title: dma-buf

categories:

  • linux
  • drivers
  • dma
    tags:
  • linux
  • drivers
  • dma
    abbrlink: 1d3fd482
    date: 2025-10-03 09:01:49

https://github.com/wdfk-prog/linux-study

文章目录

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")。

c 复制代码
/**
 * @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 之前就已就绪,避免早期路径拿到未初始化对象。


c 复制代码
/*
 * 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) 用于追踪初始化事件,便于定位生命周期与竞态问题。

c 复制代码
/*
 * __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 中断导致的重入竞态"。

c 复制代码
/*
 * 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);
相关推荐
j_xxx404_2 小时前
Linux:进程优先级与进程切换与调度
linux·运维·服务器
晚霞的不甘2 小时前
Flutter for OpenHarmony《智慧字典》英语学习模块代码深度解析:从数据模型到交互体验
前端·学习·flutter·搜索引擎·前端框架·交互
三水不滴2 小时前
从原理、场景、解决方案深度分析Redis分布式Session
数据库·经验分享·redis·笔记·分布式·后端·性能优化
never_go_away2 小时前
linux Socket限制
linux·运维·服务器
济6172 小时前
linux 系统移植(第二十四期)---- 根文件系统其他功能测试---- Ubuntu20.04根文件系统其他功能测试
linux·运维·服务器
十年编程老舅2 小时前
字节跳动 Linux C/C++ 后端 面经
linux·后端面试·八股文·字节跳动·面试八股文·服务器面试
济6172 小时前
linux 系统移植(第二十五期)---- 运用MfgTool 工具进行linux系统烧写---- Ubuntu20.04
linux·运维·服务器
EverydayJoy^v^2 小时前
RH134简单知识点——第10章——控制启动过程
linux·服务器·网络
LYS_06182 小时前
寒假学习(9)(C语言9+模数电9)
c语言·开发语言·学习