[TOC]

drivers/dma-buf DMA-BUF (DMA Buffer Sharing) Framework 高效的零拷贝缓冲区共享框架

历史与背景

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

这项技术以及其所在的dma-buf子系统,是为了解决现代异构计算系统中一个核心的性能和效率问题:如何在不同的硬件设备(驱动)之间高效地共享内存缓冲区,而无需进行昂贵的数据拷贝

  • 消除数据拷贝(Zero-Copy):在典型的多媒体处理流程中,一个数据流(如视频帧)可能需要经过多个硬件单元处理:例如,从摄像头控制器捕获,由视频编解码器(CODEC)进行编码/解码,交由GPU进行渲染或后期处理,最后发送到显示控制器进行显示。在没有dma-buf的时代,每一步之间的数据传递通常都需要CPU介入,将数据从一个设备的内存区域拷贝到另一个设备的内存区域,这会消耗大量的CPU周期和内存带宽,是系统性能的主要瓶颈。
  • 统一的共享接口:在dma-buf出现之前,不同的子系统有各自私有的缓冲区共享方式(例如V4L2的USERPTR)。 这导致了接口不统一,无法实现任意设备间的缓冲区共享,例如在V4L2设备和DRM(图形)设备之间直接共享数据就很困难。 内核需要一个通用的、标准化的框架来解决这个问题。
  • 跨进程共享:除了内核驱动之间,用户空间的多个进程之间也需要共享硬件缓冲区,例如Wayland合成器需要与客户端应用共享图形缓冲区。 dma-buf通过将缓冲区抽象成文件描述符(File Descriptor),利用了Linux成熟的进程间文件描述符传递机制,安全地实现了跨进程共享。

它的发展经历了哪些重要的里程碑或版本迭代?

dma-buf框架并非一蹴而就,它的发展是为了逐步取代功能类似但设计有局限性的旧机制,并不断完善自身功能。

  • 诞生dma-buf框架在2012年左右被引入Linux内核,其设计受到了来自Linaro等多方开发者的讨论和贡献,旨在提供一个通用的解决方案来取代各种临时的、非标准的共享方法。
  • 取代ION:在Android生态中,Google曾开发了名为ION的内存管理器,其功能与dma-buf有很多重叠。ION虽然解决了Android的燃眉之急,但其设计被认为是“中心化”的,并且长期未能被Linux内核主线完全接受。最终,社区的共识是dma-buf是更优越、更符合内核设计哲学的方案。内核逐步开发了dma-buf-heaps机制,提供了一个标准的、可扩展的方式来分配不同类型的内存(如系统内存、CMA连续内存),旨在完全取代ION。自Android 12和内核5.10起,ION已不再被支持,dma-buf-heaps成为标准。
  • 同步机制的演进:数据共享的核心难题之一是同步。dma-buf框架引入了基于dma-fence的精细化同步机制。 这允许硬件设备之间进行显式或隐式的异步操作同步。例如,GPU可以等到视频解码器完成一帧的解码(通过等待一个dma-fence信号)后,才开始对其进行渲染,整个过程无需CPU阻塞等待。

目前该技术的社区活跃度和主流应用情况如何?

dma-buf是当前Linux内核中进行跨设备缓冲区共享的唯一标准,其社区非常活跃,并且是所有现代多媒体和图形栈的基础。

  • 社区活跃度:作为图形、视频、摄像头等多个核心子系统的交汇点,dma-buf的API和实现会随着新硬件、新用户空间API(如Vulkan)的出现而不断演进。关于其同步模型、缓存处理等方面的讨论和改进一直在进行中。
  • 主流应用
    • 图形系统:DRM/KMS(Direct Rendering Manager)子系统广泛使用dma-buf来实现PRIME技术,支持多GPU之间、GPU与显示控制器之间的数据共享。
    • 视频处理:V4L2(Video for Linux 2)子系统使用dma-buf与GPU、编解码器、显示设备等进行视频帧的零拷贝交换。
    • Android系统:整个Android的多媒体和图形栈都构建在dma-buf之上。
    • GStreamer等多媒体框架:在用户空间,GStreamer等框架利用dma-buf文件描述符来构建高效的零拷贝媒体处理流水线。

