当前位置:网站首页 > 技术博客 > 正文

v4l2 poll



原文链接

本文开启 linux 内核 V4L2 框架部分的学习之旅,本文仅先对 V4L2 的框架做一个综述性的概括介绍,然后接下来的文章中会对 V4L2 框架的各个子模块进行一个全面的介绍,包括每一部分的实现原理,如何使用,用在什么地方等等。预计接下来的文章大概有5篇(不带本篇)。坑已经挖好了,开始吧。

导读:V4L2 是专门为 linux 设备设计的一套视频框架,其主体框架在 linux 内核,可以理解为是整个 linux 系统上面的视频源捕获驱动框架。其广泛应用在嵌入式设备以及移动端、个人电脑设备上面,市面上的编码产品类如:SDV、手机、IPC、行车记录仪都会用到这个框架来进行视频采集,当然,有比较厉害的厂家直接就使用自己实现的一套视频采集框架,这种属于是厂家中战斗机了。下文主要参考linux-4.4内核文档对V4L2框架进行一次全局的介绍。

几乎所有的设备都有多个 IC 模块,它们可能是实体的(例如 USB 摄像头里面包含 ISP、sensor 等)、也可能是抽象的(如 USB 设备里面的抽象拓扑结构),它们在 目录下面生成了多个设备节点,并且这些 IC 模块还创建了一些非 v4l2 设备:DVB、ALSA、FB、I2C 和输入设备。正是由于硬件的复杂性,v4l2 的驱动也变得非常复杂。

特别是 v4l2 驱动要支持 IC 模块来进行音/视频的混合/编解码操作,这就更加使得 v4l2 驱动变得异常复杂。通常情况下,有些IC模块通过一个或者多个 I2C 总线连接到主桥驱动上面,同时其它的总线仍然可用,这些 IC 就称为 ‘sub-devices’,比如摄像头设备里面的 sensor 传感器就是使用 I2C 来进行命令沟通,同时使用 MIPI 或者 LVDS 等接口进行图像数据传输。

在很长一段时间内,该框架(指老旧的 V4L2 框架)仅限于通过 video_device 结构体创建 v4l 设备节点和 video_buf 来处理视频数据。这意味着所有的驱动都必须对设备实例进行设置并将其映射到子设备上。有些时候这些操作步骤十分复杂,很难正确完成,并且有些驱动程序从来没有正确的按照这些操作步骤编写。由于缺少一个框架,有很多通用代码就没有办法被重构,从而导致这部分代码被重复编写,效率比较低下。

因此,本框架抽象构建了所有驱动都需要的代码并封装为一个个的模块,简化了设备驱动通用代码的重构。v4l2-pci-skeleton.c 是个非常好的参考例程,它是一个PCI采集卡的驱动框架。该例程演示了如何使用 v4l2 驱动框架,并且该例程可以作为一个 PCI 视频采集卡的驱动模板使用。在最开始的时候也可以参照这个代码编写方式进行联系,当然最适合的代码还是 drivers/media/video/omap3isp 文件夹里面的代码,这个代码基本上可以作为一个完整的输入设备实例代码(因为它包含了 ISP、CSI、video 等设备,并且有着一个完整的数据流 pipeline,几乎用到了 V4L2 框架的方方面面,参考价值极大)来进行参考编写自己的设备驱动代码。

蓝图解构

这是一张非常大的图,但是我只选取了其中的一个,这张图对 V4L2 里面的子模块进行简化(简化到只有子模块的名字,没有内部实现的介绍),大图如下:
V4L2 设备拓扑

V4L2 设备拓扑

