2、Linux V4L2框架详解:V4L2架构、设备节点、ioctl调用流程、缓冲区管理

好,咱们进入正题。V4L2,全称 Video for Linux 2,是 Linux 下视频设备驱动的标准框架。说白了,它就是操作系统和摄像头硬件之间的翻译官。你写的应用程序,通过 V4L2 的接口去操作摄像头,不用关心底层是 USB 摄像头还是 MIPI 接口的 sensor。

我最早接触 V4L2 是在做一款工业相机的时候。那时候不懂,直接去操作 /dev/video0 的读写,结果画面全是花屏。后来才明白,V4L2 有一套完整的流程要走,少一步都不行。

2.1 V4L2 整体架构

V4L2 的架构分三层:

  • 应用层:你写的采集程序,调用 open、ioctl、mmap 等系统调用。
  • V4L2 核心层:内核中的 video4linux 模块,负责统一管理设备节点和 ioctl 分发。
  • 驱动层:具体的摄像头驱动,比如 uvcvideo(USB 摄像头)、vivid(虚拟测试驱动)。

你想想看,应用层调用 ioctl,内核根据命令码找到对应的驱动回调函数。这个机制有点像 C++ 的虚函数表,只不过是在内核里实现的。

核心要点:V4L2 的设备节点是 /dev/videoX,其中 X 从 0 开始。一个摄像头通常对应一个 video 节点,但有些复杂的设备可能有多个节点(比如 video 和 vbi)。

2.2 设备节点与主设备号

V4L2 设备的主设备号是 81。你可以用 ls -l /dev/video* 查看:

crw-rw---- 1 root video 81, 0 /dev/video0
crw-rw---- 1 root video 81, 1 /dev/video1

次设备号从 0 开始递增。嗯,这里要注意:不是所有的 /dev/videoX 都是摄像头。有些是视频输出设备(比如显示输出),有些是 metadata 设备。怎么区分?用 v4l2-ctl 工具:

v4l2-ctl -d /dev/video0 --all

看输出的 Capabilities 字段。如果是 V4L2_CAP_VIDEO_CAPTURE,那就是采集设备。我遇到过有人把 video 节点当成摄像头,结果调了半天发现是个 TV 调谐器,哭笑不得。

2.3 ioctl 调用流程

V4L2 的核心操作就是 ioctl。每个 ioctl 调用都对应一个命令码,比如:

  • VIDIOC_QUERYCAP:查询设备能力
  • VIDIOC_ENUM_FMT:枚举支持的格式
  • VIDIOC_S_FMT:设置采集格式
  • VIDIOC_REQBUFS:申请缓冲区
  • VIDIOC_QBUF:将缓冲区放入队列
  • VIDIOC_DQBUF:从队列取出已填满的缓冲区
  • VIDIOC_STREAMON:开始采集
  • VIDIOC_STREAMOFF:停止采集

完整的调用流程是这样的:

  1. open 设备节点
  2. VIDIOC_QUERYCAP 检查能力
  3. VIDIOC_ENUM_FMT 枚举格式
  4. VIDIOC_S_FMT 设置格式
  5. VIDIOC_REQBUFS 申请缓冲区
  6. mmap 或 userptr 映射缓冲区
  7. VIDIOC_QBUF 放入空缓冲区
  8. VIDIOC_STREAMON 开始采集
  9. 循环:VIDIOC_DQBUF -> 处理数据 -> VIDIOC_QBUF
  10. VIDIOC_STREAMON 停止
  11. munmap 解除映射
  12. close 关闭设备

我刚开始写的时候,经常忘记 VIDIOC_QBUF 这一步。结果 DQBUF 一直阻塞,还以为摄像头坏了。后来养成习惯,每次申请完缓冲区先全部 QBUF 进去,再开流。

个人经验:ioctl 调用失败时,一定要检查 errno。常见的错误有 EINVAL(参数无效)、ENOMEM(内存不足)、EBUSY(设备忙)。我习惯在每次 ioctl 后加个 perror 或者打印日志,调试起来快很多。

2.4 缓冲区管理:三种模式

