2. Linux内核V4L2框架
V4L2,全称Video for Linux 2,是Linux内核中视频设备驱动的标准框架。说白了,它就是摄像头驱动和应用程序之间的“翻译官”。你写一个摄像头驱动,如果不按V4L2的规矩来,上层应用根本没法用。我最早接触V4L2时也觉得结构体太多、流程绕,但摸清楚三个核心数据结构、ioctl调用路径和Buffer管理机制后,整个框架就清晰了。
2.1 V4L2核心数据结构
V4L2框架围绕三个核心结构体展开:video_device、v4l2_device、v4l2_subdev。它们的关系,我习惯用一个比喻来理解:
- v4l2_device —— 整个摄像头设备的“根”,相当于一个总控制器。
- video_device —— 暴露给用户空间的“节点”,就是/dev/videoX。
- v4l2_subdev —— 内部子模块,比如传感器、ISP、闪光灯,每个子设备一个。
嗯,这里要注意:一个物理摄像头可能包含多个子设备。我在项目中遇到过,一个模组里传感器和自动对焦马达各是一个subdev,但对外只暴露一个video_device节点。
2.1.1 video_device
video_device是用户空间直接打交道的对象。它注册后会生成/dev/videoX节点。核心成员包括:
struct video_device {
const struct v4l2_file_operations *fops; // 文件操作函数集
struct v4l2_device *v4l2_dev; // 指向所属的v4l2_device
struct device *dev; // 内核设备结构体
u32 index; // 设备节点编号
int minor; // 次设备号
// ... 其他成员
};
我个人习惯,在注册video_device前,一定要设置好v4l2_file_operations。这个结构体定义了open、close、ioctl、mmap等操作。你想想看,如果ioctl没实现,应用层调用VIDIOC_QBUF就会直接返回-ENOTTY,调试起来很头疼。
2.1.2 v4l2_device
v4l2_device是顶层容器。一个物理摄像头设备对应一个v4l2_device实例。它管理所有子设备,并提供与内核设备模型的接口。
struct v4l2_device {
struct device *dev; // 父设备
struct list_head subdevs; // 子设备链表
spinlock_t lock; // 自旋锁
char name[V4L2_DEVICE_NAME_SIZE]; // 设备名称
// ...
};
我记得第一次写驱动时,忘了初始化v4l2_device的锁,结果在多线程访问时出了竞态问题。从那以后,我每次都会检查:spin_lock_init(&v4l2_dev->lock); 这步不能省。
2.1.3 v4l2_subdev
v4l2_subdev代表一个内部子模块。比如一个摄像头模组,传感器是一个subdev,镜头驱动是另一个subdev。它们通过v4l2_subdev_ops提供操作函数。
struct v4l2_subdev {
struct v4l2_device *v4l2_dev; // 所属的v4l2_device
struct module *owner; // 所属模块
const struct v4l2_subdev_ops *ops; // 子设备操作函数集
char name[V4L2_SUBDEV_NAME_SIZE]; // 子设备名称
u32 grp_id; // 分组ID
// ...
};
为什么要分subdev?说白了,是为了复用。比如你换了一个传感器,只要新传感器的subdev驱动符合V4L2规范,上层video_device代码几乎不用改。我在项目中就靠这个特性,快速适配了三款不同型号的传感器。
核心关系总结:
| 结构体 | 角色 | 用户空间可见? | 典型实例 |
|---|---|---|---|
| v4l2_device | 顶层容器 | 否 | 一个USB摄像头 |
| video_device | 设备节点 | 是(/dev/videoX) | 主视频流 |
| v4l2_subdev | 内部子模块 | 否(通过ioctl间接访问) | 传感器、ISP、闪光灯 |
2.2 V4L2 ioctl调用流程
用户空间通过ioctl与V4L2驱动交互。比如打开摄像头、设置格式、申请Buffer、开始采集,每一步都对应一个ioctl命令。整个调用流程,我总结为“三层传递”:
- 用户空间:调用ioctl(fd, VIDIOC_*, &arg)
- 内核V4L2核心层:根据命令号分发到video_device的v4l2_file_operations
- 驱动层:具体硬件操作
举个例子,设置图像格式的流程:
// 用户空间
struct v4l2_format fmt;
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1920;
fmt.fmt.pix.height = 1080;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_NV12;
ioctl(fd, VIDIOC_S_FMT, &fmt);
在内核中,VIDIOC_S_FMT最终会调用到你驱动中实现的vidioc_s_fmt_vid_cap函数。我建议你在调试时,在驱动里加一些dev_dbg打印,把传入的width、height、pixelformat都打出来。我曾经被一个应用层传错pixelformat的问题坑过,加了打印后一眼就发现了。
调试小技巧: 使用v4l2-ctl --set-fmt-video=width=1920,height=1080,pixelformat=NV12可以快速验证驱动是否正确处理了格式设置。不用写应用程序,省时省力。
2.3 V4L2 Buffer管理机制
Buffer管理是V4L2最核心、也最容易出问题的部分。说白了,就是解决“摄像头采集的数据怎么传给应用程序”的问题。V4L2支持三种Buffer管理方式:MMAP、USERPTR、DMABUF。
2.3.1 MMAP模式
MMAP是最常用的方式。驱动在内核空间分配物理连续或非连续的内存,应用程序通过mmap系统调用映射到用户空间。流程如下:
- 调用
VIDIOC_REQBUFS请求分配Buffer - 调用
VIDIOC_QUERYBUF查询Buffer信息(偏移量、大小) - 调用
mmap将内核Buffer映射到用户空间 - 调用
VIDIOC_QBUF将Buffer入队 - 调用
VIDIOC_DQBUF取出已填满数据的Buffer
我个人习惯,在REQBUFS时指定V4L2_MEMORY_MMAP,然后驱动在vidioc_reqbufs回调中分配内存。注意,Buffer数量一般设4个,太少容易丢帧,太多浪费内存。
2.3.2 USERPTR模式
USERPTR模式下,应用程序自己分配内存,然后把用户空间地址传给驱动。驱动通过get_user_pages将用户空间页面锁定到内核。这种方式适合应用程序需要自己管理内存的场景。
// 用户空间
void *buffer = malloc(size);
struct v4l2_buffer buf;
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);
注意: USERPTR模式下,应用程序必须保证在Buffer使用期间,用户空间地址不会被释放或重新映射。我曾经遇到过应用程序在DQBUF之前就free了buffer,导致内核访问非法地址,系统直接panic。血的教训。
2.3.3 DMABUF模式
DMABUF是最高效的方式,适合零拷贝场景。比如从摄像头直接到GPU显示,或者到编码器。驱动导出DMA-BUF文件描述符,其他驱动或应用程序通过这个fd直接访问Buffer,不需要CPU拷贝。
三种模式的对比:
| 特性 | MMAP | USERPTR | DMABUF |
|---|---|---|---|
| 内存分配者 | 驱动 | 应用程序 | 驱动(导出) |
| CPU拷贝次数 | 1次(从硬件到用户空间) | 1次 | 0次(零拷贝) |
| 适用场景 | 通用 | 应用管理内存 | 跨设备共享 |
| 实现复杂度 | 低 | 中 | 高 |
嗯,这里要提醒一下:DMABUF虽然性能好,但实现起来最复杂。我建议初学者先从MMAP入手,跑通流程后再考虑优化到DMABUF。你想想看,如果MMAP都调不通,直接上DMABUF只会让问题更复杂。
Buffer管理核心流程(MMAP为例):
REQBUFS → 驱动分配内存
↓
QUERYBUF → 获取Buffer信息
↓
mmap → 映射到用户空间
↓
QBUF → Buffer入队(空Buffer给驱动)
↓
STREAMON → 开始采集
↓
DQBUF → 取出填满数据的Buffer(循环)
↓
... 处理数据 ...
↓
QBUF → 再次入队(循环)
↓
STREAMOFF → 停止采集
这个流程,我建议你亲手写一个简单的测试程序跑一遍。我在带新人时,总是让他们先写一个基于MMAP的采集程序,能正常出图了,再谈优化。基础不牢,地动山摇。