这张图怎么看呢?它有以下几个关键因素:

  • :这个是整个输入设备的总结构体,可以认为它是整个 V4L2 框架的入口,充当驱动的管理者以及入口监护人。由该结构体引申出来 。用于视频输入设备整体的管理,有多少输入设备就有多少个抽象(比如一个USB摄像头整体就可以看作是一个 V4L2 device)。再往下分是输入子设备,对应的是例如 ISP、CSI、MIPI 等设备,它们是从属于一个 V4L2 device 之下的。
  • :用于运行时数据流的管理,嵌入在 V4L2 device 内部,运行时的意思就是:一个 V4L2 device 下属可能有非常多同类型的子设备(两个或者多个 sensor、ISP 等),那么在设备运行的时候我怎么知道我的数据流需要用到哪一个类型的哪一个子设备呢。这个时候就轮到 出手了,它为这一坨的子设备建立一条虚拟的连线,建立起来一个运行时的 pipeline(管道),并且可以在运行时动态改变、管理接入的设备。
  • :控制模块,提供子设备(主要是 video 和 ISP 设备)在用户空间的特效操作接口,比如你想改变下输出图像的亮度、对比度、饱和度等等,都可以通过这个来完成。
  • :提供内核与用户空间的 buffer 流转接口,输入设备产生了一坨图像数据,在内核里面应该放在哪里呢?能放几个呢?是整段连续的还是还是分段连续的又或者是物理不连续的?用户怎么去取用呢?都是它在管理。

层级解构

  1. 可以看到图中的入口 ,它是由用户定义的一个结构体,重要的不是它怎么定义的,重要的是它里面有一个 结构体,上文说到,这个结构体总览全局,运筹帷幄,相当于中央管理处的位置,那么中央决定了,它就是整个输入设备整体的抽象(比如整个 USB 摄像头输入设备,比如整个 IPC 摄像头输入设备)。它还有一个 结构体,上文也说道,,它是管数据流线路的,属于搞结构路线规划管理的。
  2. 往后 里面有一个链表,它维护了一个巨大的子设备链,所有的子设备都通过内核的双向循环链表结构以 为中心紧紧团结在一起。另外 在往里面去就是一个个的 (现在不需要了解它的具体含义,只需要知道它就是类似电路板上面的元器件一样的抽象体), 之间建立了自己的小圈子,在它们这个小圈子里面数据流按照一定的顺序畅通无阻,恣意遨游。
  3. 到结尾处,抽象出来了 设备节点,这个就是外交部的角色,它负责提供了一个内核与用户空间的交流枢纽。需要注意的是,该设备节点的本质还是一个字符设备,其内部的一套操作与字符设备是一样的,只不过是进行了一层封装而已。
  4. 到此为止,一个 V4L2 大概的四层结构就抽象出来了,如下图所示:
    V4L2 层次结构
V4L2 层次结构

所有的 V4L2 驱动都有以下结构体类型:

  • 每个设备都有一个设备实例结构体(上面的 ),里面包含了设备的状态;
  • 一种初始化以及控制子设备()的方法;
  • 创建v4l2设备节点并且对设备节点的特定数据()保持跟踪;
  • 含有文件句柄的文件句柄结构体( 文件句柄与句柄结构体一一对应);
  • 视频数据处理();