V4L2 支持三种缓冲区管理方式。我一个个说。

2.4.1 mmap 模式

这是最常用的方式。驱动在内核空间分配物理连续的内存,应用通过 mmap 映射到用户空间。说白了,就是内核把数据直接放到一块内存里,你通过指针去读。

struct v4l2_requestbuffers req = {0};
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_REQBUFS, &req);

// 然后对每个 buffer 做 mmap
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(fd, VIDIOC_QUERYBUF, &buf);
void *addr = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);

优点:简单,性能好。缺点:缓冲区数量有限(通常 4~8 个),而且需要内核支持大块连续内存。

我曾经在嵌入式设备上遇到过 mmap 失败的情况。原因是内核的 CMA 区域被占满了。后来我减少了缓冲区数量,从 8 个降到 4 个,问题就解决了。

2.4.2 userptr 模式

这种模式反过来:应用自己分配内存,然后把用户空间的指针传给驱动。驱动通过 DMA 直接把数据写到你的内存里。

req.memory = V4L2_MEMORY_USERPTR;
ioctl(fd, VIDIOC_REQBUFS, &req);

// 自己分配内存
void *buffer = malloc(size);
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_USERPTR;
buf.m.userptr = (unsigned long)buffer;
buf.length = size;
ioctl(fd, VIDIOC_QBUF, &buf);

优点:缓冲区数量灵活,内存由应用管理。缺点:需要内存对齐(通常要求页对齐),而且 DMA 操作时不能换页。

避坑指南:我曾经用 userptr 时,分配的内存没有做页对齐,结果驱动报 EFAULT。后来用 posix_memalign 分配页对齐的内存才搞定。另外,userptr 的内存不能是栈上的,也不能是 malloc 后马上 free 的,否则 DMA 写到一半内存被释放了,系统直接崩溃。

2.4.3 dmabuf 模式

这是最现代的方式。dmabuf 允许你在不同设备之间共享 DMA 缓冲区,比如摄像头直接采集到 GPU 显存里,省去一次 CPU 拷贝。

req.memory = V4L2_MEMORY_DMABUF;
ioctl(fd, VIDIOC_REQBUFS, &req);

// 从其他驱动获取 dmabuf fd
int dmabuf_fd = get_dmabuf_from_gpu();
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_DMABUF;
buf.m.fd = dmabuf_fd;
ioctl(fd, VIDIOC_QBUF, &buf);

优点:零拷贝,性能极致。缺点:依赖驱动支持,而且需要其他设备(如 GPU、ISP)也支持 dmabuf。

我在做 AI 摄像头项目时用过 dmabuf。摄像头直接采集到 NPU 的内存里,NPU 做推理,全程没有 CPU 参与。那性能,啧啧,帧率直接翻倍。

2.5 三种模式对比

特性 mmap userptr dmabuf
内存分配者 内核 应用 其他驱动
拷贝次数 1 次(内核到用户) 0 次(直接 DMA) 0 次(跨设备共享)
缓冲区数量 有限(通常 4~8) 灵活 灵活
兼容性 最好 较好 依赖驱动
适用场景 通用采集 需要自定义内存管理 零拷贝流水线

我个人习惯:原型开发用 mmap,简单可靠。性能优化时切 userptr。如果整个流水线都是自己控制的(比如摄像头 + GPU + 编码器),那就上 dmabuf。

2.6 总结

V4L2 框架其实不复杂,就是一套固定的流程。你只要记住:先查能力,再设格式,然后申请缓冲区,最后循环采集。缓冲区管理是重点,三种模式各有优劣,根据场景选。

嗯,最后说一句:调试 V4L2 时,v4l2-ctl 和 ffplay 是你的好朋友。先用 v4l2-ctl 确认设备能正常工作,再写代码。我曾经花了两天调试一个采集程序,最后发现是摄像头硬件坏了,用 v4l2-ctl 一测就露馅了。

下一章我们讲实际编码,把采集到的原始数据压缩成 H.264/H.265。到时候你会看到,缓冲区管理的好坏直接决定编码器的吞吐量。