核心原理与设计

它的核心工作原理是什么?

dma-buf的核心思想是将一块可被DMA访问的物理内存抽象和封装成一个内核对象(struct dma_buf),并将其生命周期与一个文件描述符绑定。

  1. 三大角色
    • Exporter(导出者):负责分配和管理实际物理内存的设备驱动。例如,一个摄像头驱动分配了一块内存用于存放图像帧,它就是Exporter。Exporter需要实现一套操作函数(struct dma_buf_ops)。
    • Importer/User(导入者/使用者):希望访问这块内存的其他设备驱动。例如,GPU驱动或显示驱动。
    • Userspace(用户空间):扮演“媒人”的角色。它从Exporter获取代表缓冲区的dma-buf文件描述符,然后将这个文件描述符传递给一个或多个Importer。
  2. 工作流程
    • 导出(Export):Exporter驱动分配好内存后,调用dma_buf_export(),传入内存的描述信息(如sg_table)和操作回调函数集。内核会创建一个dma_buf对象,并返回其指针。
    • 生成文件描述符(FD):用户空间通过一个特定于Exporter驱动的ioctl调用(例如DRM的DRM_IOCTL_PRIME_HANDLE_TO_FD),请求内核为这个dma_buf对象创建一个文件描述符,该过程内部会调用dma_buf_fd()
    • 传递:用户空间进程通过标准的IPC机制(如UNIX域套接字)将这个文件描述符传递给需要使用该缓冲区的其他进程。
    • 导入(Import):接收到FD的进程,通过另一个特定于Importer驱动的ioctl调用(例如DRM_IOCTL_PRIME_FD_TO_HANDLE),将FD交给Importer驱动。
    • 获取与附着(Get & Attach):Importer驱动在内核中,首先通过dma_buf_get()从FD得到dma_buf对象的指针,然后调用dma_buf_attach()将自己的设备“附着”到这个dma_buf上。 附着操作让Exporter知道现在有了一个新的使用者,这可能影响其后续的内存管理决策。
    • 映射(Map):当Importer真正需要对这块内存进行DMA操作时,它会调用dma_buf_map_attachment()。这个调用会触发Exporter的map回调函数,最终返回Importer设备可以用来进行DMA的地址列表(sg_table)。
  3. 同步(Synchronization)dma-buf通过dma-fence对象来管理异步访问。每个dma_buf都有一个预留对象(dma_resv),其中可以存放一个或多个dma-fence。当一个设备(如GPU)完成对缓冲区的写入后,会产生一个fence并放入dma-resv中。下一个需要读取该缓冲区的设备(如显示控制器)在访问前,必须先等待这个fence发出信号,从而保证了正确的读写顺序。

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

  • 零拷贝:避免了在不同硬件设备间传递数据时的内存拷贝,这是其最核心的性能优势。
  • 标准化:提供了一套统一的API,可以被任何类型的设备驱动实现,实现了内核级的互操作性。
  • 安全性:基于文件描述符的机制,利用了Linux成熟的访问控制和生命周期管理,比基于裸指针的共享方式更安全。
  • 强大的同步机制:集成了dma-fence,能够处理复杂的异步多设备流水线中的依赖关系,实现了高效的并发。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 缓存一致性复杂性:当CPU也需要访问dma-buf时,缓存一致性管理变得非常复杂。驱动程序必须正确地使用dma_buf_begin_cpu_access()dma_buf_end_cpu_access()来维护CPU缓存和内存之间的数据同步。 错误的使用会导致数据损坏。
  • 同步模型的复杂性:显式同步(Explicit Sync,基于dma-fence)虽然强大,但也增加了用户空间和内核驱动的编程复杂性。开发者需要精确地管理fence的依赖关系。
  • 并非为CPU密集型访问设计dma-buf的核心是为硬件DMA设计的。虽然提供了CPU访问的接口,但如果一个缓冲区的主要访问者是CPU,那么使用普通的匿名内存或共享内存(shmem)可能更简单高效。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

