3. DRM 基础

在把 DRM 的 DMA-BUF 导出到 V4L2 设备之前,必须先弄清楚 DRM 子系统里“谁在管理显存、谁在暴露句柄、谁在跨设备传递”。这一节围绕四个关键词展开:设备节点、GEM 对象、PRIME 机制、文件描述符传递。它们构成了后续实战的底层语义基础。

一、设备节点:用户态进入 DRM 的门

DRM 驱动加载后,会在 /dev/dri/ 下创建字符设备节点。最常见的是 card0 与 renderD128。前者是“显示控制节点”,支持 KMS 模式设置、帧缓冲管理;后者是“渲染节点”,只做 GPU 渲染与显存分配,不涉及显示权限。做 DMA-BUF 导出时,通常使用 render 节点,因为它不需要 master 权限,更适合无显示场景的服务器或容器。

ls -l /dev/dri/
crw-rw---- 1 root video 226,   0 card0
crw-rw---- 1 root video 226, 128 renderD128

用户态通过 open("/dev/dri/renderD128", O_RDWR) 拿到 fd,后续所有 ioctl 都基于这个 fd。注意:fd 是进程内的文件描述符,而 DMA-BUF 传递的是“内核对象引用”,两者不是一回事,这是后面最容易混淆的点。

二、GEM 对象:显存分配的抽象

GEM(Graphics Execution Manager)是 DRM 对显存缓冲区的抽象。一次 DRM_IOCTL_MODE_CREATE_DUMB 或驱动私有的分配 ioctl,会在内核里创建一个 GEM 对象,并返回一个 handle。这个 handle 是“设备内句柄”,只在当前 DRM fd 的上下文里有效,不能直接给别的进程或别的设备用。

GEM 对象通常包含:尺寸、pitch、格式、缓存属性、以及底层 scatter-gather 表或物理页。它解决的是“显存怎么分配、怎么映射到 CPU/GPU”的问题,但不解决“怎么跨设备共享”。跨设备共享,需要 PRIME。

关键区分:GEM handle 是设备内句柄;DMA-BUF fd 是跨设备、跨进程的通用句柄。前者不能直接传给 V4L2,后者才可以。

三、PRIME 机制:把 GEM 变成 DMA-BUF

PRIME 是 DRM 的跨设备共享框架。它提供两个方向的操作:

  • 导出:DRM_IOCTL_PRIME_HANDLE_TO_FD,把 GEM handle 转成一个 DMA-BUF 文件描述符。
  • 导入:DRM_IOCTL_PRIME_FD_TO_HANDLE,把外部 DMA-BUF fd 转成本设备的 GEM handle。

导出时,内核会为该 GEM 对象创建 dma_buf 结构,并注册 dma_buf_ops,其中最关键的是 map_dma_buf、unmap_dma_buf、begin_cpu_access、end_cpu_access。V4L2 设备导入这个 fd 后,会调用这些回调来拿到 scatter-gather 表,从而让摄像头、编码器或显示控制器直接访问同一块内存。

struct drm_prime_handle args = {
    .handle = gem_handle,
    .flags  = DRM_CLOEXEC | DRM_RDWR,
    .fd     = -1,
};
ioctl(drm_fd, DRM_IOCTL_PRIME_HANDLE_TO_FD, &args);
int dmabuf_fd = args.fd;   // 可传给 V4L2

四、文件描述符传递:跨进程共享的正确姿势

DMA-BUF fd 是“进程内”的整数,直接通过命令行参数或环境变量传给另一个进程是无效的。跨进程必须使用 Unix domain socket 的 SCM_RIGHTS 辅助消息,把 fd 复制到目标进程。目标进程收到后,得到一个新的 fd,指向同一个内核 dma_buf 对象。

典型流程如下:

  1. 生产者进程打开 DRM render 节点,分配 GEM,导出为 DMA-BUF fd。
  2. 通过 sendmsg + SCM_RIGHTS 把 fd 发给消费者进程。
  3. 消费者进程用 recvmsg 收到 fd,再通过 V4L2 的 VIDIOC_QBUF 或 VIDIOC_EXPBUF 相关接口导入。
  4. 双方通过 fence 或 DMA_BUF_IOCTL_SYNC 做同步,避免读写冲突。
常见误区:把 DMA-BUF fd 当成普通整数写入配置文件或日志,然后在另一个进程里 open 它。这是错误的,fd 只在创建它的进程内有效,必须走 SCM_RIGHTS 传递。

五、四者关系一览

概念作用域典型标识能否跨设备
设备节点进程与内核/dev/dri/renderD128否,需 open
GEM 对象单个 DRM 设备handle (uint32)否
PRIME / DMA-BUF内核全局dma_buf fd是
SCM_RIGHTS进程间socket 辅助消息是,跨进程

六、案例场景:从 DRM 到 V4L2 的完整链路

假设我们要把 GPU 渲染好的一帧画面送给 V4L2 编码器。第一步,在 DRM 侧创建 GEM 对象并写入像素;第二步,调用 DRM_IOCTL_PRIME_HANDLE_TO_FD 得到 DMA-BUF fd;第三步,如果编码器在同一进程,直接把 fd 传给 V4L2 的 VIDIOC_QBUF;如果编码器在另一个进程,则通过 Unix socket 把 fd 发过去;第四步,V4L2 驱动导入 fd,建立自己的 vb2 buffer,并调用 dma_buf_attach 与 dma_buf_map_attachment 拿到 sg_table;第五步,硬件开始读取这块内存,完成后通过 fence 通知 DRM 侧可以复用。

这条链路里,任何一个环节的 fd 生命周期管理出错,都会导致内存泄漏或 use-after-free。因此,导出后要明确谁负责 close(fd),导入后要明确谁负责释放 buffer。

七、复盘与检查清单

  • 是否使用了 render 节点而非 card 节点,避免权限问题?
  • GEM handle 是否只在当前 DRM fd 内使用,没有跨设备传递?
  • 导出 DMA-BUF 后,是否记录了 fd 的归属和关闭责任?
  • 跨进程传递是否使用了 SCM_RIGHTS,而不是直接传整数?
  • 导入方是否在释放时调用了 dma_buf_detach 与 dma_buf_put?
  • 是否处理了 CPU 访问与设备访问之间的 cache 同步?

理解这四层抽象之后,后续章节中“把 DRM 的 DMA-BUF 导出到 V4L2 设备”就不再是黑盒操作,而是一条可以逐段调试、逐段验证的数据通路。

DRM 设备节点 → GEM → PRIME → DMA-BUF fd → V4L2 导入 进程 A(DRM 生产者) open /dev/dri/renderD128 GEM 分配 → handle PRIME_HANDLE_TO_FD 内核 DRM 子系统 GEM 对象(显存) dma_buf 结构 + ops sg_table / 物理页 进程 B(V4L2 消费者) recvmsg + SCM_RIGHTS VIDIOC_QBUF 导入 dma_buf_attach / map ioctl fd 跨进程 fd 传递:Unix Domain Socket + SCM_RIGHTS sendmsg(fd) → 内核复制 dma_buf 引用 → recvmsg 得到新 fd 关键约束 GEM handle 仅设备内有效;DMA-BUF fd 仅进程内有效;跨进程必须 SCM_RIGHTS 导入方负责 detach / put,导出方负责 close,避免泄漏与 use-after-free