4. V4L2文件操作接口:v4l2_fops结构体
好,咱们今天聊聊V4L2驱动里最核心的那层皮——文件操作接口。说白了,就是用户空间调用open()、close()、ioctl()这些系统调用时,内核里到底发生了什么。
我个人习惯把v4l2_fops比作驱动的大门。用户程序敲敲门,你得知道怎么开门、怎么关门、怎么递东西。这个结构体定义在include/media/v4l2-dev.h里,咱们直接看代码:
static const struct v4l2_file_operations my_v4l2_fops = {
.owner = THIS_MODULE,
.open = my_v4l2_open,
.release = my_v4l2_release,
.unlocked_ioctl = my_v4l2_ioctl,
.mmap = my_v4l2_mmap,
.poll = my_v4l2_poll,
};
嗯,这里要注意,V4L2推荐用unlocked_ioctl而不是ioctl。为什么?因为内核在2.6.36之后就把大内核锁(BKL)给废了,unlocked_ioctl才是正道。我在项目中遇到过有人用了ioctl,结果在4.x内核上编译直接报错,折腾了半天。
4.1 open与release:驱动的生命周期
open回调是驱动的起点。用户程序调用open("/dev/video0")时,内核会调用这个函数。你想想看,这时候驱动该做什么?
- 分配设备私有结构体
- 初始化硬件状态(比如上电、配置时钟)
- 初始化视频缓冲区队列
- 增加模块引用计数
static int my_v4l2_open(struct file *file)
{
struct my_device *dev = video_drvdata(file);
int ret;
// 检查设备是否已被占用
if (dev->users > 0)
return -EBUSY;
// 初始化硬件
ret = my_hardware_init(dev);
if (ret < 0)
return ret;
// 初始化vb2队列
ret = vb2_queue_init(&dev->vb2_q);
if (ret < 0)
goto err_hw_deinit;
dev->users++;
file->private_data = dev;
return 0;
err_hw_deinit:
my_hardware_deinit(dev);
return ret;
}
release正好相反。用户调用close()时,你得把打开时分配的资源全部释放掉。我曾经见过一个驱动,release里忘了调用vb2_queue_release(),结果每次关闭设备都内存泄漏,跑个几天系统就挂了。
避坑指南:我曾经在release里忘记检查是否有正在进行的DMA传输,结果直接关闭了时钟,导致DMA访问挂起地址,整个系统死锁。记住:先停止硬件,再释放资源。
4.2 ioctl:V4L2的命令中枢
ioctl是V4L2驱动里最复杂的部分。用户程序通过它来设置格式、申请缓冲区、开始/停止采集等等。V4L2框架提供了video_ioctl2这个辅助函数,它会自动帮你分发命令。
static long my_v4l2_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct my_device *dev = video_drvdata(file);
// 检查设备是否已初始化
if (!dev->initialized)
return -ENODEV;
// 使用video_ioctl2进行命令分发
return video_ioctl2(file, cmd, arg);
}
你可能会问:为什么不直接在ioctl里写switch-case?嗯,video_ioctl2会自动处理VIDIOC_QUERYCAP、VIDIOC_G_FMT这些标准命令,你只需要实现对应的回调函数就行。比如:
static int my_v4l2_s_fmt(struct file *file, void *priv,
struct v4l2_format *f)
{
struct my_device *dev = video_drvdata(file);
// 验证格式是否支持
if (f->fmt.pix.pixelformat != V4L2_PIX_FMT_YUYV)
return -EINVAL;
// 保存格式信息
dev->fmt = f->fmt.pix;
// 通知硬件更改格式
my_hardware_set_format(dev);
return 0;
}
个人经验:我建议在实现ioctl时,先调用v4l2_ioctl_ops里的标准函数,再处理自定义命令。这样既保证了兼容性,又方便扩展。
4.3 mmap:零拷贝的秘密
mmap是V4L2实现高效数据传输的关键。用户程序通过它把内核空间的视频缓冲区映射到用户空间,避免了数据拷贝。说白了,就是让用户程序直接读写DMA缓冲区。
static int my_v4l2_mmap(struct file *file, struct vm_area_struct *vma)
{
struct my_device *dev = video_drvdata(file);
// 调用vb2的mmap实现
return vb2_mmap(&dev->vb2_q, vma);
}
这里有个坑:vma结构体里的vm_flags必须设置VM_SHARED标志,否则映射会失败。我记得有一次调试,用户程序mmap总是返回EINVAL,查了半天发现是用户程序忘记设置MAP_SHARED标志。
| 标志 | 说明 | 必须设置? |
|---|---|---|
| VM_SHARED | 共享映射 | 是 |
| VM_IO | IO内存映射 | 是 |
| VM_DONTEXPAND | 禁止扩展 | 建议 |
| VM_DONTDUMP | 禁止core dump | 建议 |
4.4 poll:非阻塞的等待
poll回调让用户程序可以非阻塞地等待视频数据。用户调用select()或epoll()时,内核会调用这个函数。你需要告诉内核:当前是否有数据可读?
static unsigned int my_v4l2_poll(struct file *file,
struct poll_table_struct *wait)
{
struct my_device *dev = video_drvdata(file);
// 调用vb2的poll实现
return vb2_poll(&dev->vb2_q, file, wait);
}
你想想看,poll的核心逻辑其实很简单:
- 如果有数据在缓冲区里,返回
POLLIN | POLLRDNORM - 如果没有数据,把当前进程加入等待队列,返回0
- 如果发生错误,返回
POLLERR
核心要点:V4L2框架的vb2_poll已经帮你处理了大部分逻辑。你只需要确保vb2队列正确配置了wait_queue。我在项目中遇到过poll永远不返回的问题,最后发现是vb2队列的start_streaming回调里忘记调用wake_up_interruptible。
4.5 知识体系总览
下面这张图展示了v4l2_fops各回调函数之间的关系:
这张图把整个调用流程串起来了。用户空间的请求经过V4L2核心框架分发到v4l2_fops的各个回调,最终落到硬件操作层。每个回调各司其职,互不干扰。
个人建议:写驱动时,先把这五个回调的骨架搭好,再慢慢填充细节。我习惯先实现open和release,确保设备能正常打开关闭,然后再加ioctl、mmap和poll。这样调试起来思路清晰,出了问题也容易定位。
好了,关于v4l2_fops的五个核心回调,咱们就聊到这儿。记住:open管生,release管死,ioctl管命令,mmap管映射,poll管等待。把这五个搞明白了,V4L2驱动的大门就算敲开了。