做视频采集相关开发时,判断一台摄像头能不能用好,很多时候不是看它标称多少万像素,而是要弄清楚驱动到底给了你哪几种缓冲区握手方式。项目标题里说的“检查摄像头是否支持 reqbuf.memory = V4L2_MEMORY_DMABUF”,实际上就是问驱动一句话:你认不认 dma-buf 这种内存模式?这个问题直接决定后面能不能做零拷贝流水线,能不能把相机的帧直接喂给硬编、ISP 或者 GPU,而不用在用户态和内核态之间反复搬运。
这类问题通常出现在两种场景里:一种是你拿到一块新的开发板或摄像头模组,要在上面调 AI 推理或者视频编码,需要尽量省掉内存拷贝;另一种是手头驱动是厂商闭源 BSP,你查不到 videobuf2 队列的初始化源码,只能从用户态反推它的能力。不管哪种场景,都有办法在几行代码之内判断出来,但也有很多容易误判的坑。这篇文章就把整套判断思路、最小验证代码、交叉验证手段和常见踩坑点整理一遍。
1. 为什么偏偏要查 DMABUF:三种内存模式的定位差异
1.1 MMAP、USERPTR、DMABUF 到底差在哪
V4L2 的缓冲区模式不是一个“配置项”,而是驱动和用户空间的数据交换契约。经典三种模式是 MMAP、USERPTR、DMABUF,它们在语义上差别很大。
- MMAP:驱动分配物理内存,用户态通过 mmap 得到虚拟地址。优点是实现简单,摄像头驱动普遍支持;缺点是从硬件过来的图像数据往往要先落到驱动 buffer,再被 CPU 或其它外设搬运一次,多一次拷贝。
- USERPTR:用户态自己分配内存,把地址传给驱动。听起来很灵活,但内核真正的 DMA 映射未必能落在你这段内存上,很多驱动要么做额外拷贝,要么干脆不支持,实际使用率并不高。
- DMABUF:用户态传进来的是一个 dma-buf 文件描述符,而不是普通地址。驱动通过这个 fd 拿到已经映射好的 DMA buffer。数据可以在多个设备之间直接流动,所以是零拷贝方案里最常被提起的一种。
日常最容易混淆的点是:DMABUF 不是一种“更高级的 mmap”,它改变了谁提供内存、谁做映射的模型。mmap 是驱动分配、用户来看;DMABUF 是用户提供一个已经存在的 dma-buf,驱动过来“借”这块内存干活。所以摄像头的驱动必须专门支持这种外部内存导入,否则 REQBUFS 就会明确拒绝。
1.2 支持 DMABUF 还有“进”和“出”两个方向
拿到摄像头之后,你还得区分“支持 DMABUF”到底是指哪个方向。V4L2_MEMORY_DMABUF 通常代表“导入”:用户态把外部的 dma-buf 文件描述符交给驱动,让摄像头把 DMA 数据写进这块外部内存。这在 ISP、GPU、硬件编解码器协作的管线里非常常见。
另一个方向是“导出”:摄像头自己分配 MMAP 缓冲区,然后用户态通过 VIDIOC_EXPBUF 把这些缓冲区变成 dma-buf fd,交给其它模块使用。很多回答说“摄像头支持 mmap 就能 zero-copy”,其实不准确,导出支持还要看驱动有没有实现 EXPBUF,并且底层 memops 支不支持 dma-buf 导出。
所以标题里问的reqbuf.memory = V4L2_MEMORY_DMABUF,是真真切切在探测“导入”这一条路径。如果你的使用场景是“外部硬件把内存塞给摄像头”,这个检测结果就是关键依据;如果你的场景是“摄像头采集完,把 buffer 直接给编码器”,那除了 REQBUFS 检测外,最好再补一个 VIDIOC_EXPBUF 的验证,别混为一谈。
1.3 你查的其实是驱动的 videobuf2 队列,不是传感器
有一个认知误区经常困扰新手:摄像头模组硬件明明支持 MIPI / CSI,甚至支持多路 DMA,为什么 REQBUFS 一换 DMABUF 就 EINVAL?原因在于这个问题本质上跟传感器芯片关系不大,它取决于驱动里 videobuf2 队列的 io_modes 配置。
Linux 多媒体驱动大多数基于 videobuf2 框架。驱动初始化队列时,会根据自家硬件的 DMA 能力设置 q->io_modes,哪个位打开,用户空间才能用哪个内存模式。比如内核驱动里有 VB2_MMAP、VB2_USERPTR、VB2_DMABUF 这些标志。摄像头硬件再先进,如果驱动只给它配置了 VB2_MMAP,用户在用户态怎么折腾都不可能协商出 DMABUF 模式。所以标题那句检查,实际是在验证驱动层 io_modes 是否包含 VB2_DMABUF 对应的能力。
2. 检查设备前的准备:别拿错误的节点和状态做试探
2.1 先找到真正的 capture 节点
很多主控芯片的媒体设备不是只有一个 /dev/video0,它可能拆成好几个子设备:一个主采集节点、一个 raw 传感器节点、一个 ISP 统计节点、一个视频输出节点。检查 DMABUF 支持前,先确认你选中的节点就是用来出图的 capture 节点。
- 用
ls /dev/video*只能看节点列表,看不出功能。 - 用
media-ctl --print-topology看媒体拓扑,能直观看出哪个 pad 是传感器输出、哪个是 ISP 输出。 - 用
v4l2-ctl --info -d /dev/video0看节点的driver、card、bus_info 和 capabilities。
把节点认错是最亏的。我见过有人在 output 节点上测 capture 配置,折腾半天 REQBUFS 一直报错,最后发现是拿错设备节点了。对于大多数带 ISP 的平台,真正能输出 YUV 或 RGB 图的通常是靠后那个 video 节点,而 raw sensor 节点一般只输出 Bayer。
2.2 用 QUERYCAP 看基本能力
在做 REQBUFS 之前,先执行一次 VIDIOC_QUERYCAP,确认节点支持 CAPTURE 和 STREAMING:
struct v4l2_capability cap; if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0) { printf("driver: %s\n", cap.driver); printf("card: %s\n", cap.card); printf("bus_info: %s\n", cap.bus_info); }QUERYCAP 返回的 capabilities 字段里有 V4L2_CAP_VIDEO_CAPTURE、V4L2_CAP_VIDEO_CAPTURE_MPLANE、V4L2_CAP_STREAMING 等标志。注意这里面没有“DMABUF”这个专属标志位,所以 QUERYCAP 只能帮你确认这个节点是不是个能 stream 的采集节点,不能代替 REQBUFS 探测。有些文档会说“看看设备支持哪些内存模式”,其实真正可靠的还是直接发起 REQBUFS 请求,以驱动的实际回复为准。
2.3 先释放旧缓冲区,再开始探测
如果这个节点已经被某个程序用 MMAP 模式申请过缓冲区,你再拿 DMABUF 去 REQBUFS,可能返回 EBUSY,而不是你以为的“不支持 DMABUF”。所以探测之前先做一次清理:用 count=0 把旧的缓冲区申请撤销。
struct v4l2_requestbuffers req; memset(&req, 0, sizeof(req)); req.count = 0; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; ioctl(fd, VIDIOC_REQBUFS, &req);这一步看起来多余,实际非常关键。很多驱动的 REQBUFS 实现是先判断当前队列状态,队列里还有未释放的 buffer 时,再传入另一种 memory 模式就可能直接复用旧路径或者干脆返回占用。把设备状态清干净,后面的结果才可信。
3. 核心判定:直接拿 V4L2_MEMORY_DMABUF 申请缓冲区
3.1 VIDIOC_REQBUFS 的协议细节
V4L2 的缓冲区内存模式协商,靠的是 VIDIOC_REQBUFS 这个 ioctl。它接收一个 v4l2_requestbuffers 结构体,用户态填好 count、type、memory 三个字段,然后交给驱动。结构体定义大致是:
struct v4l2_requestbuffers { __u32 count; __u32 type; __u32 memory; __u32 reserved[2]; };- count 是请求的缓冲区数量,传 0 表示释放已有缓冲区。
- type 是缓冲区类型,普通 capture 设备填 V4L2_BUF_TYPE_VIDEO_CAPTURE,多平面设备填 V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE。
- memory 是内存模式,填 V4L2_MEMORY_DMABUF 就是申请 DMA-buf 导入模式。
一次 REQBUFS 返回成功,说明驱动同意以这种内存模式建立缓冲区池,并在 count 字段回填实际可用的缓冲区个数。如果驱动不支持这种模式,通常返回 -1 并且 errno 为 EINVAL。这个 EINVAL 就是最直接的否定回答。
3.2 最小 C 探测程序
下面这段代码是我在实际调试时最喜欢用的最小版本,编译出来一个几十 KB 的小工具,拿到任何板子上都能快速试:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <errno.h> #include <unistd.h> #include <sys/ioctl.h> #include <linux/videodev2.h> int main(int argc, char *argv[]) { const char *dev = argc > 1 ? argv[1] : "/dev/video0"; int fd = open(dev, O_RDWR); if (fd < 0) { perror("open"); return 1; } struct v4l2_requestbuffers req; memset(&req, 0, sizeof(req)); // 先清理旧状态 req.count = 0; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; ioctl(fd, VIDIOC_REQBUFS, &req); // 再申请 DMABUF 模式 memset(&req, 0, sizeof(req)); req.count = 8; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_DMABUF; if (ioctl(fd, VIDIOC_REQBUFS, &req) == 0) { printf("DMABUF supported, get %u buffers\n", req.count); } else { if (errno == EINVAL) printf("DMABUF not supported (EINVAL)\n"); else if (errno == EBUSY) printf("device busy, release buffers first\n"); else perror("REQBUFS DMABUF"); } close(fd); return 0; }编译方式很简单:
gcc check_dmabuf.c -o check_dmabuf ./check_dmabuf /dev/video0如果设备是多平面模式,把 type 改成 V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE 再试一次。有些驱动对单平面和 multiplanar 的处理不一样,尤其是 ISP 相关设备,可能一个支持一个不支持。
3.3 用 Python 快速验证同一件事
C 程序适合放到板子上编译,但如果只是想在开发机上快速判断,Python 的 ctypes 也可以做到类似效果,省去交叉编译的麻烦。核心是构造 v4l2_requestbuffers 结构体,直接发起 ioctl:
#!/usr/bin/env python3 import os import sys import errno import ctypes from fcntl import ioctl VIDIOC_REQBUFS = 0x5608 V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9 V4L2_MEMORY_MMAP = 1 V4L2_MEMORY_DMABUF = 4 class V4l2Requestbuffers(ctypes.Structure): _fields_ = [ ("count", ctypes.c_uint32), ("type", ctypes.c_uint32), ("memory", ctypes.c_uint32), ("reserved", ctypes.c_uint32 * 2), ] def probe(dev, buf_type): fd = os.open(dev, os.O_RDWR) try: req = V4l2Requestbuffers() req.count = 0 req.type = buf_type req.memory = V4L2_MEMORY_MMAP ioctl(fd, VIDIOC_REQBUFS, req) req = V4l2Requestbuffers() req.count = 1 req.type = buf_type req.memory = V4L2_MEMORY_DMABUF try: ioctl(fd, VIDIOC_REQBUFS, req) print(f"{dev}: DMABUF supported, count={req.count}") return True except OSError as e: if e.errno == errno.EINVAL: print(f"{dev}: DMABUF NOT supported (EINVAL)") else: print(f"{dev}: other error {e.errno}: {os.strerror(e.errno)}") return False finally: os.close(fd) if __name__ == "__main__": dev = sys.argv[1] if len(sys.argv) > 1 else "/dev/video0" probe(dev, V4L2_BUF_TYPE_VIDEO_CAPTURE)运行:
python3 check_dmabuf.py /dev/video0这个脚本适合快速巡检一大批设备节点。之前我在一个转接方案的调试里,把 /dev/video0 到 video5 全部跑了一遍,只看输出就知道哪个节点能导入 DMABUF、哪个不能,比反复看驱动文档高效很多。
3.4 返回值的真正含义
探测结果不是只有“支持/不支持”两种,还得结合 errno 判断实际情况:
| 返回值 / errno | 含义 | 下一步 |
|---|---|---|
| 返回 0 | 驱动接受 DMABUF 模式申请,count 回填实际数量 | 继续做 QBUF/STREAMON 全链路验证 |
| EINVAL | 驱动根本不认识这种内存模式,或该队列未开启 DMABUF | 基本可以认为不支持 |
| EBUSY | 设备正忙,可能有其它进程占用或者还有未释放 buffer | 释放缓冲区、关闭其它占用程序 |
| ENOMEM | 驱动认识 DMABUF,但分配不出足够缓冲区 | 调低 count,或检查内存/dma 约束 |
| EPERM | 权限不足 | 用 root 权限重试 |
有一个细节容易被忽略:有些驱动在 REQBUFS 阶段其实没有完整验证底层的 dma-buf 支持,它只是把 memory 字段记了下来,等到用户真正用 VIDIOC_QBUF 提交一个 DMABUF fd 时才做检查。所以 REQBUFS 成功只能说明“模式被认可”,不等于“任意 dma-buf fd 都能直接用”。想要百分之百确认,必须接着做一条真的采集流程,提交合法 fd,验证 STREAMON 是否正常。
4. 交叉验证:从驱动层和工具链多方面确认
4.1 用 v4l2-compliance 做完整测试
用户态手写 REQBUFS 探测属于快速定性,想更全面、更正式一点,直接用 v4l-utils 里的 v4l2-compliance。它能对设备做一整套媒体设备能力测试,覆盖各种 buffer 模式、格式配置和 streaming 流程。
v4l2-compliance -d /dev/video0 -s-s 参数会进入 streaming 测试,工具内部会尝试多种内存模式采集帧。输出里如果出现类似 “DMABUF ... OK” 的字样,说明驱动在更完整的路径上也支持;如果出现 “DMABUF ... FAILED” 或者直接跳过,那就说明这条链路有问题。
我习惯把 v4l2-compliance 当作第二道判断门禁:REQBUFS 探测给出初步结论,compliance 的详细报告给最终结论。在量产板子上,“快速探测通过但 compliance 失败”的情况出现过不止一次,原因一般是驱动对格式、帧率或 buffer 对齐有隐藏限制。
4.2 查驱动 io_modes 和 debugfs 信息
如果你手上有驱动源码,判断就变成查配置了。videobuf2 队列初始化时,驱动会设置 io_modes,比如:
q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;看到 VB2_DMABUF 这个位,说明实现层已经登记了 DMABUF 支持。但要注意,这只是驱动“声明”支持,最终还要看它的 buf_ops 和 memops 是不是真的实现了相关回调。比如有的平台为了兼容旧代码,io_modes 配了 VB2_DMABUF,但底层用了一个不支持外部 fd 的分配器,真正跑起来还是会失败。
没有源码的时候,可以看运行期的系统信息。内核的 videobuf2 在 debugfs 里往往会暴露队列状态,路径一般在类似 /sys/kernel/debug 下。查一查设备绑定的驱动名,再结合 dmesg 里的 videobuf2 相关日志,有时能找到缓冲区分配失败的记录。这些信息不一定每个平台都有,但值得尝试,尤其是厂商驱动没给文档的情况下,日志经常是唯一线索。
4.3 用真实流验证 DMABUF 是否真的能跑通
光有 REQBUFS 还不够,我会把“真实流验证”作为最终裁决手段。最简单的做法是用 GStreamer 跑一条管道,强制 v4l2 源使用 DMABUF 相关模式,然后收一帧数据。如果你的开发环境里不方便用 GStreamer,也可以用 C 程序走完整流程:REQBUFS 之后创建 dma-buf fd,再调用 VIDIOC_QBUF 把 fd 提交进去,最后 STREAMON 采集一帧。
完整的 C 流程会比较长,但有几件事必须确认:
- dma-buf fd 必须由一个已经建立的 exporter 提供,比如另一个同样支持 dma-buf 导出的设备,或者用 udmabuf / 特定分配器创建。
- QBUF 时要把 fd 填到对应 plane 的 m.fd 字段上,而不是填 offset。
- 多平面设备要给每个 plane 分别设置正确的 fd、长度和字节偏移。
- STREAMON 后确认 DQBUF 正常返回,并且帧数据真的被填充到了外部 dma-buf 里。
真实流跑通,才算真正把“支持 DMABUF”这句话坐实了。只做 REQBUFS 测试,最多算“模式协商通过”。
5. 常见问题与排查实录
5.1 REQBUFS 成功但流建立失败的问题
我踩过最典型的一个坑是某些厂商驱动对 DMABUF 支持“半吊子”。REQBUFS 返回成功,用户态满心欢喜开始推流,结果 STREAMON 一执行就报错,或者来的帧全是花屏、空数据。
原因多半是驱动虽然接受了 DMABUF 模式,但底层 DMA 引擎只支持某一种特定 alignment,或者对 buffer 的物理连续性有要求。用户传进来的通用 dma-buf 不符合硬件约束,流自然起不来。
这时候不要只盯着 REQBUFS 的返回值,要看 dmesg 有没有“unsupported buffer”“sg table”之类的报错。另外可以对比一下同样数量下 MMAP 模式能不能正常走流:MMAP 能跑而 DMABUF 跑不起来,基本可以判断是硬件或驱动对导入 buffer 有额外限制,而不是你的调用写法问题。
5.2 单平面与 multiplanar 的 mode 区分
新手很容易在这里栽跟头。普通 USB 摄像头多半是单平面 V4L2_BUF_TYPE_VIDEO_CAPTURE,而 ISP、Raw sensor、以及某些硬件编解码器节点,常用 V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE。
DMABUF 对这两种类型的支持状态不一定相同。一个节点用单平面类型申请 DMABUF 被拒,不代表它不支持零拷贝;也要试一次 multiplanar。反过来也一样。所以探测脚本里最好把两种 type 都跑一遍,记录成表格再判断,避免漏掉真正支持的模式。
5.3 权限与占用问题
/dev/video0 一般在 video 组下,默认权限未必能让你打开。REQBUFS 返回 EPERM 时,先用 ls -l /dev/video* 看权限,再用 root 重试。还有一种隐蔽情况:某个预览程序虽然没有长时间占用节点,但启动时申请过 buffer 忘了释放,导致你的探测遇到 EBUSY。写探测脚本时加上“先 count=0 清一次”的操作就能减少这种干扰。
5.4 驱动版本和固件行为影响
同一个摄像头方案,BSP 版本不同,DMABUF 支持也可能不同。厂商在新驱动里合入了 VB2_DMABUF,旧版本没合入,这是行业里非常常见的事情。所以检验结果是“只对当前固件有效”的。换一套固件或换一版内核,结论都要重新验证,不能一个测试结果吃一辈子。
我之前调试某块主控平台时,就是被旧 BSP 坑了一次,sensor 硬件本身没有任何问题,就是驱动没开 io_modes 里的 DMABUF 位。后来升了 BSP,同一套应用代码一行不用改,REQBUFS 就顺利通过了。这个经历让我养成了习惯:凡是涉及 buffer 模式的问题,先确认驱动版本和内核版本,再谈解决方案。
5.5 判断结果如何落进自己的工程
如果检测结论是“支持 DMABUF”,管线的设计空间就大了。硬件可以把帧直接导入到预设的 dma-buf,编码器或 GPU 在同一块内存上工作,避免多次拷贝。
如果结论是“不支持”,也不必绝望。办法有两个:一是让摄像头以 MMAP 模式申请 buffer,再用 VIDIOC_EXPBUF 导出 dma-buf fd,绕开导入路径实现零拷贝;二是中间引入一个专门做 buffer 转换的模块,牺牲一点拷贝延迟换取兼容性。第一种思路更常见,因为很多所谓“支持 DMABUF 的摄像头”其实指的是导出能力,而不是导入能力,先探测导出这条路往往比硬件升级省成本得多。
还有一个工程上的小建议:把本章的探测代码封装成一个带退出码的小工具,集成进你们的自动化测试脚本里。每次刷完固件或者换完内核,跑一遍全套 video 节点的支持矩阵。这样一旦某个节点行为变了,第一时间就能发现,比出了问题再抓包分析高效得多。
我个人在实际操作里的体会是,判断 DMABUF 支持这件事,最忌讳的就是只信文档、不看驱动实际行为。REQBUFS 几行代码的探测成本极低,却能把“看起来支持”和“真正支持”区分开。很多看似玄学的视频链路卡顿,最后溯源发现都是内存模式协商错了位置。希望这篇文章能帮你少走一段弯路,至少拿到新板子、新摄像头时,能用一个快速、明确的探测流程先把底摸清楚。