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:停止采集
完整的调用流程是这样的:
- open 设备节点
- VIDIOC_QUERYCAP 检查能力
- VIDIOC_ENUM_FMT 枚举格式
- VIDIOC_S_FMT 设置格式
- VIDIOC_REQBUFS 申请缓冲区
- mmap 或 userptr 映射缓冲区
- VIDIOC_QBUF 放入空缓冲区
- VIDIOC_STREAMON 开始采集
- 循环:VIDIOC_DQBUF -> 处理数据 -> VIDIOC_QBUF
- VIDIOC_STREAMON 停止
- munmap 解除映射
- 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。到时候你会看到,缓冲区管理的好坏直接决定编码器的吞吐量。