结构体实例

  • 框架结构体()
    与驱动结构体非常类似,参考上面的解释,这里不再赘述。v4l2 框架也可以整合到 media framework 里面。如果驱动程序设置了 的 成员,那么子设备与 video 节点都会被自动当作 media framework 里的 entitiy 抽象。
  • 结构体
    每一个设备实例都被抽象为一个 结构体。一些简单的设备可以仅分配一个 结构体即可,但是
    大多数情况下需要将该结构体嵌入到一个更大的结构体()里面。必须用 来注册设备实例。该函数会初始化传入的 结构体,如果 成员为空的话,该函数就会设置其指向传入的 参数。
  • 集成 media framework
    如果驱动想要集成 media framework 的话,就需要人为地设置 指向驱动适配的结构体(该结构体由
    驱动自定义- ,里面嵌入 结构体)。在注册 之前就需要调用 来完成设置。并且必须设置 的 成员指向注册的 结构体实例。
  • 设备节点的命名
    如果 的 成员为空的话,就按照 成员的名称来命名,如果 成员也为空的话,就必须在注册 之前设置它的 成员。可以使用 函数来设置 成员,该函数会基于驱动名以及驱动实例的索引号来生成 成员的名称,类似于 ivtv0、ivtv1 等等,如果驱动名的最后一个字母是整数的话,生成的名称就类似于cx18-0、cx18-1等等,该函数的返回值是驱动实例的索引号。
  • 回调函数与设备卸载
    还可以提供一个 回调函数给 接收来自子设备的事件通知。当然,是否需要设置该回调函数取决于子设备是否有向主设备发送通知事件的需求。 的卸载需调用到 函数。在该函数被调用之后,如果 指向 的话,该指针将会被设置为NULL。该函数会将所有的子设备全部卸载掉。如果设备是热拔插属性的话,当 disconnect 发生的时候,父设备就会失效,同时 指向父设备的指针也必须被清除,可以调用 函数来清除指针,该函数并不卸载子设备,子设备的卸载还是需要调用到 来完成。如果不是热拔插设备的话,就不必关注这些。

驱动设备使用

有些时候需要对驱动的所有设备进行迭代,这种情况通常发生在多个设备驱动使用同一个硬件设备的情况下,比如 ivtvfb 驱动就是个 framebuffer 驱动,它用到了 ivtv 这个硬件设备。可以使用以下方法来迭代所有的已注册设备:

 

有时候需要对设备实例进行计数以将设备实例映射到模块的全局数组里面,可以使用以下步骤来完成计数操作:

 

如果一个热拔插设备有很多个设备节点(比如一个USB摄像头可以产生多路视频输出,虽然它的视频源是一个),那么很难知道在什么时候才能够安全地卸载 设备。基于以上问题, 引入了引用计数机制,当 函数被调用的时候,引用计数会加一,当 被释放的时候,引用计数会减一,直到 的引用计数到0的时候, 的 回调函数就会被调用,可以在该回调函数里面做一些清理工作。当其它的设备(alsa,因为这个不属于 video 设备,所以也就不能使用上面的 video 函数进行计数的加减操作)节点被创建的时候,可以人为调用以下函数对引用计数进行增减操作:

 

需要注意的是, 函数将引用计数初始化为1,所以需要在 或者 回调方法里面调用 来减少引用计数,否则引用计数将永远不会达到0。


很多设备都需要与子设备进行交互,通常情况下子设备用于音视频的编解码以及混合处理,对于网络摄像机来说子设备就是 sensors 和 camera 控制器。通常情况下它们都是 I2C 设备,但也有例外。 结构体被用于子设备管理。

每一个子设备驱动都必须有一个 结构体,这个结构体可以作为独立的简单子设备存在,也可以嵌入到更大的结构体(自定义的子设备结构体)里面。通常会有一个由内核设置的低层次结构体(,也就是上面说的 i2c 设备),它包含了一些设备数据,要调用 来设置子设备私有数据指针指向它,这样的话就可以很方便的从 找到相关的 I2C 设备数据(这个要编程实现的时候才能够了解它的用意)。另外也需要设置低级别结构的私有数据指针指向 结构体,方便从低级别的结构体访问 结构体,达到双向访问的目的,对于 来说,可以用 函数来设置,其它的需使用与之相应的函数来完成设置。

桥驱动器需要存储每一个子设备的私有数据, 结构体提供了主机私有数据指针成员来实现此目的,使用以下函数可以对主机私有数据进行访问控制:

 

从桥驱动器的角度来看,我们加载子设备模块之后可以用某种方式获取子设备指针。对于 i2c 设备来说,调用 函数即可完成,其它类型的设备也有与之相似的操作,在内核里面提供了不少的帮助函数来协助完成这部分工作,编程时可以多多使用。

