3. 字符设备驱动接口:file_operations 结构体与回调函数注册
V4L2 设备在用户空间表现为 /dev/videoX 字符设备节点。应用程序通过标准的 POSIX 系统调用(open、close、ioctl、read、write、mmap)与驱动交互,而驱动侧则通过注册 struct file_operations 结构体来响应这些调用。本章深入剖析该结构体的每个回调函数在 V4L2 框架中的注册方式、典型实现与交互流程。
3.1 struct file_operations 结构体总览
Linux 内核使用 struct file_operations 定义字符设备支持的操作。V4L2 驱动通常需要实现以下关键成员:
| 回调函数 | 对应系统调用 | V4L2 框架推荐实现 | 典型用途 |
|---|---|---|---|
| open | open() | v4l2_fh_open / 自定义 | 初始化文件句柄、分配资源、上电 |
| release | close() | v4l2_fh_release / 自定义 | 释放资源、停止流、下电 |
| unlocked_ioctl | ioctl() | video_ioctl2 | 处理 VIDIOC_xxx 控制命令 |
| read | read() | vb2_fop_read | 读取已捕获的视频帧数据 |
| write | write() | vb2_fop_write | 写入视频帧数据(输出设备) |
| mmap | mmap() | vb2_fop_mmap | 将 DMA 缓冲区映射到用户空间 |
| poll | select/poll/epoll | vb2_fop_poll | 等待缓冲区就绪或事件发生 |
unlocked_ioctl 替代了旧的 ioctl 回调。V4L2 驱动应始终注册 unlocked_ioctl,并设置为 video_ioctl2。
3.2 open 与 release 回调:设备生命周期管理
open 回调在用户程序调用 open("/dev/video0", O_RDWR) 时触发。V4L2 驱动通常在此完成:
- 调用
v4l2_fh_open()初始化 V4L2 文件句柄(struct v4l2_fh)。 - 递增设备引用计数,防止模块卸载。
- 初始化硬件状态(如设置默认分辨率、格式)。
- 申请中断、DMA 缓冲区等资源(也可延迟到流开启时)。
release 回调在 close() 或进程退出时调用,负责:
- 调用
v4l2_fh_release()清理文件句柄。 - 停止正在进行的视频流(调用
vb2_queue_release())。 - 释放硬件资源、关闭时钟、下电。
- 递减引用计数。
// 典型的 V4L2 驱动 open/release 实现模式
static int my_v4l2_open(struct file *file)
{
struct video_device *vdev = video_devdata(file);
struct my_dev *dev = video_get_drvdata(vdev);
int ret;
// 1. 调用 V4L2 框架标准 open
ret = v4l2_fh_open(file);
if (ret)
return ret;
// 2. 设备特定初始化
mutex_lock(&dev->lock);
// ... 上电、设置默认格式等
mutex_unlock(&dev->lock);
return 0;
}
static int my_v4l2_release(struct file *file)
{
struct video_device *vdev = video_devdata(file);
struct my_dev *dev = video_get_drvdata(vdev);
// 1. 停止流并释放 vb2 队列资源
mutex_lock(&dev->lock);
vb2_queue_release(&dev->vb2_q);
// ... 下电、关闭时钟
mutex_unlock(&dev->lock);
// 2. 调用 V4L2 框架标准 release
return v4l2_fh_release(file);
}
3.3 unlocked_ioctl 回调:V4L2 命令分发中心
unlocked_ioctl 是 V4L2 驱动最核心的回调。标准做法是将其注册为 video_ioctl2,该函数会自动解析 ioctl 命令码,并调用驱动注册的 v4l2_ioctl_ops 中的对应处理函数。
// file_operations 中注册 ioctl 回调
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_v4l2_open,
.release = my_v4l2_release,
.unlocked_ioctl = video_ioctl2, // V4L2 标准 ioctl 分发器
.mmap = vb2_fop_mmap,
.read = vb2_fop_read,
.poll = vb2_fop_poll,
};
video_ioctl2 的内部工作流程:
- 从用户空间拷贝 ioctl 命令码和参数。
- 根据命令码在
v4l2_ioctl_ops表中查找对应的处理函数。 - 调用处理函数(如
vidioc_s_fmt_vid_cap)。 - 将结果拷贝回用户空间。
video_ioctl2 委托给 v4l2_ioctl_ops。这样可以确保 ioctl 的序列化、权限检查和参数校验由框架统一完成。
3.4 read/write 回调:传统帧读取方式
虽然 V4L2 推荐使用流式 I/O(mmap + DQBUF/QBUF),但 read 和 write 回调仍然被广泛支持,主要用于简单应用或调试。V4L2 框架提供了 vb2_fop_read 和 vb2_fop_write 标准实现:
- vb2_fop_read:内部调用
VIDIOC_DQBUF获取已捕获的帧,将数据从内核缓冲区拷贝到用户缓冲区,然后调用VIDIOC_QBUF将缓冲区归还给驱动。 - vb2_fop_write:用于输出设备,将用户数据拷贝到内核缓冲区,入队到输出队列。
// read 回调的内部简化流程
ssize_t vb2_fop_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
// 1. 从队列中出队一个已填充的缓冲区
// 2. 将缓冲区数据拷贝到用户空间 buf
// 3. 将缓冲区重新入队
// 4. 返回实际读取的字节数
}
3.5 mmap 回调:零拷贝内存映射
mmap 回调是 V4L2 高性能视频采集的关键。驱动通过 vb2_fop_mmap 标准函数实现:
// mmap 回调的标准注册方式
static const struct file_operations my_fops = {
.mmap = vb2_fop_mmap,
};
// vb2_fop_mmap 内部逻辑
int vb2_fop_mmap(struct file *file, struct vm_area_struct *vma)
{
// 1. 从 file 获取 vb2_queue 实例
// 2. 根据 vma->vm_pgoff 计算缓冲区索引
// 3. 调用 vb2_mmap() 建立 DMA 缓冲区到用户空间的页表映射
// 4. 设置 VMA 标志(VM_SHARED、VM_IO、VM_DONTEXPAND)
}
mmap 映射的缓冲区通常是物理连续的 DMA 内存,通过 dma_alloc_coherent 或 ion 分配。映射后,用户空间可以直接读写硬件 DMA 区域,实现零拷贝。
3.6 file_operations 完整注册示例
以下是一个完整的 V4L2 字符设备驱动 file_operations 注册代码,展示了所有关键回调的绑定方式:
// 完整的 V4L2 驱动 file_operations 定义
static const struct v4l2_file_operations my_v4l2_fops = {
.owner = THIS_MODULE,
.open = my_v4l2_open,
.release = my_v4l2_release,
.unlocked_ioctl = video_ioctl2,
.mmap = vb2_fop_mmap,
.read = vb2_fop_read,
.poll = vb2_fop_poll,
};
// video_device 初始化时绑定 fops
static struct video_device my_vdev_template = {
.name = "MyV4L2Camera",
.vfl_dir = VFL_DIR_RX,
.fops = &my_v4l2_fops, // 绑定 file_operations
.ioctl_ops = &my_ioctl_ops, // 绑定 ioctl 处理函数表
.release = video_device_release_empty,
};
// 在 probe 函数中注册 video_device
static int my_probe(struct platform_device *pdev)
{
struct video_device *vdev;
int ret;
// 分配 video_device
vdev = video_device_alloc();
if (!vdev)
return -ENOMEM;
*vdev = my_vdev_template;
video_set_drvdata(vdev, dev);
// 注册到 V4L2 框架
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret) {
video_device_release(vdev);
return ret;
}
dev->vdev = vdev;
return 0;
}
- 使用
struct v4l2_file_operations而非直接使用struct file_operations,前者是 V4L2 框架的封装,增加了对v4l2_fh的支持。 unlocked_ioctl必须设置为video_ioctl2,由框架负责命令分发。mmap、read、poll优先使用vb2_fop_*系列标准函数,避免重复实现。open和release通常需要自定义,但内部应调用v4l2_fh_open/release确保框架状态正确。
3.7 回调函数注册避坑清单
| 常见错误 | 后果 | 正确做法 |
|---|---|---|
| 未注册 unlocked_ioctl | ioctl 调用返回 -ENOTTY | 必须设置为 video_ioctl2 |
| open 中未调用 v4l2_fh_open | v4l2_fh 未初始化,ioctl 可能崩溃 | 在自定义 open 中首先调用 v4l2_fh_open |
| release 中未调用 vb2_queue_release | DMA 缓冲区泄漏,内存耗尽 | 在 release 中显式停止队列并释放缓冲区 |
| mmap 未使用 vb2_fop_mmap | 映射失败或页表错误 | 直接注册 vb2_fop_mmap,不要自定义 |
| poll 未使用 vb2_fop_poll | select/poll 无法正确唤醒 | 使用标准实现,支持缓冲区就绪事件 |