一、中断处理机制
中断是一个随机事件,因此如果关中断的时间过长,CPU就不能及时的响应其他的中断请求,从而造成中断的丢失。因此,Linux内核的目标就是尽可能快的处理完中断请求,尽可能的把更多的处理向后推迟。因此,内核把中断分为了两部分:上半部和下半部,上半部(就是中断服务程序),下半部(就是一些内核函数)留着稍后处理。首先,一个快速的”上半部”来处理硬件发出的请求,它必须在一个新的中断产生之前终止。其次,“下半部”运行时是允许中断请求的,而上半部运行时是关中断的。这是二者的主要区别。
1.中断“下半部”
内核到底什么时候执行“下半部”,以何种方式组织“下半部”?
下半部实现机制中,在老的内核中,这个机制叫做bottom-half(BH),但是linux的这种BH机制有两个缺点:
(1) 在任一时刻,系统中只能有一个CPU可以执行BH代码,以防止两个或多个CPU同时来执行BH函数而相互干扰,因此BH代码的执行是严格“串行化的”
(2) BH函数不允许嵌套
而2.6内核以后,“下半部”机制处理机制有了很大的发展和改进:
(1) 软中断请求机制
(2) 小任务机制
(3) 工作队列机制
2.软中断
整个softirq机制的设计与实现中自始自终贯彻了一个思想:“谁触发,谁执行”,即触发软中断的那个CPU负责执行它所触发的中断,而且每个CPU都有它自己的软中断触发与控制机制。
2.小任务机制
tasklet机制是一种较为特殊的软中断。
特点:
(1)与一般的软中断不同,某一段tasklet代码在某个时刻只能在一个CPU上运行,而不像一般的软中断服务函数(即softirq_action结构体中的action函数指针)那样------在同一时刻可以被多个CPU并发地执行。
(2)与BH机制不同,不同的tasklet代码在同一时刻可以在多个CPU上并发地执行,而不像BH机制那样必须严格地串行化执行。
linux中用tasklet_struct来描述一个tasklet,每个结构体代表一个独立的小任务
注:tasklet_struct结构体中atomic_t count成员表示对这个tasklet的引用计数值,只有当count为0时,tasklet代码段才能执行,如果count非零,则这个tasklet是被禁止的。任何想要执行一个tasklet代码段的人都首先必须检查其count成员是否为0.
linux在interrupt.h头文件中定义了两个宏来定义tasklet_struct 结构变量的辅助宏:
DECLARE_TASKLET(name,func,data)和DECLARE_TASKLET_DISABLED(name,func,data),显然,DECLARE_TASKLET宏在初始化时是被使能的,因为其count为0,而DECLARE_TASKLET_DISABLED是被禁止的,因为其count为1.
小任务机制总结:
(1) 声明和使用小任务大多数情况下,为了控制一个常用的硬件设备,小任务机制是实现下半部的最佳选择。小任务可以动态创建,使用方便,执行起来也比较快,也可以静态创建。选择哪种方式取决于到底是想对小任务进行直接引用还是间接引用,如果准备静态地创建一个小任务(直接引用),使用下面两个宏中的一个:
DECLARE_TASKLET(name,func,data)
DECLARE_TASKLET_DISABLED(name,func,data)
(2) 编写自己的小任务处理程序,小任务处理程序必须符合如下的函数类型:
void tasklet_handler(unsigned long data)
由于小任务不能睡眠,因此不能在小任务中使用信号量或者其他产生阻塞的函数。但是小任务运行时可以响应中断
(3) 调度自己的小任务通过调用tasklet_schedule()函数
3.工作队列
工作队列是另外一种将工作推后执行的形式,它和tasklet有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说这个下半部可以在进程的上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的是工作队列允许被重新调度甚至是睡眠。
那么,什么情况下使用工作队列,什么情况下使用tasklet呢?
如果推后执行的任务需要睡眠,那么就选择工作队列;
如果推后执行的任务不需要睡眠,那么就选择tasklet。
另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也可以使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的IO操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。
我们把推后执行的任务叫做工作,描述工作的结构体是work_struct,这些工作以队列结构组织成工作队列,描述工作队列的结构体为workqueue_struct,而工作线程就是负责执行工作队列中的工作,系统默认的工作线程是events,自己也可以创建自己的工作者线程。
二、工作队列详解
Linux中workqueue机制
为什么需要workqueue?Linux中的workqueue机制就是为了简化内核线程的创建。通过调用workqueue的接口就能创建内核线程。并且可以根据当前系统CPU的个数创建线程的数量,使得线程处理的事务能够并行化。
Linux内核创建工作队列时:
__alloc_workqueue_key函数会调用kthread_create来进行线程的创建
这个线程以wq为数据执行rescuer_thread这个函数。
触摸屏驱动工作队列:
没有使用系统默认的工作队列keventd_wq(这个工作队列是在Linux系统初始化的时候就创建的),因为默认的工作队列存在性能问题。
在初始化workqueue过程中,内核需要初始化内核线程来处理工作队列,注册的内核线程工作比较简单,就是不断的扫描对应的cpu_workqueue_stuct中的任务队列,从中获取一个有效任务,然后执行该任务。
内核线程的创建调用create_singlethread_workqueue(name)来完成,该函数会创建一个工作队列,并且在内部创建了一个内核线程。
工作队列的创建方式有两种,如下图所示:
第二种方式中,会创建自己的工作队列,优点是性能高,会创建内核线程来处理工作函数。