dma-buf是现代Linux系统中进行跨硬件设备零拷贝数据交换唯一标准和首选方案。

  • Android中的视频播放

    1. 视频解码器硬件(Importer)从dma-buf中读取编码数据。
    2. 解码器将解码后的视频帧写入另一个dma-buf(此时解码器是Exporter)。
    3. 用户空间将这个包含视频帧的dma-buf的FD传递给图形合成器(如SurfaceFlinger)。
    4. GPU(Importer)附着到这个dma-buf上,读取视频帧内容,并将其与其他UI元素合成。
    5. GPU将合成后的最终画面写入一个新的dma-buf(GPU是Exporter)。
    6. 显示控制器(Importer)读取这个最终画面的dma-buf并将其显示在屏幕上。
      在整个流程中,视频数据始终在硬件可直接访问的内存中流动,CPU只负责协调,不参与数据拷贝。
  • 摄像头预览

    1. 摄像头ISP(图像信号处理器)作为Exporter,将捕获的图像帧写入一个dma-buf
    2. 用户空间将该dma-buf的FD同时传递给两个Importer:显示控制器(用于在屏幕上实时预览)和视频编码器(用于录制视频)。两个设备可以同时(或在同步信号的协调下)读取同一块内存,实现了高效的预览和录制。

是否有不推荐使用该技术的场景?为什么?

  • 纯CPU数据共享:如果数据只在多个CPU进程间共享,而没有任何硬件设备需要通过DMA访问它,那么使用传统的共享内存机制(如shm_openmmapMAP_SHARED)更简单、开销更低。
  • 非常简单的单向数据流:在一些简单的嵌入式场景中,如果数据流只是单向从一个设备到另一个固定的设备,且没有复杂的同步需求,开发者有时可能会采用一些私有的、更轻量级的机制。但这通常是以牺牲通用性和可维护性为代价的。

对比分析

请将其 与 其他相似技术 进行详细对比。

dma-buf的主要对比对象是其前身,以及一些功能上有重叠的技术。

对比一:dma-buf vs. ION (Android)

特性 dma-buf (及 dma-buf-heaps) ION (已废弃)
设计哲学 去中心化。内存的分配和管理由各自的驱动(Exporter)负责。dma-buf-heaps提供了一个标准的分配接口,但heap本身也是独立的驱动。 中心化。ION是一个核心驱动,管理着多个不同类型的内存堆(Heap),所有分配请求都经过ION核心。
内核集成 主线标准。完全集成在Linux内核主线中,被所有相关子系统原生支持。 外部模块/Staging。长期处于内核Staging目录,从未被主线完全接受,导致了内核生态的碎片化。
接口 用户空间通过/dev/dma_heap/下的设备节点来从特定的heap分配内存。 接口标准化。 用户空间通过一个单一的/dev/ion设备节点和私有的ioctl来分配,需要通过heap ID或掩码来指定内存类型。
灵活性与扩展性 。添加一种新的内存类型只需要编写一个新的、独立的heap驱动。权限可以基于每个heap设备节点进行精细化控制。 。添加新的heap类型或修改行为可能需要修改ION核心代码。
现状 当前标准。自Android 12起,是Android中唯一的标准方案。 已废弃。在新的Android和Linux内核版本中已被移除。

对比二:dma-buf vs. V4L2 USERPTR

特性 dma-buf V4L2 USERPTR
工作模式 零拷贝。内核驱动间直接交换缓冲区引用,用户空间只传递FD。 需要用户空间映射。本质上是用户空间分配内存(或mmap另一个设备的内存),然后将指针传递给V4L2驱动。如果内存来自其他设备,通常需要CPU访问,无法实现真正的零拷贝。
共享范围 通用。可以在任意类型的内核驱动之间共享(DRM, V4L2, aac, etc.)。 有限。主要用于用户空间和单个V4L2驱动之间传递缓冲区,不适合在多个不同类型的内核驱动间直接共享。
同步 原生支持。通过dma-fence提供强大的跨设备异步同步能力。 无原生机制。同步需要用户空间自己负责,或者依赖于驱动的阻塞行为。
效率 。避免了CPU拷贝和不必要的上下文切换。 。可能涉及CPU拷贝,且用户空间指针的转换和映射开销较大。

