用户空间中的 DMA-BUF

本文假设你对 unix 文件、进程及内存地址概念有基本了解

TLDR

DMA-BUF 是用户进程以文件(FD)形式从内核引用并可以传递给其他进程或内核组件的、不一定位于内存(RAM)上的存储区域。

其主要被用于在用户空间零复制(Zero-copy)地向某一硬件传递引用自相同或另一硬件并可被两硬件直接访问的存储区域,如将 Vulkan 存储引用的显存区域以 DMA-BUF 导出并导入为 EGL/OpenGL 存储。

前言

如果你经常关注 Linux 新闻或开源项目动态,你或许曾经不止一次在如下门类中瞥见过“DMA-BUF”。

  • V4L2, DRM
  • Mesa
  • Vulkan
  • Wayland
  • GNOME, KDE

这些门类往往相连相交,而连接它们的是图形/图像的渲染、显示又或是接收。而 DMA-BUF 在其中最大的用处即是以更高的性能进行图像数据的共享、传递,这就涉及到了零复制的概念。

零复制

引自维基百科零复制条目

零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。

设想现在有 A、B 两者者,

  • A 根据请求生成图像数据
  • B 根据请求显示图像
  • 需要将 A 产生的图像传递给 B

在图像传输的情景中,带宽动辄 Gbit/s,比如传输 1920x1080 32bit(RGBA) 60hz 的未压缩视频数据,带宽将达到近 475 MB/s。若是直接使用 CPU 对 A 的图像数据进行复制(memcpy)并传递给 B,即使对于现代桌面 CPU 这也是一个资源大户。更别说日益成为主流的 4K 分辨率(2160p)所需的 4 倍带宽了。

memfd

若 A、B 同属一进程,即它们共享内存地址空间,其实只需将 A 的图像数据地址传递给 B 并保证 A、B 不同时使用此数据,与其复制不如共享同一块存储区域。

但如果 A、B 是两个不同的进程呢?这就需要文件作为中介。

memfd 是由内核创建的以内存为后端的匿名文件(FD),并可以使用 mmap 映射到当前进程的虚拟地址空间来对文件进行读写。因此通过在 A、B 之间传递引用图像数据的 memfd 即可实现数据零复制共享。

FD 可通过 socket 的 SCM_RIGHTS 消息在进程间传递或使用 pidfd_getfd 间接传递,也可以使用 D-Bus 等 IPC 信道,网上已有足够文档故本文不再赘述

示例

如下为简化版的 memfd 传递示例。

exporter 创建 memfd 并打印 PID 和 FD 信息以供其他程序使用

#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

#define LEN 1024

int main(void) {
  int fd = memfd_create("image", 0);
  ftruncate(fd, LEN);

  void *data = mmap(NULL, LEN, PROT_WRITE, MAP_SHARED, fd, 0);
  // 写入字符串数据
  sprintf(data, "Hello World!\n");

  printf("/proc/%d/fd/%d\n", getpid(), fd);
  pause();
  return 0;
}

importer 使用 Linux 内核提供的 /proc/[PID]/fd/[FD] 打开其他进程的 FD

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

#define LEN 1024

int main(int argc, char *argv[]) {
  int fd = open(argv[1], O_RDWR);

  void *data = mmap(NULL, LEN, PROT_READ, MAP_SHARED, fd, 0);
  // 输出字符串数据
  printf("%s", (char *)data);
  return 0;
}

执行 exporter,其打印 fd 文件路径,可用 Ctrl + C 结束程序

$ gcc -o exporter exporter.c && ./exporter
/proc/123456/fd/3

以 exporter 输出的 fd 路径为参数执行 importer,成功打印出 exporter 之前写入 memfd 的内容

$ gcc -o importer importer.c
$ ./importer /proc/123456/fd/3
Hello World!

DMA-BUF FD

在用户空间,DMA-BUF 的形式就是 FD,故而它和 memfd 有相似的属性,它也可以和其他 FD 一样被传递,取决于实现也可能执行 mmap 操作。

不过有一点最大的不同就是 DMA-BUF 不能被用户创建,它是内核模块对存储区域的抽象引用,而你只能通过内核模块提供的 ioctl 接口或文件接口等导出、导入 DMA-BUF FD。

通常只有 DRM 模块、V4L2 模块等图形相关内核模块会提供 DMA-BUF 导出或导入接口以满足高性能零复制传递图像数据需求。用户可将模块 A 导出的 DMA-BUF FD 导入至模块 B 或是模块 A 自身,用户在此过程中扮演的角色只是路由。

而对于 DMA-BUF 在内核空间的存储后端和传递则是对用户是不透明的,它的存储可以位于显存、可以位于内存、也可以位于硬件独有存储,数据的传递也不一定通过 DMA 而可以通过 CPU。

