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。
三、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 对象。
典型流程如下:
- 生产者进程打开 DRM render 节点,分配 GEM,导出为 DMA-BUF fd。
- 通过
sendmsg+SCM_RIGHTS把 fd 发给消费者进程。 - 消费者进程用
recvmsg收到 fd,再通过 V4L2 的VIDIOC_QBUF或VIDIOC_EXPBUF相关接口导入。 - 双方通过 fence 或
DMA_BUF_IOCTL_SYNC做同步,避免读写冲突。
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 设备”就不再是黑盒操作,而是一条可以逐段调试、逐段验证的数据通路。