每个 结构体都包含有一些函数指针,指向驱动实现的回调函数,内核对这些回调函数进行了分类以避免出现定义了一个巨大的回调函数集,但是里面只有那么几个用得上的尴尬情况。最顶层的操作函数结构体内部包含指向各个不同类别操作函数结构体的指针成员,如下所示:

 

这部分的设计我个人觉得是非常实用的,linux 要想支持大量的设备的同时又要保持代码的精简就必须得这样去实现。成员对于所有的子设备来说都是通用的,其余的成员不同的驱动会有选择的去使用,例如:video 设备就不需要支持 audio 这个 ops 成员。子设备驱动的初始化使用 函数来完成(该函数只是初始化一些 的成员变量,内容比较简单),在初始化之后需要设置子设备结构体的 和 成员(如果是 i2c 设备的话,这个在 i2c helper 函数里面就会被设置)。该部分 ioctl 可以直接通过用户空间的 ioctl 命令访问到(前提是该子设备在用户空间生成了子设备节点,这样的话就可以操作子设备节点来进行 ioctl)。内核里面可以使用 函数来对这些回调函数进行调用,这个在 pipeline 管理的时候十分受用。

如果需要与 media framework 进行集成,必须初始化 结构体并将其嵌入到 结构体里面,操作如下所示:

 

其中 pads 结构体变量必须提前初始化, 的 、、、 成员需要设置。entity 的引用计数在子设备节点被打开/关闭的时候会自动地增减。在销毁子设备的时候需使用 函数对 entity 进行清理。如果子设备需要处理 video 数据,就需要实现 成员,如果要集成到 media_framework 里面,就必须要实现 成员,此时使用 pad_ops 中与 有关的成员代替 中的相关成员。

子设备驱动需要设置 成员来提供自己的 link validation 函数,该回调函数用来检查 pipeline 上面的所有的 link 是否有效(是否有效由自己来做决定),该回调函数在 函数里面被循环调用。如果该成员没有被设置,那么 将会作为默认的回调函数被使用,该函数确保 link 的 source pad 和 sink pad 的宽、高、media 总线像素码是一致的,否则就会返回错误。

有两种方法可以注册子设备(注意是设备,不是设备驱动,常用的方式是通过设备树来注册),第一种(旧的方法,比如使用 来进行注册)是使用桥驱动去注册设备。这种情况下,桥驱动拥有连接到它的子设备的完整信息,并且知道何时去注册子设备,内部子设备通常属于这种情况。比如 SOC 内部的 video 数据处理单元,连接到 USB 或 SOC 的相机传感器。另一种情况是子设备必须异步地被注册到桥驱动上,比如基于设备树的系统,此时所有的子设备信息都独立于桥驱动器。使用这两种方法注册子设备的区别是 probing 的处理方式不同。也就是一种是设备信息结构体由驱动本身持有并注册,一种是设备信息结构体由设备树持有并注册。

设备驱动需要用 信息来注册 ,如下所示:

 

如果子设备模块在注册之前消失的话,该操作就会失败,如果成功的话就会使得 指向 。如果 父设备的 成员不为空的话,子设备的 entity 就会自动地被注册到 mdev 指向的 里面。在子设备需要被卸载并且 变为NULL之后,使用如下函数来卸载子设备:

 

如果子设备被注册到上层的 父设备中,那么 函数就会自动地把所有子设备卸载掉。但为了以防万一以及保持代码的风格统一,需要注册与卸载结对使用。可以用以下方式直接调用ops成员: 使用下面的宏定义可以简化书写:该操作会检查 指针是否为空,如果是,返回 ,同时如果 或者 为空,则返回 。也可以通过以下函数调用来对 V4l2 下面挂载的所有子设备进行回调:

 

该函数会跳过所有不支持该 ops 的子设备,并且所有的错误信息也被忽略,如果想捕获错误信息,可以使用下面的函数:

 

该函数的第二个参数如果为 0,则所有的子设备都会被访问,如果非 0,则指定组的子设备会被访问。