图形 API 中的 DMA-BUF

伴随着 Wayland 对在用户空间共享屏幕或窗口内容的需求,DMA-BUF 在桌面端软件的应用也逐渐展开。而这其中的底层,也是 DMA-BUF 的最终来源和去向,就是基于图形驱动的用户空间的图形 API。

其实本文的出发点正是我之前写的 pw-capture,一个使用 Vulkan API 和 EGL/GLX API 导出 DMA-BUF FD 图像流并传递给其他程序(PipeWire)的图像截取层。

对于一张 Vulkan 图片 VkImage,它会有宽、高、层数、像素格式等属性,而它又会链接 VkMemory 作为存储。这其中的存储 VkMemory 即可选地能被导出为 DMA-BUF FD,又或是从 DMA-BUF FD 导入为 VkMemory 从而作为 VkImage 的存储。而 OpenGL 与之类似,只不过是从 Texture 导出导入且需要如 EGL 之类的中间层中介。

进而即可利用 DMA-BUF FD 可分享的属性实现图像存储的跨进程、跨 API 的传输与共享。

DRM format modifier

在 Vulkan 和 OpenGL 等图形应用中,你通常不会使用内核提供接口而是厂商实现的图形 API 进行 DMA-BUF 操作,故除了纯粹的导出、导入外,你还会获取或需要提供如像素格式、尺寸等图形相关的元信息。

而 DRM format modifier 是更底层的描述 DMA-BUF 数据的像素结构的 64-bit 整数描述符。

对于一张图像像素在内存中的排列,最自然的有线性排列,即按顺序排放从第一行到最后一行的数据,数据可按 x + y * 列宽 寻址。这种线性排列就可以用 DRM_FORMAT_MOD_LINEAR0x0)描述。

而如 Intel、AMD 等厂商通常会有私有定义的或许性能更佳的像素排列,这时他们会选择一个新的 64-bit 整数作为此排列的描述符以区分其他排列。

而其他私有但不需要区分的排列通常会使用 DRM_FORMAT_MOD_INVALID 描述。

通常只有 DRM_FORMAT_MOD_LINEAR 格式描述的数据可以被用户直接解析使用,而其他格式的数据只能被用户中转导出导入。

Vulkan

在 Vulkan 中,如果驱动实现支持 VK_EXT_external_memory_dma_buf 扩展,则用户可以利用它导入和导出 DMA-BUF FD。

而利用 VK_EXT_image_drm_format_modifier 则可进行对特定图像格式所支持的 DRM format modifier 进行查询,以便在导入 DMA-BUF FD 时使用正确格式。

OpenGL

OpenGL 是平台无关的,但 OpenGL 上下文的创建是平台相关的,这就需要中间层的介入以创建上下文、窗口、实现图像存储后端,对于 X11 有 GLX,对于 Wayland 有 EGL,对于 Windows 有 WGL。

其中 EGL 类似 Vulkan 以扩展的方式提供 X11、Wayland 等平台的支持。

而 EGL 提供了 EGL_EXT_image_dma_buf_importEGL_EXT_image_dma_buf_export 扩展以供驱动实现 DMA-BUF FD 导入导出支持。

DMA-BUF 的利用情况

桌面环境

在 KDE、GNOME 等 Wayland 桌面实现中,DMA-BUF 被用于屏幕/窗口共享,其中最通行的即是桌面环境实现 XDG Desktop Portal,而用户程序调用其中的屏幕共享接口 org.freedesktop.portal.ScreenCast 以获取屏幕/窗口数据的 DMA-BUF FD。

PipeWire

PipeWire 是视频和音频数据的交换中心,其中的视频数据就支持依赖 DRM format modifier 的 DMA-BUF 共享机制。

XDG Desktop Portal 的屏幕共享接口和摄像头接口其实就是以 PipeWire 为后端的。

VA-API

VA-API 是硬件视频编解码加速 API,它可选的支持使用 DMA-BUF 作为编码输入。故而对于屏幕录制可以先将屏幕导出为 DMA-BUF FD 再导入编码以减少传输损失。

GStreamer

GStreamer 支持 DMA-BUF 作为视频存储格式(video/x-raw(memory:DMABuf)),故只要插件支持 DMABuf 就可以使用 DMA-BUF。比如使用 pipewiresrcglimagesink 实现 EGL/OpenGL 零复制显示 PipeWire 中支持 DMA-BUF 的视频源,又或是配合 VA-API 插件进行高性能录制编码。

总结

DMA-BUF 已然是用户空间 Linux 图像共享的通行券,现在的、可见未来的图像共享应用都应该且需要使用它。