4. V4L2 文件操作接口:v4l2_fops 结构体与核心回调实现
在 Linux V4L2 框架中,用户空间通过标准的文件操作接口(open、close、ioctl、mmap、poll)与摄像头设备进行交互。V4L2 驱动核心将这些系统调用映射到驱动开发者实现的回调函数,这些回调函数统一封装在 v4l2_fops 结构体中。本章将深入剖析该结构体的定义、各回调函数的职责与典型实现模式。
4.1 v4l2_fops 结构体定义
v4l2_fops 定义在 include/media/v4l2-dev.h 中,它是对标准 struct file_operations 的 V4L2 封装。驱动通常不直接使用 file_operations,而是通过 video_device 的 fops 字段注册 V4L2 专属回调。
// 内核源码 include/media/v4l2-dev.h (简化示意)
struct v4l2_fops {
struct module *owner;
int (*open)(struct file *file);
int (*release)(struct file *file);
long (*ioctl)(struct file *file, unsigned int cmd, unsigned long arg);
int (*mmap)(struct file *file, struct vm_area_struct *vma);
__poll_t (*poll)(struct file *file, struct poll_table_struct *wait);
// ... 其他可选回调
};
| 回调函数 | 触发时机 | 典型职责 |
|---|---|---|
open |
用户调用 open("/dev/video0") |
初始化硬件、分配缓冲区、设置默认参数 |
release |
用户调用 close(fd) 或进程退出 |
停止流、释放缓冲区、关闭硬件 |
ioctl |
用户调用 ioctl(fd, cmd, arg) |
处理 VIDIOC_* 命令,如格式协商、缓冲区管理 |
mmap |
用户调用 mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0) |
将内核 DMA 缓冲区映射到用户空间 |
poll |
用户调用 select() / poll() / epoll() |
报告缓冲区是否可读(有帧数据)或可写 |
video_device 的 v4l2_fops 字段,而非直接操作 file_operations。V4L2 核心层会自动处理 v4l2_fh(文件句柄)的分配与释放,简化驱动开发。
4.2 open 回调:设备初始化入口
open 回调是用户空间访问设备的第一步。驱动在此函数中完成硬件初始化、电源管理、默认参数设置等操作。V4L2 框架提供了 v4l2_fh_open() 辅助函数,用于自动初始化文件句柄。
static int mycam_open(struct file *file)
{
struct video_device *vdev = video_devdata(file);
struct mycam_dev *dev = video_get_drvdata(vdev);
int ret;
// 1. 调用 V4L2 核心辅助函数,初始化 v4l2_fh
ret = v4l2_fh_open(file);
if (ret)
return ret;
// 2. 检查设备是否已被占用(互斥访问)
if (mutex_lock_interruptible(&dev->lock)) {
v4l2_fh_release(file);
return -ERESTARTSYS;
}
// 3. 硬件初始化:上电、配置时钟、复位传感器
if (!dev->powered) {
ret = mycam_power_on(dev);
if (ret) {
mutex_unlock(&dev->lock);
v4l2_fh_release(file);
return ret;
}
dev->powered = true;
}
// 4. 设置默认格式(例如 640x480 YUYV)
dev->fmt.pixelformat = V4L2_PIX_FMT_YUYV;
dev->fmt.width = 640;
dev->fmt.height = 480;
mutex_unlock(&dev->lock);
return 0;
}
VIDIOC_STREAMON 时进行,或使用引用计数管理硬件状态。
4.3 release 回调:资源清理与关闭
release 回调在文件描述符关闭时被调用。驱动必须在此函数中停止正在进行的视频流、释放已分配的缓冲区、关闭硬件电源。V4L2 核心的 v4l2_fh_release() 会清理文件句柄关联的资源。
static int mycam_release(struct file *file)
{
struct video_device *vdev = video_devdata(file);
struct mycam_dev *dev = video_get_drvdata(vdev);
mutex_lock(&dev->lock);
// 1. 如果流正在运行,先停止
if (dev->streaming) {
mycam_stop_streaming(dev);
dev->streaming = false;
}
// 2. 释放用户空间映射的缓冲区
// (V4L2 核心会在 v4l2_fh_release 中处理 vb2_queue 的清理)
// 驱动只需确保 vb2_queue 已正确停止
// 3. 关闭硬件电源(引用计数减一)
if (--dev->open_count == 0) {
mycam_power_off(dev);
dev->powered = false;
}
mutex_unlock(&dev->lock);
// 4. 调用 V4L2 核心释放函数
return v4l2_fh_release(file);
}
4.4 ioctl 回调:V4L2 命令分发中心
ioctl 是 V4L2 驱动中最复杂的回调函数。它负责处理所有 VIDIOC_* 命令,包括格式查询、参数设置、缓冲区管理、流控制等。V4L2 框架提供了 video_ioctl2() 标准分发函数,驱动开发者只需实现具体的命令处理函数。
// 推荐方式:使用 video_ioctl2 自动分发
static const struct v4l2_ioctl_ops mycam_ioctl_ops = {
.vidioc_querycap = mycam_querycap,
.vidioc_enum_fmt_vid_cap = mycam_enum_fmt_vid_cap,
.vidioc_g_fmt_vid_cap = mycam_g_fmt_vid_cap,
.vidioc_s_fmt_vid_cap = mycam_s_fmt_vid_cap,
.vidioc_reqbufs = mycam_reqbufs,
.vidioc_querybuf = mycam_querybuf,
.vidioc_qbuf = mycam_qbuf,
.vidioc_dqbuf = mycam_dqbuf,
.vidioc_streamon = mycam_streamon,
.vidioc_streamoff = mycam_streamoff,
};
// ioctl 回调只需一行
static long mycam_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
return video_ioctl2(file, cmd, arg);
}
video_ioctl2() 内部根据 cmd 值查表调用 v4l2_ioctl_ops 中对应的函数。驱动开发者无需手动编写 switch-case 分发逻辑,只需填充 v4l2_ioctl_ops 结构体即可。
4.5 mmap 回调:零拷贝数据传递
mmap 回调实现内核 DMA 缓冲区到用户空间的直接映射,避免数据拷贝。V4L2 的 videobuf2 框架提供了 vb2_mmap() 通用实现,驱动通常直接调用该函数。
static int mycam_mmap(struct file *file, struct vm_area_struct *vma)
{
struct video_device *vdev = video_devdata(file);
struct mycam_dev *dev = video_get_drvdata(vdev);
// 直接委托给 videobuf2 框架处理
return vb2_mmap(&dev->vb2_queue, vma);
}
// vb2_mmap 内部工作流程:
// 1. 从 vma 中获取缓冲区索引和偏移
// 2. 查找对应的 vb2_buffer
// 3. 获取 DMA 物理地址
// 4. 调用 remap_pfn_range() 建立页表映射
// 5. 设置 VM_SHARED | VM_IO | VM_DONTEXPAND 标志
4.6 poll 回调:异步事件通知
poll 回调允许用户空间通过 select() 或 poll() 等待特定事件(如新帧到达)。V4L2 的 videobuf2 框架提供了 vb2_poll() 标准实现,驱动只需调用即可。
static __poll_t mycam_poll(struct file *file, struct poll_table_struct *wait)
{
struct video_device *vdev = video_devdata(file);
struct mycam_dev *dev = video_get_drvdata(vdev);
// 委托给 videobuf2 框架
return vb2_poll(&dev->vb2_queue, file, wait);
}
// vb2_poll 内部逻辑:
// 1. 调用 poll_wait() 将进程添加到 vb2_queue 的等待队列
// 2. 检查 done_list 中是否有已完成的缓冲区
// 3. 如果有,返回 EPOLLIN | EPOLLRDNORM
// 4. 如果没有,返回 0(进程将被阻塞直到硬件中断唤醒)
4.7 回调函数协作流程
下图展示了用户空间操作与内核回调函数之间的完整协作流程:
4.8 完整注册示例
以下代码展示了如何将上述回调函数注册到 video_device 中:
static const struct v4l2_fops mycam_fops = {
.owner = THIS_MODULE,
.open = mycam_open,
.release = mycam_release,
.ioctl = mycam_ioctl, // 内部调用 video_ioctl2
.mmap = mycam_mmap, // 委托 vb2_mmap
.poll = mycam_poll, // 委托 vb2_poll
};
static int mycam_probe(struct platform_device *pdev)
{
struct mycam_dev *dev;
struct video_device *vdev;
// ... 分配设备结构体、初始化锁、vb2_queue 等 ...
// 注册 video_device
vdev = video_device_alloc();
vdev->fops = &mycam_fops; // 绑定文件操作回调
vdev->ioctl_ops = &mycam_ioctl_ops; // 绑定 ioctl 命令处理
vdev->release = video_device_release;
vdev->vfl_dir = VFL_DIR_RX;
strscpy(vdev->name, "MyCam", sizeof(vdev->name));
video_set_drvdata(vdev, dev);
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret) {
video_device_release(vdev);
return ret;
}
dev->vdev = vdev;
return 0;
}