v4l2驱动编写篇第六B--流输入输出
在本系列文章的上一期中,我们讨论了如何通过read()和write()的方式实现视频帧的传输,这样的实现可以完成基本的工作,却并不是普便上用来实现视频输入输出大家偏爱的方法。为了实现最高性能和最好的信息传输,视频驱动应该支持V4L2 流输入输出。
使用read()和write()方法,每一帧都要通过I/O操作在用户和内核空间之间拷贝数据。然而, 当使用流输入输出的方式时,这种情况就不会发生。替代的方案是用户与内核空间之间交换缓冲区的指针,这些缓冲区将被映射到应用的地址空间,这也就使零帧复制数成为可能。有两种流输入输出缓冲区:
* 内存映谢缓冲区(memory-mapped buffers) (typeV4L2_MEMORY_MMAP) 是在内核空间开辟的;应用通过themmap()系统调用将其映谢到地址空间。这些缓冲区可以是大而相临DMA缓冲区,通过vmalloc()创建的虚拟缓冲区,或者(如果硬件支持的话)直接在设备的输入输出存储器中开辟的。
* 用户空间缓冲区 (V4L2_MEMORY_USERPTR) 是在用户空间的应用中开辟的。很明显,在这种情况下,是不需要mmap()调用的,但驱动在有效地支持用户空间缓冲区上的工作将会更难一些。
注意:驱动支持流输入输出的方式并非必需,即便做了实现,驱动也不必两种缓冲区类型都做处理。一个灵活的驱动可以支持更多的应用。在实际应用中,似乎多数应用都是使用内存映射缓冲区的。同时使用两种缓冲区是不可能的。
现在,我们将要探索一下支持流输入输出的众多而邋遢的细节。任何Video4Linux2驱动的作者都要了解这部分API。然而值得指出的是,有一个更高层次的API,它能够帮助驱动作者写流驱动。当底层设备可以支持分散/聚集I/O的时候,这一层(称为 video-buf)可以使事情变得容易。关于 video-buf API我们将在将来的某期讨论。
支持流输入输出的驱动应该通知应用这一事实,方法是在vidioc_querycap()方法中设置V4L2_CAP_STREAMING标签。注意:并没有办法来描述支持的是哪一种缓冲区,那是后话。
1、v4l2_buffer结构体
当流输入输出是有效的,帧是以v4l2_buffer的形式在应用和驱动之间传输的。这个结构体是一个复杂的东西,要用很长的时间才能描述完。一个很好的出发点是要注意的,一个缓冲区可以有三种基本的状态:
* 在驱动的传入队列中:如果驱动用它做什么有用事的话,应用就可以把缓冲区放在这个队列里。对于一个视频捕获设备,传入队列(传入到应用程序)中的缓冲区是空的,等待驱动向其内填入视频数据。对于输入设备来讲,这些缓冲区内是要送入设备的帧数据。
* 在驱动的传出队列中.这些缓冲区已经经过驱动的处理,正等待应用来使用。对于捕获设备而言,传出缓冲区内是新的帧数据;对出输出设备而言,这个缓冲区是空的。
* 不在上述两个队列里.在这种状态时,缓冲区是由用户空间拥有的,驱动无法访问。这是应用可以对缓冲区进行操作的唯一的时间。我们称其为用户空间状态。
这些状态和造成他们之间传输的操作都放在一起,在下图中示出:
实际上的v4l2_buffer结构体如下:
struct v4l2_buffer
{
__u32 index;
enum v4l2_buf_type type;
__u32 bytesused;
__u32 flags;
enum v4l2_field field;
struct timeval timestamp;
struct v4l2_timecode timecode;
__u32 sequence;
/* memory location */
enum v4l2_memory memory;
union {
__u32 offset;
unsigned long userptr;
} m;
__u32 length;
__u32 input;
__u32 reserved;
};
index 字段是鉴别缓冲区的序号;它只在内存映射缓冲区中使用。与其它可以在V4L2接口中枚举的对象一样,内存映射缓冲区的index从0开始,依次递增。
type字段描述的是缓冲区的类型,通常是V4L2_BUF_TYPE_VIDEO_CAPTURE 或V4L2_BUF_TYPE_VIDEO_OUTPUT.
缓冲区的大小是论长度给定的,单位为byte。缓冲区中的图像数据大小可以在bytesused字段中找到。很明显,bytesused<=length。对于捕获设备而言,驱动会设置bytesused; 对输出设备而言,应用必须设置这个字段。
field字段描述的是图像存在缓冲区的那一个区域。这些区域在这系统文章中的part5a 中可以找到。
timestamp(时间戳)字段,对于输入设备来说,代表帧捕获的时间.对输出设备来说,在没有到达时间戳所代表的时间前,驱动不可以把帧发送出去;时间戳值为0代表越快越好。驱动会把时间戳设为帧的第一个字节传送到设备的时间,或者说是驱动所能达到的最接近的时间。timecode 字段可以用来存放时间编码,对于视频编辑类的应用是非常有用的。
驱动对传过设备的帧维护了一个递增的计数; 每一帧传送时,它都会在sequence字段中存入现行序号。对于输入设备来讲,应用可以观察这一字段来检测帧。
memory 字段表示的是缓冲是内存映射的还是用户空间的。对于内存映谢的缓冲区,m.offset 描述的是缓冲区的位置。 规范将它描述为“从设备存储器开发的缓冲区偏移”,但其实质却是一个 magic cookie,应用可以将其传给mmap(),以确定那一个缓冲区被映射了。然而对于用户空间缓冲区而言,m.userptr是缓冲区的用户空间地址。
input 字段可以用来快速切换捕获设备的输入 – 当然,这要得设备支持帧与帧的快速切换才来。reserved字段应置0.
最后,还有几个标签定义:
* V4L2_BUF_FLAG_MAPPED 暗示缓冲区己映射到用户空间。它只应用于内存映射缓冲区。
* V4L2_BUF_FLAG_QUEUED: the buffer is in the driver’s incoming queue.
* V4L2_BUF_FLAG_DONE: 缓冲区在驱动的传出队列.
* V4L2_BUF_FLAG_KEYFRAME: 缓冲区包含一个关键帧,它在压缩流中是非常有用的.
* V4L2_BUF_FLAG_PFRAME 和V4L2_BUF_FLAG_BFRAME 也是应用于压缩流中;他们代表的是预测的或者说是差分的帧.
* V4L2_BUF_FLAG_TIMECODE: timecode 字段有效.
* V4L2_BUF_FLAG_INPUT: input字段有效.
2、缓冲区设定:请求缓冲区
一旦流应用已经完成了基本的设置,它将转去执行组织I/O缓冲区的任务。第一步就是使用VIDIOC_REQBUFS ioctl()来建立一组缓冲区,它将由V4L2转换成对驱动的vidioc_reqbufs()方法的调用。
int (*vidioc_reqbufs) (struct file *file, void *private_data, struct v4l2_requestbuffers *req);
我们要关注的所以内容都在v4l2_requestbuffers结构体中,如下所示:
struct v4l2_requestbuffers
{
__u32 count;
enum v4l2_buf_type type;
enum v4l2_memory memory;
__u32 reserved[2];
};
type 字段描述的是完成的I/O操作的类型。通常它的值要么是视频获得设备的V4L2_BUF_TYPE_VIDEO_CAPTURE ,要么是输出设备的V4L2_BUF_TYPE_VIDEO_OUTPUT.也有其它的类型,但在这里我们不予讨论。
如果应用想要使用内存映谢的缓冲区,它将会把memory字段置为 V4L2_MEMORY_MMAP,count置为它想要使用的缓冲区的数目。如果驱动不支持内存映射,它就该返回-EINVAL。否则它将在内部开辟请求的缓冲区并返回0。返回之后,应用就会认为缓冲区是存在的,所以任何可以失败的任务都在在这个阶段进行处理 (比如说内存开辟)
注意:驱动并不一定要开辟与请求的一样数目的缓冲区。在很多情况下,有一个最小值缓冲区数的有意义。如果应用请求的比最小值小,可能实际返回的要多于请求的。以笔者的经验,mplayer要用两个缓冲区,如果用户空间速度慢下来的话,这将很容易溢出(因而丢失帧)。通过强制一个大一点的最小缓冲区数(可调整的模块参数),cafe_ccic驱动可以使流输入输出通道更加强壮。count 字段应设为方法返回前实际开辟的缓冲区数。
应用可以通设置count字段为0的方式来释放掉所有已存在的缓冲区。在这种情况下,驱动必须在释放缓冲前停止所有的DMA操作,否则会发生非常严重的事情。如果缓冲区已映射到用户空间,则释放缓冲区是不可能的。
相反,如果用的是用户空间缓冲区,则有意义的字段只有缓冲区的type和memory 字段V4L2_MEMORY_USERPTR这个值。应用不需要指定它想用的缓冲区的数目。因为内存是在用户空间开辟的,驱动无须操心。如果驱动支持用户空间缓冲区,它只须注意应用会使用这一特性,返 回0就可以了,否则通常的-EINVAL 返回值会被调用到.
VIDIOC_REQBUFS 命令是应用得知驱动支持的流输入输出缓冲区类型的唯一方法。
3、将缓冲区映射到用户空间
如果使用了用户空间,在应用向传入队列放置缓冲区之前,驱动看不到任何缓冲区相关的调用。内存映射缓冲区需要更多的设置。应用通常会查看每一个开辟了的缓冲区,并将其映射到自己地址空间。第一站是VIDIOC_QUERYBUF命令,它将转换成驱动中的 vidioc_querybuf() 方法:
int (*vidioc_querybuf)(struct file *file, void *private_data, truct v4l2_buffer *buf);
进入这个方法时,buf 字段中要设置的字段有type (在缓冲区开辟时,它将被检查是否与给定的类型相同)和index,它们可以确定一个特定的缓冲区。驱动要保证index有意义,并添充buf中的其余字段。通常来说,驱动内部存储着一个v4l2_buffer结构体的数组, 所以vidioc_querybuf()方法的核心只是一个结构体的赋值。
应用访问内存映射缓冲区的唯一方法就是将其映射到它们的地址空间,所以 vidioc_querybuf() 调用后面通常会跟着一个驱动的mmap()方法 -这个方法,大家要记住,是存储在相关设备的video_device结构体中的fops字段中的。 设备如何处理mmap() 将依赖于内核中缓冲区是如何设置的。如果缓冲区可以在remap_pfn_range() 或remap_vmalloc_range()之前映射,那就应该在这个时间来做。对于内核空间的缓冲区,页也可以在页错误时通过常规的使用 nopage()方法的方式单独被映射,对于需要的人来说,在Linux Device Drivers 可以找到一个关于handlingmmap()的一个很好的讨论。
mmap() 被调用时,传递的VMA结构应该含有vm_pgoff字段中的某个缓冲区的地址-当然是经过PAGE_SHIFT右移过的。特别是,它应该是你的驱动对于 VIDIOC_QUERYBUF调用返回值的楄移。请您遍历缓冲区列表,并确保传入地址匹配其中之一。视频驱动程序不应该是一个可以让恶意程序映射内存的 任意区域手段。
你所提供的偏移值可几乎所有的东西.有些驱动只是返回 (index<<PAGE_SHIFT),意思是说传入的vm_pgoff字段应该正好是缓冲区索引。有一件事你不可以做的就是把缓冲区的内 核实际地址存储到offset字段,把内核地址泄露给用户空间永远也不是一个好注意。
当用户空间映射缓冲区时,驱动应该在相关的v4l2_buffer结构体中调置 V4L2_BUF_FLAG_MAPPED标签.它也必须设定open() 和close() VMA 操作,这样它才可以跟踪映谢了缓冲区的进程数。只要缓冲区在哪里映射了,它就不可以在内核里释放掉。如果一个或多个缓冲区的映谢计算降为0,驱动就应该停 止正在进行的输入输出,因为没有进程要用它。
4、流输入输出
到现为止,我们己经看了很多设置,却没有传输过一帧的数据,我们离这步己经很近了,但在些之前还有一个步 骤要做。当应用通过 VIDIOC_REQBUFS获得了缓冲区后,那个缓冲区都处于用户空间状态。如果他们是用户空间缓冲区,他们甚至还并不真的存在。在应用可以开始流的输入输出之前,它必须至少将一个缓冲区放到驱动传入队列,对于输出设备,那些缓冲区当然还要先添完有效的帧数据。
要把一个缓冲区加入队列,应用首先要发出一个VIDIOC_QBUF ioctl()调用,这个调用V4L2会映射为对驱动的vidioc_qbuf()方法的调用。
int (*vidioc_qbuf) (struct file *file, void *private_data, struct v4l2_buffer *buf);
对于内存映射缓冲而言,还是那样,只有buf的type和 index字段有效. 驱动只能进行一些明显的检查(type 和index 有意义,缓冲区还没有在驱动的队列里,缓冲区己映射等。),把缓冲区放在传入队列里 (设置V4L2_BUF_FLAG_QUEUED 标签),并返回.
在这点上,用户空间缓冲区可能会更加复杂,因为驱动可能从来没看过缓冲区的样子。使用这个方法时,允许应用在每次向队列传入缓冲区时,传递不同的地址,所以驱动不能提前做任何的设置。如果你的驱动通过内核空间缓冲区将帧送回,它只须记录一下应用提供的用户空间地址就可以了。然而如果你正尝试将通过 DMA直接将数据传送到用户空间,那将会非常的具有挑战性。
要想把数据直接传送到用户空间,驱必须先fault in缓冲区的所以的页,并将其锁定。get_user_pages() 可以做这件事。注意这个函数会开辟很大的内存空间和硬盘I/O-它可能会卡住很长时间。你得注意要保证重要的驱动功能不能在 get_user_pages()时停止,因为它可能停止很长时间等待许多视频帧通过。
下面就是要告诉设备把图像数据传到用户空间缓冲区(或是相反的方向)了。缓冲区在物理上不是相临的,相反,它会被分散成很多很多单独的4096字节的页(在大部分架构上如此)。很明显,设备得可以实现分散/聚集DMA操作才行。如果设备立即传输一个完整的视频帧,它就需要接受一个包含很多页的分散列表(scatterlist)。一个 16位格式的VGA解决方案的图像需要150个页,随着图像大小的增加,分散列表的大小也会增加。V4L2规范说:
如果硬件需要,驱动要与物理内存交换内存页,以产生相临的内存区。这对内核子系统的虚拟内存中的应用来说是很明显的。
然而,笔者却不推荐驱动作者尝试这种深层的虚拟内存技巧。有一个更有前途的方法就是要求用户空间缓冲区分配成大的hugetlb页,但是现在的驱动不那么做。
如果你的设备传输的是小图像(如USB摄像头),直接从DMA到用户空间的设定就简单些。在任何情况下,面对支持直接I/O到用户空间缓冲的改变,驱动作者都应该做到以下两点:(1)确定的确值得这么大的麻烦,因为应用更趋向于使用内存映射缓冲区。(2)使用video buf层,它可以帮你解决一些痛苦的难 题 。
5、队列上的缓冲区传输
一旦流输入输出开始,驱动就要从它的传入队列里抓取缓冲区,让设备更快地实现传送请求,然后把缓冲区移动到传出队列。转输开始时,缓冲区标签也要相应调整。像序号和时间戳这样的字段必需在这个时候添充。最后,应用会在传出队列中认领缓冲区,让它变回为用户空 间状态。这是VIDIOC_DQBUF的工作,它最终变为如下调用:
int (*vidioc_dqbuf) (struct file *file, void *private_data, struct v4l2_buffer *buf);
这里,驱动会从传出队列中移除第一个缓冲区,把相关的信息存入buf,通常,如果传出队列是空的,这个调用会处于阻塞状态直到有缓冲区可用。然而V4L2是用来处理非阻塞I/O的,所以如果视频设备是以O_NONBLOCK方式打开的,在队列为空的情况下驱动就该返回-EAGAIN .当然,这个要求也暗示驱动必须为流I/O支持poll();
6、控制流传输
剩下最后的一个步骤实际上就是告诉设备开始流输入输出操作。这个任务的 Video4Linux2驱动方法是:
int (*vidioc_streamon) (struct file *file, void *private_data, enum v4l2_buf_type type);
int (*vidioc_streamoff)(struct file *file, void *private_data, enum v4l2_buf_type type);
对vidioc_streamon()的调用应该在检查类型有意义之后让设备开始工作。如查需要的话,驱动可以请求传入队列中有一定数目的缓冲区后再开始流的转输.
当应用关闭时,它应发出一个对vidioc_streamoff()的调用,这个调用要停止设备。驱动还应从传入传出队列中移除所有的缓冲区,使它们都处于用户空间状态。当然,驱动必须准备好,应用可能在没有停流转输的情况下关闭设备。