linux driver 中断下半部实现方法总结

来源:互联网 发布:mac上好用的手绘软件 编辑:程序博客网 时间:2024/05/17 23:14

下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作.

1)如果一个任务对时间非常敏感,将其放在中断处理程序中执行。

2)如果一个任务和硬件相关,将其放在中断处理程序中执行。

3)如果一个任务要保证不被其它中断打断,将其放在中断处理程序中执行。

4)其它所有任务,考虑放在下半部执行

 

    在中断处理程序运行的时候,当前的中断线会被屏蔽,如果一个处理程序是SA_INTERRUPT类型,它执行的时候会禁止所有本地中断(而且把本地中断线全局屏蔽掉),下半部执行的关键在于当它们运行的时候,允许响应所有中断。

    实现中断下半部有如下几种方法:

1.软中断

软中断是用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果,tasklet也是基于软中断实现的。

异步通知所基于的信号也类似于中断。

硬中断是外部设备对CPU的中断

软中断通常是硬中断服务程序对内核的中断。

信号则是由内核(或其它进程)对某个进程的中断。

 

 软中断是在编译期间静态分配的。不像tasklet那样能被动态的注册或去除。软中断由softirq_action结构表示,它定义在<linux/interrupt.h>中:

struct softirq_action {

             void( *action)(struct softirq_action *);         

/*待执行的函数*/

         Void*date;             /传递给函数的参数*/

                                  } ;

kernel/softirq.c中定义了一个包含有32个该结构体的数组。     

static strcut softirq_action softirq_vec[32]; 每个注册的软中断都占据该数组中的一项。

一个软中断不会抢占另外一个软中断,实际上,唯一可以抢占软中断的是中断处理程序,不过,其它的软中断——甚至是相同类型的软中断——可以在其它处理器上同时执行。

软中断保留给系统中对时间要求最严格以及最重要的下半部使用。内核定时器和tasklets都是建立在软中断上的,如果你想加入一个新的软中断,首先要想想为什么用tasklet实现不了,tasklet可以动态生成,由于它们对加锁的要求不高

软中断处理程序的执行的时候,允许响应中断,但自己不能睡眠。

 

2. Tasklet

tasklet是利用软中断实现的一种下半部机制。它和进程没有任何关系。它和软中断本质上很相似,行为表现也相近,但是,它的接口更简单,锁保护也要求较低。

通常你应该用tasklet,软中断一般用的很少,它只在那些执行频率很高和连续性要求很高的情况下才需要,而tasklet却有更广泛的用途.

因为是靠软件中断实现,所以tasklet不能睡眠,这意味着你不能在tasklet中使用信号量或者其它什么阻塞式的函数。

 

通过调用task_schedule()函数并传递给它相应的tasklet_struct的指针,该tasklet就会被调度以便执行。

tasklet_schedule(&my_tasklet); 

软中断和tasklet的异同:

在前期准备工作上,首先要给软中断分配索引,而tasklet则要用宏对处理程序声明。在给软中断分配索引后,还要通过open_softirq()函数来注册处理程序。这样看来,tasklet是一步到位,直接到了处理函数,而软中断需要做更多工作。接下来软中断要等待触发(raise_softirq()raise_softirq_irqoff,tasklet则是等待tasklet_schedule()tasklet_hi_schedule()对其进行调度。两者虽然在命名上不同,但殊途同归,最终的结果都是等待do_softirq()去执行处理函数,即将下半部设置为待执行状态以便稍后执行。另外,在tasklettasklet_schedule()中,需要完成的动作之一便是唤起(触发)TASKLET_SOFTIRQHI_SOFTIRQ软中断,说明tasklet仍然是基于软中断的。在进入do_softirq()之后,所做的工作仍然有所不同,不再论述。

 

3. 工作队列

工作队列(work queue)是另外一种将工作推后执行的形式,他和我们前面讨论过的其他形式完全不同。工作队列可以把工作推后,交由一个内核线程去执行——这个下半部总是会在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势,最重要的是工作队列允许重新调度甚至是睡眠。

如果你需要用一个可以重新调度的实体来执行你的下半部处理,你应该使用工作队列,它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠,这意味着你在你需要获得大量的内存时,在你需要获取信号量时,在你需要执行阻塞式的IO操作时,它都会非常有用,如果你不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet吧!

(1)创建推后的工作

首先要做的是实际创建一些需要推后执行的工作。可以通过DECLARE_WORK在编译时静态的创建该结构体:

DECLARE_WORK(name, void (*func) (void *), void *data);

这样就会静态的创建一个名为name,处理函数为func,参数为datawork_struct结构体。也可以在运行时通过指针创建一个工作:

INIT_WORK(struct work_struct *work, void (*func)(void *), void   *data);

这样就动态的初始化了一个由work指向的工作。

(2)工作队列的处理函数

原型是:void   work_handler(void   *data)

这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中,默认情况下,允许响应中断,并且不持有任何锁,如果需要,函数可以睡眠,注意的是,尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射,通常在系统调用发生时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。

(3)对工作进行调度

现在工作已经创建,我们可以调度它了,要把给定工作的处理函数提交给默认的events工作线程,只需调用: schedule_work(&work); work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。

(4)刷新操作

刷新工作队列的函数就是确保在卸载模块之前,要确保一些操作已经执行完毕了,该函数如下:

Void flush_scheduled_work(void);

该函数会一直等待,直到队列中所有对象都被执行以后才返回,在等待所以待处理的工作执行的时候,该函数会进入休眠状态,所以只能在进程上下文中使用它。

 

 

    下半部之间的选择

1 从设计的角度考虑

软中断提供的执行序列化的保障最少,这就要求软中断必须采取一些步骤确保共享数据的安全。如果被考察的代码本身多线程化的工作就做得非常好,软中断就很好,对于时间要求严格和执行频率很高的话,它执行的也快。如果代码本身多线程化的工作就做得不充分,就选择tasklet比较好,由于两个同种类型的tasklet不能同时执行,实现起来也很简单一些。

2 如果你需要把任务推到进程上下文中完成,只能选择工作队列。

如果不需要睡眠,那么软中断和工作队列就更合适。工作队列造成的开销最大,因为他要牵扯到内核线程甚至是上下文切换。

3 说到易用性,工作队列最好,使用缺省的events队列简直不费吹灰之力。接下来就tasklet。他的的接口很简单,最后才是软中断,它必须静态创建。

 

    在下半部之间加锁

使用tasklet的一个好处是在于它自己负责执行的序列化保障,两个相同类型的tasklet不允许同时执行,即使在不同的处理器上也不行,意味着你无须考虑相同类型的tasklet内部的同步问题。当然,tasklet之间的同步(两个不同类型的tasklet共享同一数据时)需要正确使用锁机制。

因为软中断根本不保障执行序列化,(即使相同类型的软中断也有可能有两个实例在同时执行)所以所有的共享数据都需要合适的锁。

如果进程上下文和一个下半部共享数据,在访问这些数据之前,你需要禁止下半部的处理并得到锁的使用权,所做的这些是为了本地和SMP的保护并且防止死锁的出现。

如果中断上下文和一个下半部共享数据,在访问数据之前,你需要禁止中断并得到锁的使用权,所做的这些是为了本地和SMP的保护并且防止死锁的出现。

任何在工作队列中被共享的数据也需要使用锁机制,其中有关锁的要点和在一般内核代码中没什么区别,因为工作队列本来就是在进程上下文中执行的.

    禁止下半部

一般单纯禁止下半部的处理是不够的,为了保证共享数据的安全,更常见的做法是先得到一个锁然后在禁止下半部的处理,驱动程序中通常使用的都是这种方法。