4. ioctl 基础:V4L2 ioctl 编号规则与核心 ioctl 列表
V4L2 驱动与用户空间应用程序之间的主要通信通道是 ioctl 系统调用。理解 V4L2 的 ioctl 编号规则是进行驱动开发和测试的基础,它决定了命令的唯一性、数据方向以及参数大小。
4.1 V4L2 ioctl 编号规则(_IOR / _IOW / _IOWR)
Linux 内核使用一套宏来生成唯一的 ioctl 命令编号,确保不同子系统之间的命令不会冲突。V4L2 严格遵循这套规则。每个 ioctl 命令编号由以下四部分组成:
- 幻数(Magic Number / Type):V4L2 使用
'V'(ASCII 0x76)作为幻数。 - 序号(Number / NR):在 V4L2 子系统内的唯一序号,从 0 开始递增。
- 数据方向(Direction):指示数据在用户空间和内核空间之间的流动方向。
- 数据大小(Size):传递的参数结构体的大小(以字节为单位)。
内核提供了四个宏来构造 ioctl 命令:
| 宏 | 方向 | 含义 | 典型用途 |
|---|---|---|---|
_IO(type, nr) |
无数据 | 不带数据的简单命令 | 查询/设置状态(如流开关) |
_IOR(type, nr, size) |
从内核读 | 用户空间读取内核数据 | 查询能力、获取格式 |
_IOW(type, nr, size) |
写入内核 | 用户空间向内核写入数据 | 设置格式、设置参数 |
_IOWR(type, nr, size) |
双向 | 先写入后读取(或反之) | 查询并修改、扩展命令 |
关键点: V4L2 的所有 ioctl 命令都使用
'V' 作为幻数。例如,VIDIOC_QUERYCAP 的定义为 _IOR('V', 0, struct v4l2_capability),表示这是一个从内核读取 v4l2_capability 结构体的命令。
以下是一个典型的 V4L2 ioctl 命令定义示例:
// 定义在 include/uapi/linux/videodev2.h 中
#define VIDIOC_QUERYCAP _IOR('V', 0, struct v4l2_capability)
#define VIDIOC_ENUM_FMT _IOWR('V', 2, struct v4l2_fmtdesc)
#define VIDIOC_S_FMT _IOWR('V', 4, struct v4l2_format)
#define VIDIOC_REQBUFS _IOWR('V', 8, struct v4l2_requestbuffers)
#define VIDIOC_QBUF _IOWR('V', 15, struct v4l2_buffer)
#define VIDIOC_DQBUF _IOWR('V', 17, struct v4l2_buffer)
#define VIDIOC_STREAMON _IO('V', 18)
#define VIDIOC_STREAMOFF _IO('V', 19)
4.2 核心 ioctl 列表(VIDIOC_QUERYCAP 等)
V4L2 规范定义了大量 ioctl 命令,覆盖了从设备查询到数据流控制的完整生命周期。以下是开发者和测试人员必须掌握的核心 ioctl 列表,按功能分组:
4.2.1 设备查询与能力
| ioctl 命令 | 方向 | 参数结构体 | 功能描述 |
|---|---|---|---|
VIDIOC_QUERYCAP |
_IOR | struct v4l2_capability |
查询设备能力(驱动名、卡名、总线信息、支持的功能标志) |
VIDIOC_ENUM_FMT |
_IOWR | struct v4l2_fmtdesc |
枚举支持的像素格式 |
VIDIOC_ENUM_FRAMESIZES |
_IOWR | struct v4l2_frmsizeenum |
枚举支持的帧尺寸 |
VIDIOC_ENUM_FRAMEINTERVALS |
_IOWR | struct v4l2_frmivalenum |
枚举支持的帧间隔(帧率) |
4.2.2 格式协商
| ioctl 命令 | 方向 | 参数结构体 | 功能描述 |
|---|---|---|---|
VIDIOC_G_FMT |
_IOWR | struct v4l2_format |
获取当前数据格式 |
VIDIOC_S_FMT |
_IOWR | struct v4l2_format |
设置数据格式(驱动可能调整参数) |
VIDIOC_TRY_FMT |
_IOWR | struct v4l2_format |
尝试格式但不实际设置(用于探测) |
4.2.3 缓冲区管理
| ioctl 命令 | 方向 | 参数结构体 | 功能描述 |
|---|---|---|---|
VIDIOC_REQBUFS |
_IOWR | struct v4l2_requestbuffers |
请求分配缓冲区(内存映射/用户指针/DMABUF) |
VIDIOC_QUERYBUF |
_IOWR | struct v4l2_buffer |
查询已分配缓冲区的信息(偏移、长度等) |
VIDIOC_QBUF |
_IOWR | struct v4l2_buffer |
将缓冲区放入队列(入队) |
VIDIOC_DQBUF |
_IOWR | struct v4l2_buffer |
从队列取出缓冲区(出队,通常阻塞等待) |
VIDIOC_EXPBUF |
_IOWR | struct v4l2_exportbuffer |
导出缓冲区为 DMABUF 文件描述符 |
4.2.4 流控制
| ioctl 命令 | 方向 | 参数结构体 | 功能描述 |
|---|---|---|---|
VIDIOC_STREAMON |
_IO | 无(传入缓冲队列类型指针) | 启动视频流 |
VIDIOC_STREAMOFF |
_IO | 无(传入缓冲队列类型指针) | 停止视频流 |
4.2.5 控制与事件
| ioctl 命令 | 方向 | 参数结构体 | 功能描述 |
|---|---|---|---|
VIDIOC_QUERYCTRL |
_IOWR | struct v4l2_queryctrl |
查询控制项信息(亮度、对比度等) |
VIDIOC_G_CTRL |
_IOWR | struct v4l2_control |
获取控制项当前值 |
VIDIOC_S_CTRL |
_IOWR | struct v4l2_control |
设置控制项值 |
VIDIOC_DQEVENT |
_IOR | struct v4l2_event |
出队一个事件(如帧丢失、控制变化) |
测试提示: 在
v4l2-compliance 测试中,VIDIOC_QUERYCAP 是第一个被调用的 ioctl。如果这个调用失败,测试会立即终止并报告“无法查询设备能力”。确保驱动正确实现 vidioc_querycap 回调是驱动测试通过的第一步。
4.3 V4L2 ioctl 调用流程
下图展示了用户空间应用程序通过 ioctl 与 V4L2 驱动交互的典型流程,以及 ioctl 编号在其中的作用:
4.4 ioctl 编号的调试与验证
在驱动开发和测试过程中,验证 ioctl 编号是否正确生成和解析非常重要。可以使用 strace 工具捕获应用程序发出的 ioctl 调用:
# 使用 strace 捕获 v4l2-ctl 的 ioctl 调用
strace -e ioctl v4l2-ctl -d /dev/video0 --list-formats 2>&1 | grep VIDIOC
# 输出示例:
# ioctl(3, VIDIOC_QUERYCAP, {driver="uvcvideo", card="UVC Camera"}) = 0
# ioctl(3, VIDIOC_ENUM_FMT, {index=0, type=V4L2_BUF_TYPE_VIDEO_CAPTURE, ...}) = 0
常见错误: 如果驱动中错误地使用了
_IOR 代替 _IOWR(或反之),虽然 ioctl 编号可能不同,但更严重的问题是内核的访问权限检查会失败。例如,一个需要写入数据的命令被错误标记为只读,内核可能会拒绝复制用户空间数据,导致驱动读取到未初始化的内存。
4.5 核心 ioctl 调用顺序
一个典型的 V4L2 视频采集应用程序会按照以下顺序调用核心 ioctl:
- VIDIOC_QUERYCAP:检查设备是否支持所需功能(如视频捕获)。
- VIDIOC_ENUM_FMT / VIDIOC_ENUM_FRAMESIZES:枚举支持的格式和分辨率。
- VIDIOC_S_FMT:设置采集格式和分辨率。
- VIDIOC_REQBUFS:请求分配缓冲区(通常为 3~4 个)。
- VIDIOC_QUERYBUF:查询每个缓冲区的信息,用于 mmap 映射。
- VIDIOC_QBUF:将所有缓冲区入队。
- VIDIOC_STREAMON:启动视频流。
- VIDIOC_DQBUF:循环出队已填充数据的缓冲区(处理数据)。
- VIDIOC_QBUF:将处理完的缓冲区重新入队。
- VIDIOC_STREAMOFF:停止视频流。
测试要点:
v4l2-compliance 会严格按照上述顺序测试每个 ioctl 的合法性,并检查驱动在异常调用顺序下的行为(例如,在未调用 REQBUFS 的情况下直接调用 QBUF 应返回错误)。