linux底半部机制在视频采集驱动中的应用

时间:2024-01-06 13:22:50

最近在做一个arm+linux平台的视频驱动。本来这个驱动应该是做板子的第三方提供的,结果对方软件实力很差,自己做不了这个东西,外包给了一个暑期兼职的在读博士。学生嘛,只做过实验,没做过产品,给出的东西自然和产品的实际需要相去十万八千里。博士同学给我们的驱动甚至是从未编译过的,充满了"unsigned void "这样可笑的语法错误,不得已跑到北平追着那厮现场联调,最后所谓的“调通”,也仅仅是寄存器配置正确而已。

视频驱动的输出是连续的帧数据,必须要有完善的缓冲、跳帧和同步阻塞机制。而这些机制在博士同学给我的驱动程序中全部付之阙如。在这份驱动程序中,是没有阻塞机制的,在采集一帧图像期间连续两次调用采集API,得到的会是同一帧图像的buffer。如果你写一个线程循环采集,那么这个线程必定会耗尽CPU所有空闲的资源。诸如此类的问题还有一大堆,这样的驱动肯定是没法用到实际产品中的。

不得已只好自己操刀上阵改写这个驱动。我的思路是这样的:

驱动程序流程

1、在驱动中创建两个内核链表,一个用来缓冲新采集到的图像buffer,而另一个用来缓冲废弃的图像buffer。

2、从预先分配的buffer中取出三个buffer,第一个作为前帧buffer,第二个作为当前buffer设置为dma目的地址,第三个作为后备buffer。

3、当新图像到来时,将dma目的地址更新为后备buffer,然后将前帧buffer链入新图像链表尾端。

4、将当前buffer的值赋给前帧buffer,将后备buffer的值赋给当前buffer,再判断废弃图像链表中是否有buffer存在。

5、如果废弃链表不为空,则从废弃图像链表取出一个buffer赋给后备buffer;如果废弃图像链表中没有buffer可用,则从新图像链表中获取一个buffer赋给后备buffer,也就是发生了跳帧。

备注:链表中buffer的进出使用信号量进行增减计数

客户程序流程

1、判断新图像链表是否为空。

2、不为空则从新图像链表获取一个buffer,即采集到一帧图像;为空则由于计数信号量为0,导致线程挂起等到新buffer链入链表。

3、使用采集到的图像。

4、将使用过的图像buffer送入废弃链表。

由于使用信号量对链表进行了计数,当链表为空时,从链表获取buffer的线程可以挂起,也就达到了阻塞同步的目的。新图像被链表串联在一起,达到了缓冲的目的。当废弃链表中的buffer耗尽时,从新图像链表获取buffer进行采集,达到了跳帧的目的。这样的流程设计似乎是没有问题,但是实现起来就遇到了些麻烦。这个机制中,链表的更新是在中断和线程并发执行的,有很大的几率发生访问冲突,这就要求引入互斥机制。然而中断是不可阻塞或挂起的,互斥量不能在中断中使用。基于循环等待的自旋锁,在这种场合又会引起死锁(当用户线程获得自旋锁之后,如果有中断发生,线程会被中断抢占;此时中断试图获取自旋锁,必然由于获取不到而循环等待;此时线程被中断抢占,无法释放手中的自旋锁,就会造成死锁)。这就要求引入一种机制,既要能够被阻塞,又要能在中断isr返回之后立即执行。将链表的更新操作全部安排到这种机制中,就能够实现与用户线程的互斥。

linux提供了一种称为“底半部(bottom half)”的机制。ISR是软件代码,由硬件设备触发。底半部机制类似中断,却是由软件代码触发的。常用的底半部机制有软中断、基于软中断的tasklet(小任务)和基于内核线程的工作队列等。软中断类似于中断,也是不可阻塞的,显然软中断和基于软中断的tasklet等机制无法满足我的需求。而工作队列是可以阻塞的,又可以在中断中触发执行,看起来这是一个不错的选择。

于是我将原先规划给中断的链表操作全部移到了工作队列绑定的函数中,每次中断触发时,isr将后备buffer的值置为dma目的地址,然后触发工作队列。工作队列绑定的函数根据链表当前的情况更新当前buffer和后备buffer的地址,供isr下一次执行时使用。当然这个过程是加了互斥机制的,在用户线程调用的驱动api中也加了同样的互斥机制,这样二者就不会发生冲突。

在我看来,所谓“底半部”机制这种说法很有故弄玄虚的味道。在任何处理器或操作系统中,中断都是一种不可阻塞且必须快速返回的机制,遇到长时间的运算或必须阻塞的情形,都会采用由中断触发某种线程或任务的方法。例如在DSP/BIOS或ucOS-II中,一般会为类似的工作预留一个高优先级的线程,平时被信号或邮箱阻塞,在中断isr中发送信号或消息来触发这个线程。虽然采用了类似的处理方法,但是并没有什么“底半部”之类的术语。可能是linux内核太过庞杂了吧,还严格区分了内核态和用户态,不能像准操作系统内核那样使用普通线程实现这种机制,而内核线程等机制调用又太过复杂,必须进行专门的封装才能得到易于使用的API。按照我的理解,所谓的“底半部”,其实就是这种“封装”而已。