组ID使得桥驱动能够更加精确的去调用子设备操作函数,例如:在一个单板上面有很多个声卡,每个都能够改变音量,但是通常情况下只访问一个,这时就可以设置子设备的组 ID 为 AUDIO_CONTROLLER 并指定它的值,这时 函数就会只去访问指定组的子设备,提高效率。

如果子设备需要向 父设备发送事件通知的话,就可以调用 宏定义来回调 成员(前文有提到过)。

使用 的优点是不包含任何底层硬件的信息,它是对底层硬件的一个抽象,因此一个驱动可能包含多个使用同一条 I2C 总线的子设备,也可能只包含一个使用 GPIO 管脚控制的子设备,只有在驱动设置的时候才有这些差别,而一旦子设备被注册之后,底层硬件对驱动来说就是完全透明的。

另外子设备还提供了一组内部操作函数,该内部函数的调用时机在下面有描述,原型如下所示:

 

这些函数仅供 v4l2 framework 使用,驱动程序不应该显式的去调用这些回调

  • registered/unregister:在子设备被注册()/反注册的时候被调用。
  • open/close:如果子设备在用户空间创建了设备节点,那么这两个函数就会在用户空间的设备节点被打开/关闭的时候调用到,主要是用来创建/关闭v4l2_fh以供v4l2_ctrl_handler等的使用。

可以在 文件夹下创建 设备节点以供用户直接操作子设备硬件。如果需要在用户空间创建设备节点的话,就需要在子设备节点注册之前设置 标志,然后调用 函数,就可以在用户空间创建设备节点,设备节点会在子设备卸载的时候自动地被销毁。

 

上述 ioctls 可以通过设备节点访问,也可以直接在子设备驱动里面调用。

 

要使用上述事件,就必须设置 的 标志位,实现 的 相关的回调函数,回调函数里面需要初始化 events,然后注册 。一些私有的 ioctls 可以在 的 里面实现。

要想在 I2C 驱动里面添加 支持,就需要把 结构体嵌入到每个 I2C 实例结构体里面,有一些比较简单的 I2C 设备不需要自定义的状态结构体,此时只需要创建一个单独的 结构体即可。一个典型的驱动自定义状态结构体如下所示:

 

使用 去初始化一个 I2C 子设备,该函数会填充 的所有成员并确保 与 互相指向对方。也可以添加内联函数来从 的指针获取到 结构体:

 

该函数会加载给定的模块(可以为空)并且调用 根据传入的参数创建子设备结构体,最后注册 。

可以动态的分配:

 

如果需要将 结构体嵌入到更大的结构体里面的话,就需要设置 的 成员。内核提供了两个默认的 回调函数,如下:

 

以下的函数成员必须被设置:

  • v4l2_dev:必须指向v4l2_device父设备
  • vfl_dir:VFL_DIR_RX(capture设备)、VFL_DIR_TX(输出设备)、VFL_DIR_M2M(codec设备)
  • fops:设置v4l2_file_operations结构体
  • ioctl_ops:ioctls,可以通过设备节点被用户空间程序访问,需设置fops的.unlocked_ioctl指向video_ioctl2
  • lock:如果想要在驱动空间里做锁操作,可以设置为NULL。否则需要指向一个已经初始化的mutex_lock结构体
  • queue:指向一个vb2_queue结构体,如果queue->lock不为空,那么与队列相关的ioctls就会使用queue内部的锁,这样的话就不用等待其它类型的ioctls操作
  • prio:对优先级进行跟踪,用在VIDIOC_G/S_PRIORITY上,如果为空的话就会使用v4l2_device里面的v4l2_prio_state
  • dev_parent:指向v4l2_device即可

如果想忽略 中某个 ioctls 的话可以调用下面的函数:

 

如果要集成到 media_framework 里面,就需要设置 里面的 成员,同时需要提供 :

 
  • 的注册
    的注册函数如下:
 