drivers/dma-buf/dma-buf.c

DMA-BUF VFS接口:将缓冲区生命周期与文件系统对象绑定

本代码片段揭示了DMA-BUF框架如何与Linux的虚拟文件系统(VFS)深度集成,以管理DMA缓冲区的生命周期。其核心功能是定义和实现dmabuf伪文件系统所需的回调函数,特别是dentry_operations。通过将每一个DMA缓冲区(struct dma_buf)与一个目录项(dentry)关联,DMA-BUF巧妙地利用了VFS成熟的引用计数机制。当代表DMA缓冲区的所有文件描述符都被关闭,并且内核中没有其他地方持有对它的引用时,VFS会自动调用此代码中定义的dma_buf_release函数,从而触发缓冲区的最终销毁和资源的释放。

实现原理分析

此机制的实现原理是将一个内核内存对象(dma_buf)的生命周期完全委托给一个文件系统对象(dentry)来管理。

  1. 文件系统上下文初始化 (dma_buf_fs_init_context):

    • dma_buf_init函数(在上一节分析过)调用kern_mount时,VFS会调用这个init_fs_context回调。
    • 它使用init_pseudo来设置一个标准的内存文件系统上下文。
    • 最关键的一步是ctx->dops = &dma_buf_dentry_ops;。这行代码将所有在该文件系统下创建的dentry的操作,都指向了我们自定义的dma_buf_dentry_ops结构体。
  2. Dentry操作 (dma_buf_dentry_ops):

    • 这是一个dentry_operations结构体,它定义了当VFS对一个dentry执行特定操作时应该调用的函数。
    • .d_release: 这是最重要的回调。当一个dentry的引用计数(d_count)降为零时,VFS会调用此函数。在DMA-BUF的场景下,这意味着内核中不再有任何地方引用这个特定的DMA缓冲区。
    • .d_dname: 这是一个辅助回调,用于动态生成dentry的名字,主要用于调试目的(例如,在/proc/[pid]/fdinfo/中显示更友好的名字)。
  3. 缓冲区释放 (dma_buf_release):

    • 这是DMA-BUF的“析构函数”。当它被VFS调用时,它会执行一系列清理步骤来彻底销毁DMA缓冲区:
      a. 通过dentry->d_fsdata获取与之关联的dma_buf结构体指针。
      b. 执行一系列BUG_ONWARN_ON检查,确保缓冲区的状态是一致的(例如,没有活动的虚拟映射,没有悬挂的回调,没有遗留的附件)。
      c. 调用dmabuf->ops->release(dmabuf)。这是一个函数指针,指向导出者驱动(即创建该缓冲区的驱动)提供的release方法。这是将控制权交还给具体设备驱动以释放硬件相关资源(如释放DMA内存)的关键步骤。
      d. 清理DMA预留对象(dma_resv)、释放dma_buf结构体本身以及其名字字符串所占用的内存。
  4. 文件释放 (dma_buf_file_release):

    • 当一个打开DMA-BUF所得的文件描述符被close()时,VFS会调用这个函数。它与dma_buf_release不同,dma_buf_release是在dentry(代表缓冲区本身)的最后一个引用消失时调用,而dma_buf_file_release是在一个struct file(代表一个打开的实例)的最后一个引用消失时调用。此函数主要负责清理与该文件实例相关的簿记信息。

