分类: LINUX
2015-11-17 10:46:53
中断处理程序是内核中很有用的——实际上也是必不可少的—部分。但是,由于本身存在一些局限,所以它只能完成整个中断处理流程的上半部分。这些局限包括:
1) 中断处理程序以异步方式执行并且它有可能会打断其它重要代码(甚至包括其它中断处理程序)的执行。因此,它们应该执行的越快越好。
2) 如果当前有一个中断处理程序正在执行,在最好的情况下,与该中断同级的其它中断会被屏蔽,在最坏的情况下,所有其它中断都会被屏蔽。因此,仍应该让它们执行的越快越好。
3) 由于中断处理程序往往需要对硬件进行操作,所以它们通常有很高的时限要求
4)中断处理程序不在进程上下文中运行,所以它们不能阻塞。
现在,为什么中断处理程序只能作为整个硬件中断处理流程一部分的原因就很明显了。我们必须有一个快速、异步、简单的处理程序负责对硬件做出迅速响应并完成那些时间要求很严格的操作。中断处理程序很适合于实现这些功能,可是,对于那些其它的、对时间要求相对宽松的任务,就应该推后到中断被激活以后再去做。
这样,整个中断处理流程就被分为了两个部分,或叫两半。第一个部分是中断处理程序(上半部),就像我们在上一章讨论的那样,内核通过对它的异步执行完成对硬件中断的即时响应。在本章中,我们要研究的是中断处理流程中的另外那一部分,下半部(bottom halves)。
7.1 下半部
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。对于在上半部和下半部之间划分工作,尽管不存在某种严格的规则,但还是有一些提示可供借鉴:
(1)如果一个任务对时间非常敏感,将其放在中断处理程序中执行。
(2)如果一个任务和硬件相关,将其放在中断处理程序中执行。
(3)如果一个任务要保证不被其它中断打断,将其放在中断处理程序中执行。(4)其它所有任务,考虑放在下半部执行。
理解为什么要让工作推后执行以及在什么时候推后执行非常关键。你希望尽量减少中断处理程序中需要完成的工作量,因为在它运行的时候当前的中断线会被屏蔽。更糟糕的是如果一个处理程序是SA_INTERRUPT类型,它执行的时候会禁止所有本地中断(而且把本地中断线全局地屏蔽掉)。而缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要。再加上中断处理程序要与其它程序——甚至是其它的中断处理程序——异步执行,所以很明显,我们必须尽力缩短中断处理程序的执行。解决的方法就是把一些工作放到以后去做。现在的问题是:下半部具体放到以后的什么时候去做呢?下半部并不需要指明一个确切时间,只要把这些任务推迟一点,让他们在系统不太繁忙并且中断恢复后执行就可以了。通常下半部在中断处理程序一返回就会马上执行。下半部执行的关键在于当它们运行的时候,允许响应所有中断。在2.6中,内核提供了三种不同形式的下半部实现机制:软中断、tasklet和工作队列。
7.2 软中断(kernel/softirq.c)
7.2.1 软中断的实现
软中断是在编译期间静态分配的。不像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];
每个注册的软中断都占据该数组中的一项。
(1)软中断处理程序:软中断处理程序action的函数原型如下:
void softirq_handler(struct softirq_action *)
当内核运行一个软中断处理程序(什么时候如何运行一个软中断程序)的时候,它就会执行这个action函数,其唯一的参数为指向相应的softirq_action结构体的指针。
(2)执行软中断:一个注册的软中断必须在被标记后才会执行。这被称作触发软中断(raising the softirq)。通常,中断处理程序会在返回前标记它的软中断,使其在稍后被执行。软中断被标记后,可以用softirq_pending()检查到这个标记并按照索引号将softirq_pending()的返回值的相应位置1。软中断在do_softirq()中执行。do_softirq()经过简化后的核心部分:
u32 pending = sofeirq_pending(cpu);
if(pending) {
struct softirq_action *h = softirq_vec;
softirq_pending(cpu) = 0;
do {
if(pending&1) h->action(h); //调用action函数
h ;
pending>>=1;
}while(pending);
}
7.2.2 使用软中断
(1)分配索引:在编译期间,可以通过<linux/interrupt.h>中定义的一个枚举类型来静态的声明软中断。
(2)注册处理程序:接着,在运行时通过调用open_softirq()注册软件中断处理程序,该函数有三个参数:索引号、处理函数和data域存放的数值。如 open_softirq(NET_TX_SOFTIRQ, net_tx_action,NULL);
(3)触发你的软中断:通过在枚举类型的列表中添加新项以及调用open_softirq()进行注册以后,新的软中断处理程序就能够运行。raise_softirq()函数可以将一个软中断设置为挂起状态,让他在下次调用do_softirq()函数时投入运行。一个例子:
raise_softirq(NET_TX_SOFTIRQ);
这会触发NET_TX_SOFTIRQ软中断。它的处理程序net_tx_action()就会在内核下一次执行软中断时投入运行。该函数在触发一个软中断前要禁止中断,触发后再恢复回原来的状态。在中断处理程序中触发软中断是最常见的形式。这样,内核在执行完中断处理程序后,马上就会调用do_softirq。
7.3 tasklet
tasklet是利用软中断实现的一种下半部机制。它和进程没有任何关系。它和软中断本质上很相似,行为表现也相近,但是,它的接口更简单,锁保护也要求较低。
7.3.1 tasklet的实现
因为tasklet是通过软中断实现的,所以它本身也是软中断。
(1)tasklet结构体:tasklet由tasklet_struct结构表示。每个结构体单独代表一个tasklet,它在
struct tasklet_struct {
struct task_struct *next; /*指向链表中的下一个tasklet */
unsigned long state; /* tasklet的状态 */
atomic_t count; /* 引用计数器 */
void (*func) (unsigned long); /* tasklet处理函数 */
unsigned long data; /*给tasklet处理函数的参数 */
};
结构体中的func成员是tasklet的处理程序,data是它唯一的参数。state成员只能在 0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。TASKLET_STATE_SCHED表明tasklet 已经被调度,正准备投入运行,TASKLET_STATE_RUN表示该tasklet正在运行。只有count为0时,tasklet才被激活,否则被允许,不允许执行。
(2)调度tasklet
tasklet是由tasklet_schedule()和tasklet_hi_schedule()函数进行调度的,它们接受一个指向tasklet_struct结构的指针作为参数。已调度的tasklet存放在两个链表中:tasklet_vec和task_hi_vec中。它们都是由tasklet_struct结构体构成的链表。链表中的每个tasklet_struct代表一个不同的tasklet。
7.3.2 使用tasklet
(1)声明自己的tasklet:可以静态创建,也可以动态创建,分别对应直接引用和间接引用。静态创建,使用下面
DECLARE_TASKLET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data);
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);运行代码实际上等价于:
struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0), my_tasklet_handler, dev };
这样就创建了一个名为my_tasklet,处理程序为tasklet_handler并且已经被激活的tasklet。
(2)编写自己的tasklet处理程序:必须符合规定的函数类型:
void tasklet_handler(unsigned long data)
(3)调度自己的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()去执行处理函数,即将下半部设置为待执行状态以便稍后执行。另外,在tasklet的tasklet_schedule()中,需要完成的动作之一便是唤起(触发)TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,说明tasklet仍然是基于软中断的。在进入do_softirq()之后,所做的工作仍然有所不同,不再论述。
7.4 工作队列
工作队列(work queue)是另外一种将工作推后执行的形式,他和我们前面讨论过的其他形式完全不同。工作队列可以把工作推后,交由一个内核线程去执行——这个下半部总是会在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是它允许重新调度甚至睡眠。
7.4.1 工作队列的实现
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的其他任务。它创建的这些内核线程被称作工作者线程。工作队列可以让驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个默认的工作者线程来处理这些工作。因此,工作队列最基本的表现形式就转变成了一个把需要推后执行的任务交给特定的通用线程这样一个接口。
(1)表示线程的数据结构
工作者线程用workqueue_struct结构表示:
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
const char *name;
struct list_head list;
};
该结构内是一个由cpu_workqueue_struct结构组成的数组,定义在kernel/workqueue.c中,数组的每一项对应一个系统中的处理器。每个工作者线程都对应这样的cpu_workqueue_struct结构体。cpu_workqueue_struct是 kernel/workqueue.c中的核心数据结构:
struct cpu_workqueue_struct {
spinlock_t lock; /* 锁定以便保护该结构体 */
long romove_sequeue; /* 最近一个被加上的(下一个要运行
的) */
long insert_sequeue; /*下一个要加上的 */
wait_queue_head_t more_work;
wait_queue_head_t work_done;
struct workqueue_struct *wq; /* 有关联的
workqueue_struct结构 */
task_t *thread; /* 有关联的线程 */
int run_depth; /* run_workqueue()循环深度 */
};
由此可以看出,每个工作者线程类型关联一个自己的workqueue_struct。在该结构体里面,给每个线程分配一个 cpu_workqueue_struct,因而也就是给每个处理器分配一个,因为每个处理器都有一个该类型的线程。
(2)表示工作的数据结构
所有工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数(worker_thread)开始休眠。当有操作被插入到队列的时候,线程就会被唤醒,以便执行这些操作。工作用
unsigned long pending; /* 这个工作是否正在等待处理 */
struct list_head entry; /* l连接所有工作的链表 */
void (* func) (void *); /* 处理函数 */
void *wq_data; /* 内部使用 */
struct timer_list timer; /* 延迟的工作队列所用到的定时器 */
};
这些结构体被连接成链表,在每个处理器的每种类型的队列都对应这样一个链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。当工作完毕时,他会将相应的work_struct对象从链表中移去。
7.4.2 使用工作队列
(1)创建推后的工作
首先要做的是实际创建一些需要推后执行的工作。可以通过DECLARE_WORK在编译时静态的创建该结构体:
DECLARE_WORK(name, void (*func) (void *), void *data);
这样就会静态的创建一个名为name,处理函数为func,参数为data的work_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马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。
7.5 下半部机制的选择
在各种不同的下半部实现机制之间做出选择是很重要的。在当前的2.6版内核中,有三种可能的选择:软中断、tasklets和工作队列。Tasklets基于软中断实现,所以两者很相近。工作队列机制与它们完全不同,它靠内核线程实现。
如果你需要把任务推后到进程上下文中完成,那么在这三者中就只能选择工作队列了。如果进程上下文并不是必须的条件——明确点说,就是如果并不需要睡眠——软中断和tasklets可能更合适。工作队列造成的开销最大,因为它要牵扯到内核线程甚至是上下文切换。这并不是说工作队列的效率低,如果每秒钟有几千次中断,就像网络子系统时常经历的那样,那么采用其它的机制可能更合适一些。不管怎么说,针对大部分情况,工作队列都能提供足够的支持。
如果讲到易于使用,工作队列就当仁不让了。使用缺省的events队列简直不费吹灰之力。下来要数tasklets,它的接口也很简单。位居末座的是软中断,它必须静态创建。
表6.3是对三种下半部接口的比较
表6.3 对下半部的比较
下半部 |
上下文 |
顺序执行保障 |
软中断 |
中断 |
没有 |
Tasklet |
中断 |
同类型不能同时执行 |
工作队列 |
进程 |
没有(和线程一样被调度) |
简单的说,一般的驱动程序的编写者需要做两个选择。首先,你是不是需要一个可调度的实体来执行需要推后完成的工作——你有任何休眠的需要吗?要是有,工作队列就是你的唯一选择。否则最好用tasklet。要是必须专注于性能的提高,那么就考虑软中断吧。