全部博文(573)
分类: LINUX
2015-12-09 15:36:06
Linux 2.6中断下半部机制分析
摘要 本文主要从使用者的角度对Linux 2.6内核的下半部机制softirq、tasklet和workqueue进行分析,对于这三种机制在内核中的具体实现并未进行深入分析,倘若读者有兴趣了解,可以直接阅读Linux内核源代码的相关部分。
说明 本文档由流星自网上收集整理,按照自由软件开放源代码的精神发布,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。
目 录
1 概述
2 Linux 2.6内核中断下半部机制
2.1 softirq机制
2.2 tasklet机制
2.3 workqueue机制
3 几种下半部机制的比较
4 下半部机制的选择
5 Linux与NGSA的下半部机制比较
5.1 NGSA中断下半部机制分析
5.2 NGSA下半部机制缺陷分析
1 概述
中断服务程序往往都需要在CPU关中断的情况下运行,以避免中断嵌套而使控制复杂化,但是关中断的时间又不能太长,否则会造成中断信号的丢失。为此,在Linux中,将中断处理程序分为两部分,即上半部和下半部。上半部通常用于执行跟硬件关系密切的关键程序,这部分执行时间非常短,而且是在关中断的环境下运行的。对时间要求不是很严格,而且通常比较耗时的一些操作,则交给下半部来执行,这部分代码是在开中断中执行的。上半部处理硬件相关,称为硬件中断,这通常需要立即执行。下半部则可以延迟一定时间,在内核合适的时间段来执行程序,这就是我们这里要讨论的软中断。
本文以目前最新版本的Linux内核2.6.22为例,来讨论Linux的中断下半部机制。在2.6版本的内核中,下半部机制主要由softirq、tasklet和workqueue来实现,下面着重对这3种机制进行分析。
2 Linux 2.6内核中断下半部机制
老版本的Linux内核中,下半部是以一种叫做Bottom Half(简称为BH)的机制来实现的,最初它是借助中断向量来实现的,在系统中用一组(共32个)函数指针,分别表示32个中断向量,这种实现方式目前在2.4版本的内核中还可以看到它的身影。但是目前在2.6版本的内核中已经看不到它了。现在的Linux内核,一般以一种称为softirq的软中断机制来实现下半部。
2.1 softirq机制
原来的BH机制有两个明显的缺陷:一是系统中一次只能有一个CPU可以执行BH代码,二是BH函数不允许嵌套。这在单处理器系统中或许没关系,但在SMP系统中却是致命的缺陷。但是软中断机制就不一样了。Linux的softirq机制与SMP是紧密相连的,整个softirq机制的设计与实现始终贯穿着一个思想:“谁触发,谁执行”(Who marks, who runs),也就是说,每个CPU都单独负责它所触发的软中断,互不干扰。这就有效地利用了SMP系统的性能和特点,极大地提高了处理效率。
Linux在include/linux/interrupt.h中定义了一个softirq_action结构来描述一个softirq请求,如下所示:
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
其中,函数指针action指向软中断请求的服务函数,而data则指向由服务函数自行解释的参数数据。
基于上述结构,系统在kernel/softirq.c中定义了一个全局的softirq软中断向量表softirq_vec[32],对应32个softirq_action结构表示的软中断描述符。但实际上,Linux并没有使用到32个软中断向量,内核预定义了一些软中断向量的含义供我们使用:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
#ifdef CONFIG_HIGH_RES_TIMERS
HRTIMER_SOFTIRQ,
#endif
};
其中HI_SOFTIRQ用于实现高优先级的软中断,比如高优先级的hi_tasklet,而TASKLET_SOFTIRQ则用于实现诸如tasklet这样的一般性软中断。关于tasklet,我们在后面会进行介绍。我们不需要使用到32个软中断向量,事实上,内核预定义的软中断向量已经可以满足我们绝大多数应用的需求。其他向量保留给今后内核扩展使用,我们不应去使用它们。
要使用softirq,我们必须先初始化它。我们使用open_softirq()函数来开启一个指定的软中断向量nr,初始化nr对应的描述符softirq_vec[nr],设置所有CPU的软中断掩码的相应位为1。函数do_softirq()负责执行数组softirq_vec[32]中设置的软中断服务函数。每个CPU都是通过执行这个函数来执行软中断服务的。由于同一个CPU上的软中断服务例程不允许嵌套,因此,do_softirq()函数一开始就检查当前CPU是否已经正处在中断服务中,如果是则立即返回。在同一个CPU上,do_softirq()是串行执行的。
使用open_softirq()注册完一个软中断之后,我们需要触发它。内核使用函数raise_softirq()来触发一个软中断。对于一个指定的softirq来说,只会有一个处理函数,这个处理函数是所有CPU共享的。由于同一个softirq的处理函数可能在不同的CPU上同时执行,并产生竞争条件,处理函数本身的同步机制是非常重要的。激活一个软中断一般在中断的上半部中执行。当一个中断处理程序想要激活一个软中断时,raise_softirq()就会被调用。在后来的某个时刻,当do_softirq()在某个CPU上运行时,就会调用相关的软中断处理函数。
需要注意的是,在softirq机制中,还包含有一个很小的内核线程ksoftirqd。这是为了平衡系统负载而设的。试想,如果系统一直不断触发软中断请求,CPU就会不断地去处理软中断,因为至少每次时钟中断都会执行一次do_softirq()。这样一来,系统中其他重要任务不是要因长期得不到CPU而一直处于饥饿状态吗?在系统繁忙的时候,这个小小的内核线程就显得特别有用了,过多的软中断请求会被放到系统合适的时间段执行,给其他进程更多的执行机会。
在2.6内核中,do_softirq()被放到irq_exit()中执行。在中断上半部的处理中,只在irq_exit()中才调用do_softirq()进行软中断的处理,这非常有利于软中断模块的升级和移植。如果需要在我们的NGSA中移植Linux的软中断,这样的处理确实给了我们许多便利,因为我们只需要对我们的中断上半部的执行作很小的改动。如果在中断上半部有许多软中断调用的入口,那我们的移植岂不是会很痛苦?
可能有人会产生这样的疑问:系统中最多可以有32个softirq,那么这么多softirq,CPU是如何查找的呢?显然,我们在执行raise_softirq()对软中断进行触发时,必须要有一个很好的机制保证这个触发动作能够快速准确地进行。在Linux中,我们使用一种结构irq_cpustat_t来组织软中断。它在include/asm-xxx/hardirq.h中定义,其中xxx表示相应的处理器体系结构。比如对于PowerPC处理器,这个结构在include/asm-powerpc/hardirq.h中定义如下:
typedef struct{
unsigned int __softirq_pending; /* set_bit is used on this */
unsigned int __last_jiffy_stamp;
} ____cacheline_aligned irq_cpustat_t;
extern irq_cpustat_t irq_stat[]; /* defined in asm/hardirq.h */
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
其中,__softirq_pending成员使用bit map的方式来指示相应的softirq是否激活(即是否处于pending状态)。raise_softirq的主要工作就是在__softirq_pending中设置softirq的相应位,它的实现如下:
void fastcall raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
inline fastcall void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
if (!in_interrupt())
wakeup_softirqd(); /* 唤醒内核线程ksoftirqd */
}
#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
#define or_softirq_pending(x) (local_softirq_pending() |= (x))
#define local_softirq_pending() \
__IRQ_STAT(smp_processor_id(), __softirq_pending)
这里有一个宏函数local_softirq_pending(),其实就是用于返回当前cpu的相应irq_cpustat_t结构irq_stat[cpu]的__softirq_pending成员值。因此__raise_softirq_irqoff(nr)的作用就是把要触发的softirq在__softirq_pending中的相应位置1,在do_softirq()中则通过检查irq_stat[cpu]中相应的pending位是否设置来执行该softirq。
2.2 tasklet机制
tasklet实际上是一种较为特殊的软中断,软中断向量HI_SOFTIRQ和TASKLET_SOFTIRQ均是用tasklet机制来实现的。tasklet一词原意为“小片任务”,在这里指一小段可执行的代码。从某种程度上来讲,tasklet机制是Linux内核对BH机制的一种扩展,但是它和BH不同,不同的tasklet代码在同一时刻可以在多个CPU上并行执行。同时,它又和一般的softirq软中断不一样,一段tasklet代码在同一时刻只能在一个CPU上运行,而softirq中注册的软中断服务函数(即softirq_action结构中的action函数指针)在同一时刻可以被多个CPU并发地执行。
Linux内核用tasklet_struct结构来描述一个tasklet,该结构也是定义在include/linux/interrupt.h中的,如下所示:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
其中,各个成员的含义如下:
(1)next指针指向下一个tasklet,它用于将多个tasklet连接成一个单向循环链表。为此,内核还专门在softirq.c中定义了一个tasklet_head结构用来表示tasklet队列:
struct tasklet_head
{
struct tasklet_struct *list;
};
(2)state定义了tasklet的当前状态,这是一个32位无符号整数,不过目前只使用了bit 0和bit 1,bit 0为1表示tasklet已经被调度去执行了,而bit 1是专门为SMP系统设置的,为1时表示tasklet当前正在某个CPU上执行,这是为了防止多个CPU同时执行一个tasklet的情况。内核对这两个位的含义也进行了预定义:
enum
{
TASKLET_STATE_SCHED,/* Tasklet is scheduled for execution */
TASKLET_STATE_RUN /* Tasklet is running (SMP only) */
};
(3)count是一个原子计数,对tasklet的引用进行计数。需要注意的是,只有当count的值为0的时候,tasklet代码段才能执行,即这个时候该tasklet才是enable的;如果count值非0,则该tasklet是被禁止的(disable)。因此,在执行tasklet代码段之前,必须先检查其原子值count是否为0。
(4)func是一个函数指针,指向一个可执行的tasklet代码段,data是func函数的参数。
tasklet的使用其实很简单:先定义一个tasklet执行函数,然后用该函数去初始化一个tasklet描述符,接着使用tasklet的软中断触发函数去登记定义好的tasklet,以便让系统在适当的时候调度它运行。
内核为tasklet准备了两个宏定义用于声明并初始化一个tasklet描述符:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = {NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = {NULL, 0, ATOMIC_INIT(1), func, data }
从上面的定义可以看出,DECLARE_TASKLET在初始化一个tasklet之后,该tasklet是enable的,而DECLARE_TASKLET_DISABLED则用于初始化并disable一个tasklet。
tasklet的enable和disable操作总是成对出现,分别使用tasklet_enable()函数和tasklet_disable()函数实现。
初始化指定tasklet描述符的一般操作是用tasklet_init()来实现的,而tasklet_kill()则用来将一个tasklet杀死,即恢复到未调度的状态。如果tasklet还未执行完,内核会先等待它执行完毕。需要注意的是,由于调用该函数可能会导致休眠,所以禁止在中断上下文中调用它。
尽管tasklet机制是特定于软中断向量HI_SOFTIRQ和TASKLET_SOFTIRQ的一种实现,但是tasklet机制仍然属于softirq机制的整体框架范围内的,因此,它的设计与实现仍然必须坚持“谁触发,谁执行”的思想。为此,Linux为系统中的每一个CPU都定义了一个tasklet队列头部,来表示应该由各个CPU负责执行的tasklet队列。
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec) = {NULL };
static DEFINE_PER_CPU(structtasklet_head, tasklet_hi_vec) = { NULL };
其中,软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ的执行分别由各自的软中断服务程序tasklet_action()函数和tasklet_hi_action()函数来实现,这是在softirq_init()函数中指定的。前面讲到tasklet初始化完毕必须使用触发函数去登记,系统才能在适当的时候执行它们,这两个软中断的触发,分别是由函数tasklet_schedule()和tasklet_hi_schedule()来执行的。
2.3 workqueue机制
由于BH机制本身的局限性,早在2.0内核中就开始使用task queue(任务队列)机制对其进行了扩充。而在2.6内核中,则使用了另外一种机制workqueue(工作队列)来替换任务队列。
workqueue看起来有点儿类似于tasklet,它也允许内核代码请求在将来某个时间调用一个函数,所不同的是,workqueue是运行于一个特殊的内核进程上下文中的,而tasklet是运行于中断上下文中的,它的执行必须是短暂的,而且是原子态的。另外一个和tasklet不同的是,你可以请求工作队列函数被延后一个明确的时间间隔后再执行。workqueue通常用来处理不是很紧急的事件,因此它往往有比tasklet更高的执行周期,但不需要是原子操作,而且允许睡眠。
workqueue机制在include/linux/workqueue.h和kernel/workqueue.c中定义和实现。工作队列由workqueue_struct结构来维护,定义如下:
struct workqueue_struct {
struct cpu_workqueue_struct *cpu_wq;
struct list_head list;
const char *name;
int singlethread;
int freezeable; /* Freeze threads during suspend */
};
其中,cpu_workqueue_struct结构是针对每个CPU定义的。对于每一个CPU,内核都为它挂接一个工作队列,这样就可以将新的工作动态放入到不同的CPU下的工作队列中去,以此体现对“负载平衡”的支持(将work分配到各个CPU)。该结构定义如下:
struct cpu_workqueue_struct {
spinlock_t lock; /* 结构锁 */
struct list_head worklist; /* 工作列表 */
wait_queue_head_t more_work; /* 要进行处理的等待队列 */
struct work_struct *current_work; /* 处理完毕的等待队列 */
struct workqueue_struct *wq; /* 工作队列节点 */
struct task_struct *thread; /* 工作者线程指针 */
int run_depth; /* Detect run_workqueue() recursion depth */
} ____cacheline_aligned;
我们看到,在上面有一个work_struct结构,称作工作节点结构。要提交一个任务给一个工作队列,你必须填充一个工作节点。该结构定义如下:
struct work_struct {
atomic_long_t data;
#define WORK_STRUCT_PENDING 0 /* T if work item pending execution */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
struct list_head entry; /* 连接所有工作的链表节点 */
work_func_t func; /* 工作队列函数指针,指向具体需要处理的工作 */
};
为了方便对工作队列的维护,内核创建了一个工作队列链表,所有的工作队列都可以挂接到这个链表上来:
static LIST_HEAD(workqueues);
工作队列任务可以静态或动态地创建,它创建时需要填充一个work_struct结构。内核提供了一个宏定义用来方便地声明并初始化一个工作队列任务:
#define DECLARE_WORK(n, f) \
struct work_struct n = __WORK_INITIALIZER(n, f)
如果你想在运行时动态地初始化工作队列任务,或者重新建立一个工作任务结构,你需要下面2个接口:
#define PREPARE_WORK(_work, _func) \
do { \
(_work)->func = (_func); \
} while (0)
#define INIT_WORK(_work, _func) \
do { \
(_work)->data = (atomic_long_t) WORK_DATA_INIT(); \
INIT_LIST_HEAD(&(_work)->entry); \
PREPARE_WORK((_work), (_func)); \
} while (0)
其实只要用到INIT_WORK即可,PREPARE_WORK在INIT_WORK中调用。
工作队列的使用,其实也很简单。首先你需要建立一个工作队列,这一般通过函数create_workqueue(name)来实现,其中name是工作队列的名字。它会为每个CPU创建一个工作线程。当然,如果你觉得单线程用来处理你的工作已经足够,你也可以使用函数create_singlethread_workqueue(name)来创建单线程的工作队列。然后你需要把你所要做的工作提交给该工作队列。首先创建工作队列的任务,这在上面已经讲过了,接着使用函数queue_work(wq, work)把创建好的任务提交给工作队列,其中wq是要提交任务的工作队列,work是一个work_struct结构,就是你所要提交的任务。当你想要延后一段时间再提交你的任务,那么你可以使用queue_delayed_work(wq, work, delay)来提交,delay是你要延后的时间,以tick为单位,delay保证你的任务至少在指定的最小延迟之后才可能得到执行。当然了,由于delay任务的提交需要用到timer,因此你应当用另外一个结构delayed_work来替代work_struct,它实际上是在work_struct结构的基础上再增加一个timer而已:
struct delayed_work {
struct work_struct work;
struct timer_list timer;
};
相应地,初始化工作任务的接口应该改为DECLARE_DELAYED_WORK和INIT_DELAYED_WORK:
#define DECLARE_DELAYED_WORK(n, f) \
struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f)
#define PREPARE_DELAYED_WORK(_work, _func) \
PREPARE_WORK(&(_work)->work, (_func))
#define INIT_DELAYED_WORK(_work, _func) \
do { \
INIT_WORK(&(_work)->work, (_func)); \
init_timer(&(_work)->timer); \
} while (0)
工作队列中的任务由相关的工作线程执行,可能是在一个无法预期的时间段内执行,这要取决于系统的负载、中断等等因素,或者至少要在延迟一段时间以后执行。如果你的任务在一个工作队列中等待了无限长的时间都无法得到运行,那么你可以用下面的方法取消它:
int cancel_delayed_work(structdelayed_work *work);
如果当一个取消操作的调用返回时任务正在执行,那么这个任务将会继续执行下去,不会因为你的取消而终止,但是它不会再加入到工作队列中来。你可以使用下面的方法清除工作队列中的所有任务:
void flush_workqueue(struct workqueue_struct *wq);
如果工作队列中还有已经提交的任务还没执行完,那么内核会进入等待,直到所有提交的任务都执行完毕为止。flush_workqueue确保所有提交的任务都能执行完,这在设备驱动关闭时候的处理程序中特别有用。
当你用完了一个工作队列,你可以销毁它:
void destroy_workqueue(struct workqueue_struct *queue);
需要注意的是,destroy一个workqueue时,如果队列上还有未完成的任务,该函数首先会执行它们。destroy操作保证所有未处理的任务在工作队列被销毁之前都能顺利完成,所以你不必担心,当你想要销毁工作队列时,是否还有工作未完成。
由于工作队列运行在内核进程的上下文中,执行过程可能休眠,因此,工作队列处理的应该是那些不是很紧急的任务,通常在系统空闲时执行。
在workqueue的初始化函数中,定义了一个针对内核中所有线程可用的事件工作队列keventd_wq,其他内核线程建立的事件工作结构就都挂到该队列上来:
static struct workqueue_struct *keventd_wq __read_mostly;
void __init init_workqueues(void)
{
/* …… */
keventd_wq = create_workqueue("events");
/* …… */
}
使用内核提供的事件工作队列keventd_wq,事实上,你提交工作任务只需要使用schedule_work(work)或schedule_delayed_work(work)即可。
我们在编写设备驱动的时候,并非所有驱动程序都需要有自己的工作队列的。事实上,一个工作队列,在许多情况下,都不需要建立自己的工作队列。如果只偶尔提交任务给工作队列,简单地使用内核提供的共享的缺省工作队列,或许会更有效。不过,由于这个工作队列可能是由很多驱动程序共享的,任务可能会需要比较长的一段时间后才能开始执行。为了解决这个问题,工作函数的延迟应该保持最小,或者干脆不要。
对于工作队列,有必要补充说明的一点是,工作队列是在2.5内核开发版本中引入的用来替代任务队列的,它的数据结构比较复杂。或许到现在,你还对上面3个数据结构的关系感到混乱,理不出头绪来。在这里,我们把3个数据结构放在一起,对它们的关系进行一点说明。这3个数据结构的关系如下图所示:
从上面的图可以看出,位于最高一层的是工作者线程(worker_thread),就是我们在cpu_workqueue_struct结构中看到的thread成员。内核为每个CPU创建了一个工作者线程,关联一个cpu_workqueue_struct结构。每个工作者线程都是一个特定的内核线程,它们都会执行worker_thread()函数,它初始化完毕后,就开始执行一个死循环并休眠。当有任务提交给工作队列时,线程会被唤醒,以便执行这些任务,否则就继续休眠。
工作处于最底层,用work_struct结构来描述。这个结构体最重要的一个部分是一个指针,它指向一个函数,正是该函数负责处理需要延后执行的具体任务。工作被提交给工作队列后,实际上是提交给某个具体的工作者线程,然后该线程会被唤醒并执行提交的工作。
我们编写设备驱动的时候,通常大部分的驱动程序都是使用系统默认的工作者线程,它们使用起来简单、方便。但是在有些要求更严格的情况下,驱动程序需要使用自己的工作者线程。在这种情况下,系统允许驱动程序根据需要来创建工作者线程。也就是说,系统允许有多个类型的工作者线程存在,对于每种类型,系统在每个CPU上都有一个该类的工作者线程,对应于一个cpu_workqueue_struct结构。而workqueue_struct结构则用于表示给定类型的所有工作者线程。这样,在一个CPU上就可能存在多个工作队列,每一个工作队列维护一个cpu_workqueue_struct结构,也就是关联一种类型的工作者线程。
举个例子,我们的驱动在系统已有的默认工作者events类型(这是在init_workqueues中创建的系统默认工作者)的基础上,再自己加入一个falcon工作者类型:
struct workqueue_struct *mydriver_wq;
mydriver_wq = create_workqueue("falcon");
并且我们在一台具有4个处理器的计算机上工作。那么现在系统中就有4个events类型的线程和4个falcon类型的线程(相应的,就有8个cpu_workqueue_struct结构体,分别对应2种类型的工作者。同时,会有一个对应events类型的workqueue_struct和一个对应falcon类型的workqueue_struct。在提交工作的时候,我们的工作会提交给一个特殊的falcon线程,由它进行处理。
3 几种下半部机制的比较
Linux内核提供的几种下半部机制都用来推后执行你的工作,但是它们在使用上又有诸多差异,各自有不同的适用范围,使用时应该加以区分。
Linux 2.6内核提供的几种软中断机制都贯穿着“谁触发,谁执行”的思想,但是它们各自有不同的特点。softirq是整个软中断框架体系的核心,是最底层的一种机制,内核程序员很少直接使用它,大部分应用,我们只需要使用tasklet就行了。内核提供了32个softirq,但是仅仅使用了其中的几个。softirq是在编译期间静态分配的,它不像tasklet那样能够动态地创建和删除。softirq的软中断向量通过枚举对其含义进行预定义,这我们在前面2.1节中可以看到。其中,HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断都是通过tasklet来实现的,而且也是用得最普遍的软中断。在SMP系统中,不同的tasklet可以在多个CPU上并行执行,但是同一个tasklet在同一时刻只能在一个CPU上执行,这一点和softirq不一样,softirq都可以在多个CPU上同时执行,不管是不同的softirq还是同一softirq的不同实例。tasklet是利用软中断来实现的,它和softirq在本质上非常相近,行为表现也很接近,但是它的接口更简单,锁保护的要求也较低,因而也获得了更广泛的用途。通常,只有在那些执行频率很高和连续性要求很高的情况下,我们才需要使用softirq。
HI_SOFTIRQ和TASKLET_SOFTIRQ两个软中断依靠tasklet来实现,它们的差别仅仅在于HI_SOFTIRQ的优先级高于TASKLET_SOFTIRQ,因此它会优先执行。前者称为高优先级的tasklet,而后者则称为一般的tasklet。
workqueue是另外一种能够使你的工作延后执行的机制。实际上它不是一种软中断机制,因为它和前面的两种机制都不一样,softirq和tasklet通常运行于中断上下文中,而workqueue则运行于内核进程的上下文中。之所以把它们放在一起讨论,是因为它们都是用于把中断处理剩下的工作推后执行的一种下半部机制。工作队列可以把工作推后,交由一个内核线程来执行,因此它允许重新调度,甚至是睡眠,这在softirq和tasklet一般都是不允许的。如果你推后执行的任务不需要睡眠,那么你可以选择softirq或者tasklet,但是如果你需要一个可以重新调度的实体来执行你的下半部处理,你应该使用工作队列。这是一种唯一能在进程上下文中运行的下半部实现机制,也只有它才可以睡眠。除了上面所说的差异,工作队列和软中断还有一点明显的不同,就是它可以指定一个明确的时间间隔,用来告诉内核你的工作至少要延迟到指定的时间间隔之后才能开始执行。另外,工作队列在默认情况下和软中断一样,由最初提交工作的处理器负责执行延后的工作,但是它另外提供了一个接口queue_delayed_work_on(cpu, wq, work, delay)用来提交任务给一个特定的处理器(如果是使用默认的工作队列,相应的可以使用schedule_delayed_work_on(cpu, work, delay)来提交)。这一点,也是工作队列和软中断不一样的地方。
4 下半部机制的选择
在各种下半部实现机制之间作出选择是很重要的。在目前的2.6版本内核中,有3种可能的选择,就是本文讨论的3种机制:softirq,tasklet,以及工作队列。tasklet基于softirq实现,因此两者非常相近,而workqueue则不一样,它依靠内核线程来实现。
从设计的角度考虑,softirq提供的执行序列化保障是最少的,两个甚至更多个相同类别的softirq可能在不同的处理器上同时执行,因此你必须格外小心地采取一些步骤确保共享数据的安全。如果被考察的代码本身多线索化的工作就做得非常好,比如网络子系统,它完全使用单处理器变量,那么softirq就是一个非常好的选择。对于时间要求严格和执行频率很高的应用来说,它执行得也最快。如果代码多线索化考虑得并不充分,那么选择tasklet或许会更好一些,它的接口非常简单,而且由于同一类型的tasklet不能同时在多个CPU上执行,所以它实现起来也比较简单一些。驱动程序开发者应尽可能选择tasklet而非softirq。tasklet是有效的软中断,但是它不能并发运行。如果你可以确保软中断能够在多个处理器上安全运行,那么,你还是选择softirq比较合适。
当你需要将任务推迟到进程上下文中完成,毫无疑问,你只能使用工作队列。工作队列的开销太大,因为它牵涉到内核线程甚至是上下文切换。所以如果进程上下文不是必须的,更确切地说,如果不需要睡眠,那么工作队列就应该尽量避免,softirq和tasklet或许会更合适。这并不是说工作队列的工作效率就低,在大部分情况下,工作队列都能够提供足够的支持。只是,在诸如网络子系统这样的环境中,时常经历的每秒钟几千次的中断,那么采用softirq或者tasklet机制可能会更合适一些。
当然,从易于使用的角度来考虑,首推工作队列,其次才是tasklet。最后才是softirq,它必须静态地创建,并且需要慎重地考虑其实现,确保共享数据的安全。
一般来说,驱动程序编写者经常需要做两个选择:首先,你是不是需要一个可调度的实体来执行需要推后的工作——从根本上来讲,你有休眠的需要吗?如果有,那么,工作队列将是你唯一的选择。否则最好用tasklet。其次,如果你必须专注于性能的提高,那么就考虑用softirq吧。这个时候,你还要考虑的一点是,该如何采取有效的措施,才能保证共享数据的安全。