该段代码会注册一个字符设备驱动程序并在用户空间生成一个设备节点。如果 父设备的 成员不为空的话, 的 会被自动的注册到 media framework 里面。函数最后一个参数是设备节点索引号,如果是 -1 的话就取用第一个内核中可用的索引号值。注册的设备类型以及用户空间中的节点名称取决于以下标识:

 

当一个设备节点被创建时,相关属性也会被创建,可以在 里面看到这些设备文件夹,在文件夹里面可以看到 等属性,可以使用 命令查看。‘dev_debug’ 可以用于 video 设备调试,每个 video 设备都会创建一个 ‘dev_debug’ 属性,该属性以文件夹的形式存在与 下面以供使能 log file operation。'dev_debug’是一个位掩码,以下位可以被设置:

 

当以上的位被设置的时候,发生相关的调用或者操作的时候内核就会打印出来相关的调用信息到终端上面。类似于

 
  • video设备的清理
    当 video 设备节点需要被移除或者USB设备断开时,需要执行以下函数:
 

来进行设备的卸载,该函数会移除 下的设备节点文件,同时不要忘记调用 来清理 entity。

V4L 核心层提供了可选的锁服务,最主要的就是 里面的锁,用来进行 ioctls 的同步。如果使用了 框架,那么 锁也会被用来做 queue 相关的 ioctls 同步。使用不同的锁有很多优点,比如一些设置相关的 ioctls 花费的时间比较长,如果使用独立的锁,就不用等待设置操作的完成就可以执行,这个在网络摄像机驱动中很常见。当然,也可以完全由驱动本身去完成锁操作,这时可以设置所有的锁成员为NULL并实现一个驱动自己的锁。

如果使用旧的 videobuf,需要将 的锁传递给 videobuf queue 初始化函数,如果 videobuf 正在等待一帧数据的到达,此时会将锁暂时释放,等数据到达之后再次加锁,否则别的处理程序就无法访问。所以不推荐使用旧的 videobuf。如果是在 videobuf2 框架下,需要实现 与 回调函数去释放或者获取锁,如果使用了 ,可以使用 V4L2 提供的回调 帮助函数来完成加锁与解锁的操作,它们会使用 这个锁(此时一定要将该锁初始化)。

该结构体提供了一种简单的保存文件句柄特定数据的方法。 的使用者-v4l2 framework 可以通过检查 的 位来知道驱动是否使用 作为 指针,该标志位通过调用函数 来设置。

 

如以上代码所示,由于 函数可能会被多个应用 app 所调用,所以 fh 也会有多个,但是 永远指向最新的一个 ,通过这个 可以找到整个 链表中的所有元素。一些驱动需要在第一个文件句柄打开后以及最后一个文件句柄关闭前的时候做一些其它的工作,下面两个帮助函数可以检查 结构体是否只剩下一个 entry:

 

V4L2 events 提供一种通用的方法来传递 events 到用户空间,驱动程序必须使用 (设置 的 位)才能够实现对 V4L2 events 的支持。events 用类型和 ID 作为区分标识,没有使用到的 events 的ID就是0。

当用户订阅 event 时,用户空间会相应地为每个 event 分配一个 kevent 结构体(如果 elems 参数为0的话只有一个,不为0就按照指定的数量分配),所以每个 event 都有一个或多个属于自己的 kevent 结构体,这就保证了如果驱动短时间内生成了非常多的 events 也不会覆盖到其它的同类型 events,可以看作是分了好几个篮子来放不同类型的水果。event 结构体是 结构体的最后一个成员,以数组的形式存在,并且是一个柔性数组(),也就是说在分配 结构体空间的时候,events 并不占用空间,需要额外为指定数量的 events 分配空间,在使用的时候,完全可以按照数组的方式去对 kevent 进行寻址,很方便。