代码分析

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
// dmabuffs_dname: 动态生成dentry的名字,用于调试。
static char *dmabuffs_dname(struct dentry *dentry, char *buffer, int buflen)
{
struct dma_buf *dmabuf;
char name[DMA_BUF_NAME_LEN];
ssize_t ret = 0;

// 从dentry的私有数据中获取dma_buf指针。
dmabuf = dentry->d_fsdata;
// 加锁以安全地访问可能被并发修改的name字段。
spin_lock(&dmabuf->name_lock);
if (dmabuf->name)
ret = strscpy(name, dmabuf->name, sizeof(name));
spin_unlock(&dmabuf->name_lock);

// 格式化输出字符串,格式为 "/<inode号>:<缓冲区名>"
return dynamic_dname(buffer, buflen, "/%s:%s",
dentry->d_name.name, ret > 0 ? name : "");
}

// dma_buf_release: dentry的release回调,是dma_buf的最终析构函数。
// 当dentry的最后一个引用消失时,由VFS调用。
static void dma_buf_release(struct dentry *dentry)
{
struct dma_buf *dmabuf;

dmabuf = dentry->d_fsdata;
if (unlikely(!dmabuf))
return;

// BUG_ON检查,确保在释放时没有任何活动的虚拟映射。
BUG_ON(dmabuf->vmapping_counter);

/*
* 如果触发了这个BUG(),可能意味着:
* * 在dma_buf_poll / dma_buf_poll_cb或其他地方存在文件引用不平衡。
* * 尽管没有挂起的fence回调,但dmabuf->cb_in/out.active仍不为0。
*/
BUG_ON(dmabuf->cb_in.active || dmabuf->cb_out.active);

// 清理统计信息。
dma_buf_stats_teardown(dmabuf);
// 调用导出者驱动提供的release回调函数,这是释放硬件资源的关键步骤。
dmabuf->ops->release(dmabuf);

// 如果预留对象是内联分配的,则对其进行清理。
if (dmabuf->resv == (struct dma_resv *)&dmabuf[1])
dma_resv_fini(dmabuf->resv);

// 警告:在释放时,附件列表应为空。
WARN_ON(!list_empty(&dmabuf->attachments));
// 递减导出者模块的引用计数。
module_put(dmabuf->owner);
// 释放为名字和dma_buf结构体本身分配的内存。
kfree(dmabuf->name);
kfree(dmabuf);
}

// dma_buf_file_release: file的release回调。
// 当一个打开dma_buf文件的struct file的最后一个引用消失时调用。
static int dma_buf_file_release(struct inode *inode, struct file *file)
{
if (!is_dma_buf_file(file))
return -EINVAL;

// 从一个全局列表中删除此文件实例的记录。
__dma_buf_list_del(file->private_data);

return 0;
}

// dma_buf_dentry_ops: 为dmabuf文件系统中的dentry定义的操作函数。
static const struct dentry_operations dma_buf_dentry_ops = {
.d_dname = dmabuffs_dname, // 动态命名函数
.d_release = dma_buf_release, // 核心的释放/析构函数
};

// dma_buf_mnt: 指向内部挂载的dmabuf文件系统的vfsmount结构体。
static struct vfsmount *dma_buf_mnt;

// dma_buf_fs_init_context: dmabuf文件系统上下文的初始化函数。
static int dma_buf_fs_init_context(struct fs_context *fc)
{
struct pseudo_fs_context *ctx;

// 使用通用的伪文件系统辅助函数进行初始化。
ctx = init_pseudo(fc, DMA_BUF_MAGIC);
if (!ctx)
return -ENOMEM;
// 关键步骤:将该文件系统所有dentry的操作指向我们自定义的dma_buf_dentry_ops。
ctx->dops = &dma_buf_dentry_ops;
return 0;
}

// dma_buf_fs_type: 定义了 "dmabuf" 这个伪文件系统的类型。
static struct file_system_type dma_buf_fs_type = {
.name = "dmabuf",
.init_fs_context = dma_buf_fs_init_context,
.kill_sb = kill_anon_super,
};

DMA-BUF子系统初始化:创建用于缓冲区共享的伪文件系统