如果获得的 event 数量比 kevent 的还要多,那么旧的 events 就会被丢弃。可以设置结构体 的 回调函数(其实默认的函数就足够用了),它们会在 event 被捕获并且没有更多的空间来存放 event 时被调用。在 里面有一个很好的关于 的例子, 被作为回调函数使用。由于这两个函数可以在中断上下文被调用,因此必须得快速执行完毕并返回。

关于events的循环是一个比较有意思的操作,入队时:三个变量(first-下一个准备被dequeue的eventm,elems-总kevent数量,in_use-已经使用的kevent数量)

  1. 若elems == in_use,说明队列成员已经用完。
  2. 取出第一个kevent,从available队列中删掉,first指向数组的下一个成员,in_use --。
  3. 找到上一步中(first指向数组的下一个成员),将上一步(取出第一个kevent)的changes位进行合并赋值给前者。
    因为后者比前者更新,所以数值完全可以覆盖前者,同时又保留了前者的变化。
  4. 取出第in_use + first >= elems ? in_use + first - elems : in_use + first;个数组kevent项作为新的填充项。
  5. in_use ++

一些有用的函数:

 
 

参数允许驱动程序设置以下四个回调函数成员:

  • add:添加一个事件订阅时被调用
  • del:取消一个事件订阅时被调用
  • replace:event以新换旧,队列满时被调用,下同,常用于只有一个elems的情况下,拷贝kevent.u.ctrl项。
  • merge:将旧的event合并到新的event中,用于多个elems的情况下,只合并changes项,原因见上面event循环过程描述。

events 通过 poll 系统调用传递到用户空间,驱动可以将 作为 的参数。子设备可以直接通过 notify 函数向 发送 events(使用)。给出了如何使用event的实例。

注意事项:

  • 注意v4l2_event_subscribe的elems参数,如果为0,则内核就默认分配为1,否则按照指定的参数值分配。
  • 最好不要使用内核默认的v4l2_subscribed_event_ops,因为它的add函数会尝试在v4l2_ctrl里面查找相应id的ctrl,如果
    是自定义的event id的话,有可能找不到相关的ctrl项,这样的话用户空间的VIDIOC_SUBSCRIBE_EVENT就会返回失败。
  • 用户空间dqevent之后不必关心还回的操作,因为内核会自动获取用过的kevent,用柔性数组去管理而不是分散的链表。
  • 子设备可以通过v4l2_subdev_notify_event函数调用来入队一个event并通知v4l2设备的notify回调。
  • v4l2_event_queue函数会遍历video_device上面所有的v4l2_fh,将event入队到每一个fh的列表当中。fh由用户打开video
    设备节点的时候产生,每一个用户打开video节点时都会为其分配一个单独的v4l2_fh。
  • file->private永远指向最新的一个v4l2_fh,通过这个v4l2_fh可以找到整个v4l2_fh链表中的所有元素。
  • v4l2_fh_release函数会将所有挂载该fh上面的事件全部取消订阅。

写到这里,本文就算结束了,这部分会发现很多东西都是点到即撤,没有深入去解释,深入的这部分放在后面来完成,还有一个就是可能会感觉里面有很多东西看着可能知道是什么,但是反应到实际代码里面,实际应用里面就不知道是什么了,这个时候就必须结合代码来进行实际操作实验才能够确切了解。还有一种情况就是可能需求比较简单,一些特性永远用不到,这个时候也没关系,那就用到的时候再去翻看就好。

版权声明


相关文章:

  • java的jdk下载哪个版本最好2025-04-25 20:30:04
  • 哈夫曼树及哈夫曼编码2025-04-25 20:30:04
  • log4net appender2025-04-25 20:30:04
  • c++ bitset原理2025-04-25 20:30:04
  • 数据库有啥2025-04-25 20:30:04
  • 代码雨制作2025-04-25 20:30:04
  • eclipse打断点后怎么运行断点2025-04-25 20:30:04
  • 小红书算法工程师待遇2025-04-25 20:30:04
  • linux抓包教程2025-04-25 20:30:04
  • 三态门作用2025-04-25 20:30:04