本代码片段展示了Linux内核中DMA-BUF子系统的核心初始化和退出逻辑。其主要功能是在内核启动期间,注册一个名为dmabuf的内部专用的伪文件系统(pseudo-filesystem),并创建一个该文件系统的挂载实例。这个文件系统是整个DMA-BUF框架的基石,它允许将内核中的DMA缓冲区(dma_buf)封装成匿名的文件描述符(file descriptor),从而使得这些缓冲区可以在不同的设备驱动之间,甚至跨进程安全、高效地共享,而无需进行昂贵的内存拷贝。

实现原理分析

该初始化过程的实现原理是利用Linux的虚拟文件系统(VFS)层来为非持久化的内核对象(即DMA缓冲区)提供一个文件接口。

  1. 文件系统类型定义 (dma_buf_fs_type):

    • 代码首先定义了一个file_system_type结构体。这是向VFS注册一种新文件系统的标准方式。
    • .name = "dmabuf": 指定了文件系统的名字。
    • .init_fs_context.kill_sb: 分别是文件系统挂载和卸载时用于处理超级块(superblock)的回调函数。kill_anon_super是一个通用的辅助函数,用于销毁基于内存的、匿名文件系统的超级块。
  2. 初始化函数 (dma_buf_init):

    • 这是一个标准的内核模块初始化函数,通过subsys_initcall宏被注册,以确保它在系统启动的早期阶段被调用。
    • dma_buf_init_sysfs_statistics(): 初始化用于统计和监控DMA-BUF使用情况的sysfs接口。
    • kern_mount(&dma_buf_fs_type): 这是最关键的一步。kern_mount函数在内核内部创建一个指定文件系统类型的新挂载点,并返回一个vfsmount结构体指针(dma_buf_mnt)。这个挂载点是完全存在于内存中的,与任何物理存储设备无关。它为后续创建代表DMA缓冲区的文件(inode)提供了一个必要的命名空间和上下文。
    • dma_buf_init_debugfs(): 如果内核启用了debugfs,此函数会创建相应的调试接口,方便开发者查看DMA-BUF的内部状态。
  3. 退出函数 (dma_buf_deinit):

    • 这是一个标准的模块退出函数。它执行与初始化相反的操作:清理debugfs和sysfs接口,并调用kern_unmount来卸载之前创建的内部文件系统挂载点,释放所有相关资源。

代码分析

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
// dma_buf_fs_type: 定义了 "dmabuf" 这个伪文件系统的类型。
static struct file_system_type dma_buf_fs_type = {
.name = "dmabuf", // 文件系统的名字,内部使用。
.init_fs_context = dma_buf_fs_init_context, // 挂载时用于初始化上下文的函数。
.kill_sb = kill_anon_super, // 卸载时用于销毁超级块的通用函数。
};

// dma_buf_init: DMA-BUF子系统的初始化函数。
static int __init dma_buf_init(void)
{
int ret;

// 初始化用于DMA-BUF统计的sysfs接口。
ret = dma_buf_init_sysfs_statistics();
if (ret)
return ret;

// 在内核内部挂载 "dmabuf" 文件系统。
// 这是整个框架的基础,为DMA缓冲区文件提供了一个挂载点。
dma_buf_mnt = kern_mount(&dma_buf_fs_type);
if (IS_ERR(dma_buf_mnt))
return PTR_ERR(dma_buf_mnt); // 如果挂载失败,返回错误。

// 初始化用于DMA-BUF调试的debugfs接口。
dma_buf_init_debugfs();
return 0;
}
// 将 dma_buf_init 注册为子系统初始化调用,确保在早期启动阶段执行。
subsys_initcall(dma_buf_init);

// dma_buf_deinit: DMA-BUF子系统的退出函数。
static void __exit dma_buf_deinit(void)
{
// 卸载并清理debugfs接口。
dma_buf_uninit_debugfs();
// 卸载在内核内部创建的文件系统挂载点。
kern_unmount(dma_buf_mnt);
// 卸载并清理sysfs统计接口。
dma_buf_uninit_sysfs_statistics();
}
// 将 dma_buf_deinit 注册为模块退出函数。
__exitcall(dma_buf_deinit);