kernel-buttom-halves

第6章下半部和推后执行的工作
在上一章中,我们研究了内核为处理中断而提供的中断处理程序机制。中断处理程序是内核中很有用的——实际上也是必不可少的——部分。但是,由于本身存在一些局限,所以它只能完成整个中断处理流程的上半部分。这些局限包括:
n          中断处理程序以异步方式执行并且它有可能会打断其它重要代码(甚至包括其它中断处理程序)的执行。因此,它们应该执行的越快越好。
n          如果当前有一个中断处理程序正在执行,在最好的情况下,与该中断同级的其它中断会被屏蔽,在最坏的情况下,所有其它中断都会被屏蔽。因此,仍应该让它们执行的越快越好。
n          由于中断处理程序往往需要对硬件进行操作,所以它们通常有很高的时限要求。
n          中断处理程序不在进程上下文中运行,所以它们不能阻塞。
现在,为什么中断处理程序只能作为整个硬件中断处理流程一部分的原因就很明显了。我们必须有一个快速、异步、简单的处理程序负责对硬件做出迅速响应并完成那些时间要求很严格的操作。中断处理程序很适合于实现这些功能,可是, 对于那些其它的、对时间要求相对宽松的任务, 应该推后到中断被激活以后再去做。
这样,整个中断处理流程就被分为了两个部分,或叫两 。第一个部分是中断处理程序(上半部),就像我们在上一章讨论的那样,内核通过对它的异步执行完成对硬件中断的即时响应。在本章中,我们要研究的是中断处理流程中的另外那一部分, 下半部( bottom halves
下半部
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。在理想的情况下,最好 中断处理程序将所有工作都交给下半部分执行全部操作,因为我们希望在中断处理程序中完成的工作越少越好(也就是越快越好)。我们期望中断处理程序能够尽可能快的返回。
但是,中断处理程序注定要完成一部分工作。例如,中断处理程序几乎都需要通过操作硬件对中断的 到达进行确认。有时它还会从硬件拷贝数据。因为这些工作对时间非常敏感,所以只能靠中断处理程序自己去完成。
剩下的几乎所有其它工作都是下半部执行的目标。例如,如果你在上半部中把数据从硬件拷贝到了内存,那么当然应该在下半部中处理它们。遗憾的是,并不存在严格明确的规定来说明到底什么任务应该在哪个部分中完成——如何做决定完全取决于驱动程序开发者自己的判断。尽管在理论上不存在什么 错误,但轻率的实现效果 总是往往不很理想。记住,中断处理程序会异步执行,并且在最好的情况下它也会锁定当前的中断线。因此将中断处理程序持续执行的时间缩短到最小程度非常重要。对于在上半部和下半部之间划分工作,尽管不存在某种严格的规则,但还是有一些提示可供借鉴:
n          如果一个任务对时间非常敏感,将其放在中断处理程序中执行。
n          如果一个任务和硬件相关,将其放在中断处理程序中执行。
n          如果一个任务要保证不被其它中断(特别是相同的中断)打断,将其放在中断处理程序中执行。
n          其它所有任务,考虑放置在下半部执行。
当你开始尝试写自己的驱动程序的时候,读一下别人的中断处理程序和相应的下半部可能会你受益匪浅。在决定怎样把你的中断处理流程中的工作划分到上半部和下半部中去的时候,问问自己什么必须放进上半部而什么可以放进下半部。通常,中断处理程序要执行的越快越好。
为什么要用下半部?
理解为什么要让工作推后执行以及在什么时候推后执行非常关键。你希望尽量减少中断处理程序中需要完成的工作量,因为在它运行的时候当前的中断线会被屏蔽。更糟糕的是如果一个处理程序是SA_INTERRUPT类型,它执行的时候会 屏蔽掉禁止所有本地中断(而且附加把本地中断线全局屏蔽掉)。而缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要。再加上中断处理程序要与其它程序——甚至是其它的中断处理程序——异步执行,所以很明显,我们必须尽力缩短中断处理程序的执行。解决的方法就是把一些工作放到以后去做。
但具体放到以后的什么时候去做呢?在这里, 以后仅仅用来强调 不是马上而已,理解这一点相当重要。下半部并不需要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太繁忙并且中断恢复后执行就可以了。通常下半部在中断处理程序一返回就会马上运行。 下半部执行的关键在于当它们运行的时候,允许响应所有的中断。
不仅仅是Linux,许多操作系统也把处理硬件中断的过程分为两个部分。上半部分简单快速,执行的时候 禁止一些或者全部中断。下半部分(无论具体如何实现)稍后执行, 而且执行期间可以响应所有的中断。这种设计可使系统处于中断屏蔽状态的时间尽可能的短,以此来提高系统的响应能力。
下半部的环境
不像 半部分只能通过中断处理程序实现 的上半部,下半部可以通过多种机制实现。这些用来实现下半部的机制分别由不同的接口和子系统组成。上一章,我们了解到实现中断处理程序的方法只有一种 译者注 (译者注:在 Linux 中,由于上半部从来都只能通过中断处理程序实现,所以它和中断处理程序可以说是等价的。) 但在本章中你会发现,实现一个下半部会有许多不同的方法。实际上,在Linux发展的过程中曾经出现过多种下半部机制。让人倍受困扰的是,其中不少机制名字起得很相像,甚至还有一些机制名字起得辞不达意。
在本章中,我们将要讨论2.6内核中的下半部机制是如何设计和实现的。 同时我们也会讨论怎么在你自己编写的内核代码中使用它们。而那些过去使用的、已经废除了有一段时间的机制,由于曾经闻名遐尔,所以在必要的时候我们还是会有所提及。
最早的Linux只提供 “bottom half” 一种机制用于实现下半部。这个名字在那个时候毫无异义,因为当时它是将工作推后的唯一方法。这种机制也被称为“BH”,我们现在也这么叫它,以避免和 “下半部”这个通用词汇混淆。像过往的那段美好岁月中的许多东西一样,BH接口也非常简单。它提供了一个静态创建、由32个bottom half组成的链表。 上半部通过一个32位整数的一位来标 出哪个bottom half可以执行。每个BH都在全局范围内进行同步。即使分属于不同的处理器,也不允许任何两个bottom half同时执行。这种机制使用方便却不够灵活;简单却有性能瓶颈。
不久,内核开发者们就引入了 任务队列( task queues 机制来实现工作的推后 执行,并用它来替代BH机制。内核为此定义了一组队列。其中每个队列都包含一个 等待调用的函数组成链表 ,上面全都是等待被调用的函数。根据其所处队列的位置,这些函数会在某时刻被执行。驱动程序可以把它们自己的下半部注册到合适的队列上去。这种机制表现的还不错,但仍不够灵活,没法替代整个BH接口。对于一些性能要求较高的子系统,像网络部分,它也不能胜任。
在2.3这个开发版本中,内核开发者引入了软中断(softirqs) (译着 2 这里的软中断与第 4 章实现系统调用所提到的软中断(准确说该叫它软件中断)指的是不同的概念)和tasklet。如果无须考虑和过去开发的驱动程序兼容 这个问题的话,软中断和tasklet可以完全替代BH接口 [1]。软中断是一组静态定义的下半部接口,有32个,可以在所有处理器上同时执行——即使两个类型相同也可以。 t Tasklet名字起得很糟糕,让人费解 [2], 它们是一种 基于软中断实现的灵活性强、动态创建的下半部实现机制 ,基于软中断实现。两个不同类型的tasklets可以在不同的处理器上同时执行,但类型相同的tasklet不能同时执行。Tasklet其实是一种在性能和易用性之间寻求平衡的产物。对于大部分下半部处理来说,用tasklet就足够了。 如果 软中断用于像网络这样对性能要求非常高的情况才需要使用软中断。可是,使用软中断需要特别小心,因为两个相同的软中断有可能同时被执行。此外,软中断还必须在编译期间就进行静态注册。与此相反,tasklet可以通过代码进行动态注册。
       有些人被这些概念彻底搞糊涂了,他们把所有的下半部都当成是软件产生的中断或软中断。换句话说就是他们把软中断机制和下半部统统都叫软中断。别管他们好了。
       在开发2.5版本的内核时,BH接口被最终弃置荒野了,所有的BH使用者必须转而使用其它下半部接口。此外,任务队列接口也被工作队列接口取代了。工作队列是一种简单但很有用的方法,它们先对要推后执行的工作排队,稍后在进程上下文中执行它们。
       综上所述,在2.6这个当前版本中,内核提供了三种不同形式的下半部实现机制:软中断、tasklets和工作队列。内核过去曾经用过的BH和任务队列接口,现在已经被湮没在记忆中了。
内核定时器
另外一个可以用于将工作推后执行的机制是内核定时器。不像本章到目前为止介绍到的所有这些机制,内核定时器把操作推迟到某个确定的时间段之后执行。也就是说,尽管本章讨论的其它机制可以把操作推后到除了现在以外的任何时间进行,但是当你必须保证在一个确定的时间段过去以后再运行时,你应该使用内核定时器。
较之本章讨论到的这些机制,定时器还有一些其它功能。有关定时器的详细内容在第九章“定时器和时间管理”一章中讨论。
混乱的下半部概念
这些东西确实把人搅得云里雾里,但它们其实只不过是一些起名的问题,我们再来梳理一遍。
“下半部(bottom half)”是一个操作系统通用词汇,用于指代中断处理流程中推后执行的那一部分。在Linux中,这个词目前确实就是这个含义。所有用于实现将工作推后执行的内核机制都被称为“下半部机制”。一些人错误 把所有的下半部机制都叫做“软中断”,真是他们是在自寻烦恼。
“下半部”这个词也指代Linux最早提供的那种将工作推后执行的实现机制。由于该机制也被叫做“BH”,所以,我们就使用它的这个名字,而让 “下半部”这个词仍然保持它通常的含义。BH机制很早以前就被反对使用了,在2.5版内核中,它被完全去除了。
当前,有三种机制可以用来实现将工作推后执行:软中断、tasklet和工作队列。Tasklets通过软中断实现,而工作队列与它们完全不同。表6.1揭示了下半部机制的演化历程。
1
2 它们和进程没有一点关系。可以把它们当作一种简单的、便于使用的软中断。
表6.1 下半部状态
下半部机制
状态
BH
在2.5中去除
任务队列(Task queues)
在2.5中去除
软中断(Softirq)
从2.3开始引入
Tasklet
从2.3开始引入
工作队列(Work queues)
从2.5开始引入
 
在搞清楚了这些混乱的命名之后,让我们开始具体研究各个机制。
软中断
我们的讨论从 软中断机制这个实际的下半部实现——软中断——方法开始。软中断使用的比较少;而tasklets是下半部更常用的一种形式。但是,由于tasklets是通过软中断实现的,所以我们先来研究软中断。软中断的代码位于kernel/softirq.c文件中。
软中断的实现
软中断是在编译期间静态分配的。它不像tasklets那样能被动态的注册或去除。软中断由softirq_action结构 表示,它定义在<linux/interrupt.h>中:
/ *
*本结构 代表一个软中断项
*/
struct softirq_action
{
       void (*action) (struct softirq_action *);              /* 待执行的函数 */
       void *data;                           /* 传给函数的参数 */
};
 
kernel/softirq.c中定义了一个包含有32个该结构体的数组。
 
static struct softirq_action softirq_vec[32];
 
每个被注册的软中断都占据该数组的一项。因此最多可能有32个软中断。注意,这是一个定值——注册的软中断数目的最大值没法动态改变。在当前版本的内核中,这32个项中只用到了6个 [3]
3 大部分驱动程序都使用 tasklets 来实现它们的下半部。我们将在下一节看到, Tasklets 是用软中断实现的。
软中断处理程序
软中断处理程序action的函数原型如下:
 
void softirq_handler(struct softirq_action *)
 
当内核运行一个软中断处理程序的时候,它就会执行这个action函数,其唯一的参数为指向相应softirq_action结构体的指针。例如,如果my_softirq指向softirq_vec数组的某项,那么内核会用如下的方式调用软中断处理程序中的函数:
 
my_softirq->action(my_softirq);
 
当你看到内核把整个结构体都传递给软中断处理程序而不是仅仅传data的时候,你可能会很吃惊。这个小技巧可以保证将来在结构体中加入新的域时,无须对所有的软中断处理程序都进行变动。如果需要,软中断处理程序可以方便的解析它的参数,从数据成员中提取数值。
       一个软中断不会抢占另外一个软中断。实际上,唯一可以抢占软中断的是中断处理程序。不过,其它的软中断——甚至是相同类型的软中断——可以在其它处理器上同时执行。
执行软中断
一个注册的软中断必须在被标记后才会执行。这被称作 触发软中断( raising the softirq 。通常,中断处理程序会在返回前标记它的软中断,使其在稍后被执行。于是,在合适的时刻,该软中断就会运行。在下列地方,待处理的软中断会被检查和执行:
n          在处理完一个硬件中断以后。
n          在ksoftirqd内核线程中。
n          在那些显式检查和执行待处理的软中断的代码中,如网络子系统中。
不管是用什么办法被唤起,软中断都要在do_softirq()中执行。该函数很简单。如果有待处理的软中断,do_softirq()会循环遍历每一个,调用它们的处理程序。让我们观察一下do_softirq()经过简化的核心部分:
 
u32 pending = softirq_pending(cpu);
 
if (pending) {
       struct softirq_action *h = softirq_vec;
      
       softirq_pending(cpu) = 0;
       do {
              if (pending &1)
       h->action(h);
              bh++;
              pending >> = 1;
}while(pending);
}
 
以上摘录的是软中断处理的核心部分。它检查并执行所有待处理的软中断,具体要做的包括:
n          用局部变量pending保存softirq_pending()函数的返回值。它是待处理的软中断的32位位图——如果第n位被设置为1,那么第n位对应类型的软中断等待处理。
n          现在待处理的软中断位图已经被保存,可以将实际的软中断位图清零了 [4]
n          将指针h指向softirq_vec的第一项。
n          如果pending的第一位被置为1,h->action(h)被调用。
n          指针加1,所以现在它指向softirq_vec数组的第二项。
n          bitmask_pending右移一位。这样会丢弃第一位,然后让其它各位依次向右移动一个位置。于是,原来的第二位现在就在第一位的位置上了(依次类推)。
n          现在指针h指向数组的第二项,pending_mask的第二位现在也到了第一位上。重复执行上面的步骤。
n          一直重复下去,直到pending变为0,这表明已经没有待处理的软中断了,我们的任务也就完成了。注意,这种检查足以保证h总指向softirq_vec的有效项,因为pending最多只可能设置32位,循环最多也只能执行32次。
使用软中断
软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个子系统——网络和SCSI——直接使用了软中断。此外,内核定时器和tasklets都是建立在软中断上的。如果你想加入一个新的软中断,首先应该问问自己为什么用tasklet实现不了。Tasklets可以动态生成,由于它们对加锁的要求不高,所以使用起来也很方便,而且它们的性能也非常不错。当然,对于时间 要求严格并能自己高效完成加锁工作的应用,软中断会是正确的选择。
4 实际上在执行此步操作时需要屏蔽本地中断。但在这个简化版本中被省略了。如果中断不被屏蔽,在保存位图和清除它的间隙,可能会有一个新的软中断被唤起(它自然也就会等待处理)。这可能会造成对此待处理的位进行不应该的清零。要求严格并能自己高效的完成加锁工作的应用,软中断会是正确的选择。
分配索引
在编译期间,你通过<linux/interrupt.h>中定义的一个枚举类型来静态地声明软中断。内核用这些从0开始的索引来表示一种相对优先级。索引号小的软中断在索引号大的软中断之前执行。
 
表6.2 下半部控制方法列表
Tasklet
优先级
软中断描述
HI_SOFTIRQ
0
优先级高的tasklets
TIMER_SOFTIRQ
1
定时器的下半部
NET_TX_SOFTIRQ
2
发送网络数据包
NET_RX_SOFTIRQ
3
接收网络数据包
SCSI_SOFTIRQ
4
SCSI的下半部
TASKLET_SOFTIRQ
5
Tasklets
 
建立一个新的软中断必须在此枚举类型中加入新的项。而加入时,你不能像在其它地方一样,简单的把新项加到列表的末尾。相反,你必须根据你希望赋予它的优先级来决定加入的位置。习惯上,HI_SOFTIRQ通常作为第一项,而TASKLET_SOFTIRQ作为最后一项。新项可能插在网络相关的那些项之后,TASKLET_SOFTIRQ之前。
注册你的处理程序
接着,在 运行时 执行期通过调用open_softirq()注册软中断处理程序,该函数有三个参数:软中断的索引号、处理函数和data域存放的数值。例如网络子系统,通过以下方式注册自己的软中断:
 
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
 
软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。在一个处理程序运行的时候,当前处理器上的软中断被 屏蔽禁止。但其它的处理器可以执行别的软中断。实际上,如果一个软中断在它被执行的同时再次被触发了,那么另外一个处理器可以同时运行其处理程序。这意味着任何共享数据——甚至是仅在软中断处理程序内部使用的全局变量——都需要严格的锁保护(下面两个章节会讨论)。这点很重要,它也是为什么tasklets更受青睐的原因。单纯的禁止你的软中断处理程序被同时执行很难算得上理想。如果仅仅通过互斥的加锁方式来防止它自身的并发执行,那么使用软中断就没有任何意义。因此,大部分软中断处理程序都通过采取 使用与处理器相关的数据(仅属于某一个处理器的数据,因此根本不需要加锁)和其它一些技巧来避免显式加锁,从而提供更出色的性能。
 
触发你的软中断
通过在枚举类型的列表中添加新项以及调用open_softirt()进行注册以后,新的软中断处理程序就能够运行。raise_softirq()函数可以将一个软中断设置为 待执行挂起状态,让它在下次do_softirq()函数被调用时投入运行。举个例子,网络子系统可能会调用
rase_softirq(NET_TX_SOFTIRQ);
这会触发NET_TX_SOFTIRQ软中断。它的处理程序net_tx_action()就会在内核下一次执行软中断时投入运行。该函数在触发一个软中断之前先要 屏蔽禁止中断,触发后再恢复回原来的状态。如果中断本来就已经被屏蔽禁止了,那么可以调用一个另外的函数raise_softirq_irqoff(),这会带来一些优化效果。如:
/*
* 中断已经被禁止 interrupt must already off;
*/
raise_softirq_irqoff(NET_TX_SOFTIRQ);
在中断处理程序中触发软中断是最常见的形式。在这种情况下,中断处理程序执行对硬件设备的相关操作, 然后触发相应的软中断, 后退出。内核在执行完中断处理程序以后,马上就会调用do_softirq()函数。于是软中断开始执行中断处理程序留给它去完成的剩余任务。在这个例子中,“上半部”和“下半部”名字中的含义一目了然。
Tasklets
Tasklets是利用软中断实现的一种下半部机制。我们早就提到过,它和进程没有任何关系。Tasklets和软中断在本质上很相似,行为表现也相近;但是,它的接口更简单,锁保护也要求较低。
     选择到底是用软中断还是tasklets其实很简单:通常你应该用tasklet。就像我们在前面看到的,软中断的使用者屈指可数。它只在那些执行频率很高和连续性要求很高的情况下才需要。而tasklet却有更广泛的用途。大多数场合下用tasklets效果都不错,而且它们还非常容易使用。
Tasklets的实现
因为tasklet是通过软中断实现的,所以它们本身也 软中断。前面讨论过了,tasklets由两类软中断代表:HI_SOFTIRQ和TASKLET。这两者之间唯一的实际区别在于HI_SOFTIRQ类型的软中断先于TASKLET_SOFTIRQ类型的软中断执行。
Tasklet结构体
Tasklets由tasklist_struct结构 表示。每个结构体单独代表一个tasklet ,它在<linux/interrupt.h>中定义:
Sstruct tasklet_struct {

      struct tasklet_struct *next;  /*指向链表中的下一个结构体*/
      unsigned long state;              /* tasklet的状态 */
      atomic_t count;      /* 引用计数器 */
      void (*func) (unsigned long);              /* tasklet处理函数 */
      unsigned long data;      /* 给tasklet处理函数的参数 */
};
 
结构体中的func成员是tasklet的处理程序(像软中断中的action一样),data是它唯一的参数。
state成员只能在 0 、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。TASKLET_STATE_SCHED表明tasklet已被调度,正准备投入运行,TASKLET_STATE_RUN表明该tasklet正在运行。TASKLET_STATE_RUN只有在多处理器的系统上才会作为一种优化被使用,单处理器系统什么时候都清楚一个tasklet是不是正在运行(它要么就是当前正在执行的代码,要么不是)。
count成员是tasklet的引用计数器。如果它不为 0tasklet被 屏蔽禁止,不允许执行;只有它为零,tasklet才被激活, 并且在被设置为 待执行挂起状态时,该tasklet才能够执行。
调度tasklets
Tasklets经由tasklet_schedule()和tasklet_hi_schedule()函数进行调度,它们接受一个指向tasklet_struct结构的指针作为参数。两个函数非常类似(区别在于一个使用TASKLET_SOFTIRQ而另外一个用HI_SOFTIRQ)。在下一节我们将仔细研究怎么编写和使用tasklet。现在,让我们先考察一下tasklet_schedule()的细节:
n          检查tasklet的状态是否为TASKLET_STATE_SCHED。如果是,说明tasklet已经被调度过了 译者注 译者注:有可能是一个 tasklet 已经被调度过但还没来得及执行,而该 tasklet 又被唤起了一次 ,函数返回。
n          保存中断状态,然后 屏蔽禁止本地中断。在我们执行tasklet代码时,这么做能够保证处理器上的数据不会被弄乱。
n          把需要调度的tasklet加到每个处理器一个的tasklist_vec链表或tasklet_hi_vec链表的表头上去。
n          唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次do_softirq()调用时就会执行该tasklet。
n          恢复中断到原状态并返回。
 
5 此处又是一个命名混乱的实例。为什么软中断是唤醒而 tasklets 是调度呢?谁能说得清?两个词其实都表示将此下半部设置为待执行挂起状态以便稍后执行。
译者注:有可能是一个 tasklet 已经被调度过但还没来得及执行,而该 tasklet 又被唤起了一
在前面的小节中我们曾经提到,do_softirq()会尽可能早的在下一个合适的时机被执行。由于大部分tasklets和软中断都是在中断处理程序中被设置成待处理状态的,所以最近一个中断返回的时候看起来就是执行do_softirq()的最佳时机。因为TASKLET_SOFTIRQ和HI_SOFTIRQ已经被触发了,所以do_softirq()会执行相应的软中断处理程序。而这两个处理程序,tasklet_action()和tasklet_hi_action(),就是tasklet处理的核心。让我们观察它们做了什么:
n          屏蔽禁止中断,并为当前处理器检索tasklet_vec或tasklist_hig_vec链表。
n          将当前处理器上的该链表设置为NULL,达到清空的效果。
n          允许响应中断(没有必要再恢复它们回原状态,因为这段程序本身就是作为软中断处理程序被调用的,所以中断是应该被允许的)。
n          循环遍历获得的链表上的每一个待处理的tasklet。
n          如果是多处理器的系统,通过检查TASKLET_STATE_RUN来判断这个tasklet是否正在其它处理器上运行。如果它正在运行,那么现在就不要执行,跳到下一个待处理的tasklet去(回忆一下,同一时间里,相同类型的tasklet只能有一个执行)。
n          如果当前这个tasklet没有执行,将其状态设置为TASKLET_STATE_RUN,这样别的处理器就不会再去执行它了。
n          检查count值是否为 0 ,确保tasklet没有被 屏蔽禁止。如果tasklet被 屏蔽禁止了,跳到下一个待执行挂起的tasklet去。
n          我们已经清楚的知道这个tasklet没有在其它地方执行,并且被我们设置成执行状态,这样它在其它部分就不会被执行,并且引用计数为 0 ,现在可以执行tasklet的处理程序了。
n          tasklet运行完毕,清除tasklet的state域的TASKLET_STATE_RUN状态标志。
n          重复执行下一个tasklet,直至没有剩余的等待处理的tasklets。
Tasklets的实现很简单,但非常巧妙。我们可以看到,所有的tasklets都通过复用HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断实现。当一个tasklet被调度时,内核就会唤起这两个软中断中的一个。随后,该软中断会被特定的函数处理,执行所有已调度的tasklet。这个函数保证同一时间里只有一个给定类别的tasklet会被执行(但其它不同类型的tasklet可以同时执行)。所有这些复杂性都被一个简洁的接口隐藏起来了。
使用Tasklets
大多数情况下,为了控制一个寻常的硬件设备,tasklet机制都是实现你自己的下半部的最佳选择。Tasklet可以动态创建,使用方便,执行起来也还算快。
声明你自己的Tasklet
你既可以静态的创建tasklet,也可以动态的创建它。选择那种方式取决于你到底是有(或者是想要)一个对tasklet的直接引用还是一个间接引用。如果你准备静态的创建一个tasklet(也就是有一个它的直接引用),使用下面<linux/interrupt.h>中定义的两个宏中的一个:
 
DECLARE_TASKLET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data)
 
这两个宏都能根据给定的名字静态的创建一个tasklist_struct结构。当该tasklet被调度以后,给定的函数func会被执行,它的参数由data给出。这两个宏之间的区别在于引用计数器的初始值设置不同。头一个宏把创建的tasklet的引用计数器设置为0,该tasklet处于激活状态。另一个把引用计数器设置为1,所以该tasklet处于 屏蔽禁止状态。下面是一个例子:
 
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);
 
这行代码其实等价于
struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0),
                                                 tasklet_handler, dev};
这样就创建了一个名为my_tasklet,处理程序为tasklet_handler并且已被激活 tasklet。当处理程序被调用的时候,dev就会被传递给它。
还可以通过将一个间接引用(一个指针)赋给一个动态创建的tasklet_struct结构 t的方式来初始化一个tasklet:
 
tasklilst_init(t, tasklet_handler, dev);      /* 动态而不是静态创建 */
编写你自己的Tasklet处理程序
tasklet处理程序必须符合规定的函数类型:
void tasklet_handler(unsigned long data)
因为是靠软中断实现,所以tasklet不能 眠。这意味着你不能在tasklet中使用信号量或者其它什么阻塞式的函数。由于tasklet运行时允许响应中断,所以你必须做好预防工作(比如屏蔽中断然后获取一个锁),如果你的tasklet和中断处理程序之间共享了某些数据的话。两个相同的tasklet决不会同时执行,这点和软中断不同——尽管两个不同的tasklet可以在两个处理器上同时执行。如果你的tasklet和其它的tasklet或者是软中断共享了数据,你必须进行适当的锁保护。(参看第七章“内核同步介绍”和第八章“内核同步方法”)。
调度你自己的tasklet
通过调用tasklet_schedule()函数并传递给它相应的tasklt_struct的指针,该tasklet就会被调度以便执行:
tasklet_schedule(&my_tasklet);      /*把 my_tasklt 标记为挂起 */

在tasklet被调度以后,只要有机会它就会尽可能早的运行。在它还没有得到运行机会之前,如果一个相同的tasklet又被调度了 译者注 (译者注:这里应该是唤起的意思,在前面讲述调度流程的小节里可以看到,调度 tasklet 的第一个步骤就是检查是否重复,所以这里根本不会完成调度。)那么它仍然只会运行一次。而如果这时它已经开始运行了,比如说在另外一个处理器上,那么这个新的tasklet会被重新调度并再次运行。作为一种优化措施,一个tasklet总在调度它的处理器上执行——这是希望能更好的利用处理器的高速缓存。
       你可以调用tasklet_disable()函数来 屏蔽禁止某个指定的tasklet。如果该tasklet当前正在执行,这个函数会等到它执行完毕再返回。你也可以换成调用tasklet_disable_nosync()函数,它也用来 屏蔽禁止指定的tasklet,不过它无须在返回前等待tasklet执行完毕。这么做往往不太安全,因为你无法估计该tasklet是否仍在执行。调用tasklet_enable()函数可以激活一个tasklet,如果希望把以DECLARE_TASKLET_DISABLED()创建的tasklet激活,你也得调用这个函数,如:
 
tasklet_disable(&my_tasklet);        /* tasklet现在被 屏蔽禁止 */
 
/* 我们现在毫无疑问的知道tasklet不能运行 .. */
 
tasklet_enable(&my_tasklet); /* tasklet现在被激活 */
 
你可以调用tasklet_kill()函数从 等待执行挂起的队列中去掉一个tasklet。该函数的参数是一个指向某个tasklet的tasklet_struct的长指针。在处理一个经常重新调度它自身的tasklet的时候,从 待执行挂起的队列中移去已调度的tasklet会很有用。这个函数首先等待该tasklet执行完毕,然后再在将它移去。当然,没有什么可以阻止其它地方的代码重新调度该tasklet。由于该函数可能会引起休眠,所以禁止在中断上下文中使用它。
Ksoftirqd    
每个处理器都有一组辅助处理软中断(和tasklet)的内核线程。当内核中出现大量软中断的时候,这些内核进程就会辅助处理它们。
我们前面曾经讨论过,对于软中断,内核会选择在几个时机进行处理。而在中断处理程序返回时处理是最常见的。软中断被触发的频率有时可能很高(像在进行大流量的网络通信期间)。雪上加霜的是,处理函数有时还会自行重复触发。也就是说,当一个软中断执行的时候,它可以重新触发自己以便再次得到执行(事实上,网络子系统就会这么做)。如果软中断本身出现的频率就高,再加上它们又有将自己重新设置为可执行状态的能力,那么就会导致用户空间进程无法获得足够的处理器时间,因而处于饥饿状态。但是,单纯的对重新触发的软中断采取不立即处理的策略,也无法让人接受。这是一个让人进退维谷的问题,亟待解决,而直观的解决方案却都不理想。首先,就让我们看看两种最容易想到的直观的方案。
    第一种方案是不管三七二十一,只要还有被触发并等待处理的软中断,本次执行就要负责处理,重新触发的软中断也在本次执行返回前被处理。这样做可以保证对内核的软中断采取即时处理的方式,关键在于,对重新触发的软中断也会立即处理。当负载很高的时候这样做就会出问题,此时会有大量被触发的软中断,而它们本身又会重复触发。系统可能会一门心思的处理软中断,根本不能完成其它任务。用户空间 的任务被忽略 不计——实际上,只有软中断和中断处理程序被轮流执行,而系统的用户只能等着发狂。只有在系统永远处于低负载的情况下,这种方案才会有理想的运行效果;只要系统有哪怕是中等程度的负载量,这种方案就无法让人满意。用户空间根本不能容忍有明显的停顿出现。
第二种方案选择不处理重新触发的软中断。在从中断返回的时候,内核和平常一样,也会检查所有 等待执行挂起的软中断并处理它们。但是,任何自行重新触发的软中断都不会马上处理,它们被放到下一个软中断执行时机去处理。而这个时机通常也就是下一次中断返回的时候,这等于是说,一定得等一段功夫,新的(或者重新触发的)软中断才能被执行。可是,在比较空闲的系统中,立即处理软中断才是比较好的做法。很不幸,这个方案显然又是一个时好时坏的选择。尽管它能保证用户空间不处于饥饿状态,但它却让软中断忍受饥饿的痛苦,而根本不去好好利用被闲置的系统资源。
这里需要一些折中。内核实际选中的方案是不立即处理重新触发的软中断。而作为改进,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低的优先级上运行(nice值是19),这能避免它们跟其它重要的任务抢夺资源。但它们最终肯定会被执行,所以,这个折中方案能够保证在软中断负担很重的时候用户程序不会因为得不到处理时间而处于饥饿状态。相应的,也能保证“过量”的软中断终究会得到处理。最后,在空闲系统上,这个方案同样表现良好,软中断被处理的非常迅速(因为仅存的内核线程肯定会马上调度)。
每个处理器都有一个这样的线程。所有线程的名字都叫做ksoftirad/n,区别在于n,它对应的是处理器的编号。在一个双CPU的机器上就有两个这样的线程,分别叫ksoftirad/0和ksoftirad/1。为了保证只要有空闲的处理器,它们就会处理软中断,所以给每个处理器都分配一个这样的线程。一旦该线程被初始化,它就会执行类似下面这样的死循环:
 
for (;;) {
    if ( !softirq_pending(cpu))
        schedule();
   
    set_current_state(TASK_RUNNING);
   
    while( softirq_pending(cpu)) {
        do_softirq();
        if (need_resched())
            schedule();
    }
 
    set_current_state(TASK_INTERRUPTIBLE);
}
只要有待处理的软中断(由softirq_pending()函数负责发现),ksoftirq就会调用do_softirq去处理它们。通过重复执行这样的操作,重新触发的软中断也会被执行。如果有必要的话,每次迭代后都会调用schedule()以便让更重要的进程得到处理机会。当所有需要执行的操作都完成以后,该内核线程将自己设置为TASK_INTERRUPTIBLE状态,唤起调度程序选择其它可执行进程投入运行。
只要 do_softirq() 函数发现已经正在执行的内核线程重新触发了它自己,软中断内核线程就会被唤醒。 ( 译者注:在深入理解linux 内核一书中,第四章解释)
曾经使用的BH机制
尽管BH机制令人欣慰的退出历史舞台,在2.6版内核中已经难觅踪迹了。可是,它毕竟曾经历经了漫长的时光——从最早版本的内核就开始了。由于其余威尚存,所以仅仅不经意的提起它是不够的,尽管2.6已经不再使用它了,但历史就是历史,应该被了解。
BH很古老,但它能揭示一些东西。所有BH都是静态定义的,最多可以有32个。由于处理函数必须在编译时就被定义好,所以实现模块时不能直接使用BH接口。不过业已存在的BH倒是可以利用。随着时间的推移,这种静态要求和最大32个的数目限制最终成为烦恼之源。
每个BH处理程序都被严格 顺序执行——不允许任何两个BH处理程序同时执行,即使它们的类型不同。这样做倒是使同步变得简单了,可是却不利于在多处理时得到良好的性能表现。使用BH的驱动程序很难从多个处理器上受益,特别是网络层,可以说它们为此饱受困扰。
除了这些特点,BH机制和tasklets就很像了。实际上,在2.4内核中,BH就是基于tasklets实现的。所有可能的32个BH都通过在<linux/interrupt.h>中定义的常量表示。如果需要将一个BH标志为 待执行挂起状态,可以把相应的BH号传给mark_bh()函数 ,调用它就可以了。在2.4内核中, 这将导致随后 这会导致对调度BH tasklet ,具体工作是由函数  bh_action ()完成的 重新调度 bh_action() 执行在2.4以前,BH机制独立存在;所作所为和现在的软中断很像。
由于这种形式的下半部机制存在的缺点,内核开发者们希望引入任务队列机制来替代它 。尽管任务队列得到了不少使用者的认可,但它实际上并没有达成这个目的。在2.3版的内核中,引入新的软中断和tasklet机制也就结束了对BH的使用。BH机制基于tasklet实现。不幸的是,因为新接口本身降低了对 顺序执行的序列化( serialization 保障 [6],所以从BH接口移植到tasklets或软中断接口操作起来非常复杂。在2.5中,这种移植最终在定时器和SCSI(最后的BH使用者)转换到软中断机制后完成了。于是内核开发者们立即除去BH接口。终于解脱了,BH!
实际上,接口降低对顺序执行的保障能够提高性能,但却难于编程。移植一个 BH tasklet ,需要仔细的斟酌:代码与其它的 tasklets 同时执行时是否安全?不过,当最终完成这样的移植以后,性能上的提高会使这些额外工作物有所值。
工作队列(work queue)
工作队列是另外一种将工作推后执行的形式 ,它和我们前面讨论的所有其它形式都不相同。工作队列可以把工作推后,交由一个内核线程去执行——该工作总是会在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许重新调度甚至是睡眠。
通常,在工作队列和软中断/tasklet中作出选择非常容易。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要 眠,那么就选择软中断或tasklet。实际上,工作队列通常可以用内核线程 替换。但是由于内核开发者们非常反对创建新的内核线程(在有些场合,使用这种冒失的方法可能会吃到苦头),所以我们也推荐使用工作队列。当然,这种接口也的确很容易使用。
如果你需要用一个可以重新调度的实体来执行你的下半部处理,你应该使用工作队列。它是唯一在进程上下文运行的下半部实现机制,也只有它才可以 眠。这意味着在你需要获得大量的内存时、在你需要获取信号量时,在你需要执行阻塞式的I/O操作时,它都会非常有用。如果你不需要用一个内核线程来将工作推后执行,那么就考虑使用tasklet吧。
工作队列的实现
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行 内核其它部分排到队列里的任务 ,这是工作队列的基本表现形式。它创建的这些内核线程被称作工作者线程(worker threads)。工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个缺省的工作者线程来处理这些工作。因此,工作队列最基本的表现形式就转变成了一个把需要推后执行的任务交给特定的通用线程这样一种接口。
缺省的工作者线程叫做events/n,这里n是处理器的编号;每个处理器对应一个线程。比如,单处理器的系统只有一个这样的线程events/0。而双处理器的系统就会多一个events/1进程。缺省的工作者线程会从多个地方得到被推后的工作。许多内核驱动程序都把它们的下半部交给缺省的工作者线程去做。除非一个驱动程序或者子系统必须建立一个属于它自己的内核线程,否则最好使用缺省线程。
不过并不存在什么东西能够阻止代码创建属于自己的工作者线程。如果你需要在工作者线程中执行大量的处理操作,这样做或许会带来好处。处理器密集型和性能要求严格的任务会因为拥有自己的工作者线程而获得好处。此时这么做也有助于减轻缺省线程的负担,避免工作队列中其它需要完成的工作处于饥饿状态。
表示线程的数据结构
工作者线程用workqueue_struct结构 表示:
/*
* 外部能够看到的工作队列抽象是一个数组,
* 由 每个CPU的工作队列组成:
*/
struct workqueue_struct {
    struct cpu_workqueue_struct cpu_wq[NR_CPUS];
};                                                                                                       
 
该结构 内是一个由cpu_workqueue_struct结构 组成的数组,数组的每一项对应系统中的一个处理器。由于系统中每个处理器对应一个工作者线程。所以对于给定的某台计算机来说,就是每个处理器,每个工作者线程对应一个这样的cpu_workqueue_struct结构体。cpu_workqueue_struct是kernel/workqueue.c中的核心数据结构:
 
/*
* 单 CPU对应的workqueue
*/
struct cpu_workqueue_struct {
    spinlock_t lock;
   
    atomic_t nr_gueued;
    struct list_head worklist;
    wait_queue_head_t more_work;
    wait_queue_head_t work_done;
   
    struct workqueue_struct *wq;
    task_t *thread;
    struct completion exit;
};
注意,每个工作者线程 类型关联一个自己的workqueue_struct。在该结构体里面,给每个线程分配一个cpu_workqueue_struct,因而也就是给每个处理器分配一个,因为每个处理器都有一个该类型的工作者线程。
表示工作的数据结构
所有的工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数执行进入一个死循环并开始休眠。当有操作被插入到队列里的时候,线程就会被唤醒,以便执行这些操作。当没有剩余的操作时,它又会继续休眠。
    工作用<linux/workqueue.h>定义的work_struct结构体表示:
struct work_struct{
    unsigned long pending;   /* 这个工作正在等待处理吗?*/
    struct list_head entry;    /* 连接所有工作的链表 */                   
    void (*func) (void *);    /* 处理函数 */
    void *data;             /* 传递给处理函数的参数 */
    void *wq_data;         /* 内部使用 */
    struct timer_list timer;    /* 延迟的工作队列所用到的定时器 */
};
这些结构体被连接成链表,每种类型的链表,在每个处理器上都有一个。比如,每个处理器上用于执行被推后的工作的那个通用线程都有一个这样的的链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续去休眠。
我们可以看一下worker_thread()函数的核心流程,简化如下:
 
for (;;) {
    set_task_state(current, TASK_INTERRUPTIBLE);
    add_wait_queue(&wq->more_work, &waiiat);
 
    if (list_empty(&wq->worklist))
        schedule();
    else
        set_task_state(current, TASK_RUNNING);
    remove_wait_queue(&cwq->more_work, &wait);
 
    if (!list_empty(&cwq->worklist))
        run_workqueue(cwq);
}
 
该函数在一个死循环中完成了以下功能:
n          线程将自己设置为休眠状态(state被设成TASK_INTERRUPTIBLE)并把自己加入到等待队列上。
n          如果工作链表是空的,线程调用schedule()函数进入 眠状态。
n          如果链表中有对象,线程不会 眠。相反,它将自己设置成TASK_RUNNING,脱离等待队列。
n          如果链表非空,调用run_workqueue()函数执行被推后的工作。
run_workqueue()
下一步,由run_workqueue()函数来实际完成推后到此的工作:
while (!list_empty(&cwq->worklist()) {
    struct work_struct *work = list_entry(cwq->worklist.next,
                 struct work_struct, entry);
    void (*f) (void *) = work->func;
    void *data = work->data;
 
    list_del_init(cwq->worklist.next);
   
    clear_bit(0, &work->pending);
    f(data);
}
该函数循环遍历链表上每个表示待处理的工作,执行链表每个节点上的workqueue_struct ( 译者注:应该是 work_struct )中的func成员函数:
n          当链表不为空时,选取下一个节点对象。
n          获取我们希望执行的函数func及其参数data。
n          把该节点从链表上解下来,将待处理标志位pending清 0
n          调用函数。
n          重复执行。
对不起,你到底都说了些什么?
这些数据结构之间的关系确实让人觉得入坠云中,难以摸清头绪。图6.1给出了示意图,把所有这些关系放在一起进行解释。
    位于最高一层的是工作者线程。系统允许有多种类型的工作者线程存在。对于指定的一个类型,系统的每个CPU上都有一个该类的工作者线程。内核中有些部分可以根据需要创建工作者线程。而在默认情况下内核只有events这一种类型的工作者线程。每个工作者线程都由一个cpu_workequeue_struct结构体 表示。而workqueue_struct结构体则表示给定类型的所有工作者线程。
    举个例子,在系统默认的通用events 工作者类型之外,我自己加入了一种falcon工作者类型。并且我使用的是一个拥有四个处理器的计算机。那么,系统中现在有四个events类型的线程(因而也就有四个cpu_workqueue_struct结构体)和四个falcon类型的线程(因而会有另外四个cpu_workqueue_struct结构体)。同时,有一个对应events类型的workqueue_struct和一个对应falcon类型的workqueue_struct。
    工作处于在最底下一层。你的驱动程序创建这些需要推后执行的工作 译者注 ( 译者注:这其实可以理解成用“工作”这种接口封装我们实际需要推后的工作。以便后续的工作者线程处理 )。它们用work_struct结构 来表示。这个结构体中最重要的部分是一个指针,它指向一个函数,而正是该函数负责处理需要推后执行的具体任务。工作会被提交给某个具体的工作者线程。然后这个工作者线程会被唤醒并执行这些被排好的工作。
大部分驱动程序都使用的是现存的默认工作者线程。它们使用起来也很简单方便。可是,在有些要求更严格的情况下,驱动程序需要自己的工作者线程。比如说XFS文件系统就为自己创建了两种新的工作者线程。
使用工作队列
工作队列的使用非常简单。我们先来看一下缺省的events任务队列,然后再看看创建新的工作者线程。
创建推后的工作
首先要做的是实际创建一些需要推后完成的工作。可以在 运行时静态的创建该结构体:
DECLARE_WORK(name, void (*func) (void *), void *data);
译者注:应该是 work_struct
译者注:这其实可以理解成用“工作”这种接口封装我们实际需要推后的工作。以便后续的工作者线程处理。
 
图6.1 工作(work),工作队列和工作者线程之间的关系.
 
这样就会静态的创建一个名为name,处理函数为func,参数为data的work_struct结构体。
    同样,也可以在运行时通过指针创建一个工作:
 
INIT_WORK(struct work_struct *work, woid(*func) (void *), void *data);
 
这会动态的初始化一个由work指向的 工作队列 ( 译者注:此处应该是工作( work ),而不是工作队列( work queue )。 ),处理函数为func,参数为data。
你的工作队列处理函数
工作队列处理函数的原型是
 
void work_handler(void *data)
 
这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中。默认情况下,允许响应中断, 并且不持有任何什么锁。如果需要,函数可以眠。需要注意的是,尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。通常在系统调用发生时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。
    在工作队列和内核其它部分之间使用锁机制就像在其它的进程上下文中使用锁机制一样方便。这使编写处理函数变得相对容易。接着的两章会讨论到锁机制。
对工作进行调度
现在工作已经被创建,我们可以调度它了。想要把给定工作的处理函数提交给缺省的events,只需调用
schedule_work(&work);
work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。
    有时候你并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,你可以调度它在指定的时间执行:
    schedule_delayed_work(&work, delay);
这时,&work指向的work_struct直到delay指定的时钟节拍用完以后才会执行。在第九章将介绍这种使用时钟节拍作为时间单位的方法。
刷新操作
排入队列的工作会在工作者线程下一次被唤醒的时候执行。有时,在继续下一步工作之前,你必须保证一些操作已经执行完毕了。这一点对模块来说就很重要,在卸载之前,它就有可能需要调用下面的函数。而在内核的其它部分,为了防止竞争条件的出现,也可能需要确保不再有待处理的工作。
    出于以上目的,内核准备了一个用于刷新指定工作队列的函数:
void flush_scheduled_work(void);
函数会一直等待,直到队列中所有对象都被执行以后才返回。在等待所有待处理的工作执行的时候,该函数会进入休眠状态,所以只能在进程上下文中使用它。
    注意,该函数并不取消任何延迟执行的工作。就是说,任何通过schedule_delayed_work()调度的工作,如果其延迟时间未结束,它并不会因为调用flush_scheduled_work()而被刷新掉。取消延迟执行的工作应该调用:
int cancel_delayed_work(struct work_struct *work);
这个函数可以取消任何与work指向的work_struct相关的 待执行挂起工作。
创建新的工作队列
如果缺省的队列不能满足你的需要,你应该创建一个新的工作队列和与之相应的工作者线程。由于这么做会在每个处理器上都创建一个工作者线程,所以只有在你明确了必须要靠自己的一套线程来提高性能的情况下,再创建自己的工作队列。
    创建一个新的任务队列和与之相关的工作者线程,你只需调用一个简单的函数:
struct workqueue_struct *create_workqueue(const char *name);
name参数用于该内核线程的命名。比如,缺省的events队列的创建就调用的是
struct workqueue_struct *keventd_wq = create_workqueue(“events”);
这样就会创建所有的工作者线程(系统中的每个处理器一个)并且做好所有开始处理工作之前的准备工作。
    创建一个工作的时候无须考虑工作队列的类型。在创建之后,可以调用下面列举的函数。这些函数与schedule_work()以及schedule_delayed_work()相近,唯一的区别就在于它们针对给定的工作队列而不是缺省的event队列进行操作。
 
int queue_work(struct workqueue_struct *wq, struct work_struct *work);
 
int queue_delayed_work(struct workqueue_struct *wq,
struct work_struct *work, unsigned long delay);
 
最后,你可以调用下面的函数刷新指定的工作队列
 
flush_workqueue(struct workqueue_struct *wq);
 
该函数和前面讨论过的flush_scheduled_work()作用相同,只是它在返回前等待变空的是给定的队列。
曾经使用的老的任务队列机制
像BH接口被软中断和tasklet替代一样,由于任务队列接口存在的种种缺陷,它也被工作队列接口取代了。像tasklet一样,任务队列接口(内核中常常称作tq)其实也和进程没有什么意义相关之处 7[7]。任务队列接口的使用者在2.5开发版中分为了两部分。其中一部分转向了 使用tasklets。还有另外一部分继续使用任务队列接口。而 目前任务队列接口剩余的部分 已经演化成了工作队列接口。由于任务队列在内核中曾经使用过一段时间,所以出于了解历史的目的,我们对它进行一个大体回顾。
    任务队列机制通过定义一组队列来实现其功能。每个队列都有自己的名字,比如调度程序队列、立即队列和定时器队列。不同的队列在内核中的不同场合使用。keventd内核线程负责执行调度程序队列的相关任务。它是整个工作队列接口的先驱。定时器队列会在系统定时器的每个时间节拍都被执行,而立即队列能够得到双倍的运行机会,以保证它能“立即”执行。当然,还有其它一些队列。此外,你还可以动态创建自己的新队列。
    这些听起来都挺有用,但任务队列接口实际上是一团乱麻。这些队列基本上都是些随意创建的抽象概念。唯有调度队列有点意义,唯有它能用来把工作推后到进程上下文完成。
    任务队列的唯一好处就是接口简单这一条了。如果不考虑这些队列的数量和执行时随心所欲的规则,它的接口确实够简单。但这也就是全部意义所在了——任务队列剩下的东西乏善可陈。
   
许多任务队列接口的使用者都已经转向使用其它的下半部实现机制了。大部分选择了tasklets。只有调度程序队列的使用者苦苦支撑。最终,keventd代码演化成了我们今天使用的工作队列机制,而任务队列最终退出了历史舞台。
到底我该选择哪种下半部机制?
在各种不同的下半部实现机制之间做出选择是很重要的。在当前的2.6版内核中,有三种可能的选择:软中断、tasklets和工作队列。Tasklets基于软中断实现,所以两者很相近。工作队列机制与它们完全不同,它靠内核线程实现。
    从设计的角度考虑,软中断提供的 顺序执行序列化的保障最少。这就要求软中断处理函数必须格外小心的采取一些步骤确保共享数据的安全,两个甚至更多相同类别的软中断有可能在不同的处理器上同时执行。如果被考察的代码本身多线索化的工作就做得非常好,比如像网络子系统,它全部用的是和单处理器相关的变量,那么软中断就是非常好的选择。对于时间要求严格和执行频率很高的应用来说,它执行的也最快。如果代码多线索化考虑的并不充分,那么选择tasklets意义更大。它的接口非常简单,而且,由于两个同种类型的tasklet不能同时执行,所以实现起来也会简单一些。
如果你需要把任务推后到进程上下文中完成,那么在这三者中就只能选择工作队列了。如果进程上下文并不是必须的条件——明确点说,就是如果并不需要 眠——软中断和tasklets可能更合适。工作队列造成的开销最大,因为它要牵扯到内核线程甚至是上下文切换。这并不是说工作队列的效率低,如果每秒钟有几千次中断,就像网络子系统时常经历的那样,那么采用其它的机制可能更合适一些。不管怎么说,针对大部分情况,工作队列都能提供足够的支持。
如果讲到易于使用,工作队列就当仁不让了。使用缺省的events队列简直不费吹灰之力。下来要数tasklets,它的接口也很简单。位居末座的是软中断,它必须静态创建。
表6.3是对三种下半部接口的比较
 
表6.3 对下半部的比较
下半部
上下文
顺序执行保障
软中断
中断
没有
Tasklet
中断
同类型不能同时执行
工作队列
进程
没有(和线程一样被调度)
 
简单的说,一般的驱动程序的编写者需要做两个选择。首先,你是不是需要一个可调度的实体来执行需要推后完成的工作——你有任何休眠的需要吗?要是有,工作队列就是你的唯一选择。否则最好用tasklet。要是必须专注于性能的提高,那么就考虑软中断吧。
在下半部之间加锁
到现在为止,我们没讨论过锁机制,这是一个非常有趣的话题,我将在接下来的两章里仔细讨论它。不过,在这里还是应该对它的重要性有所了解,在使用下半部机制时,即使是在一个单处理器的系统上,避免共享数据被同时访问也是至关重要的。记住,一个下半部实际上可能在任何时候执行。如果你对锁机制一无所知的话,你也可以在读完后面两章以后再回过头来看这部分。
    使用tasklets的一个好处在于它自己负责对 顺序执行的序列化保障:两个相同类型的taslet不允许同时执行,即使在不同的处理器上也不行。这意味着你无须为tasklet内部的同步问题操心了。Tasklet之间的同步(就是当两个不同类型的tasklet共享同一数据时)需要正确使用锁机制。
    因为软中断根本不保障 顺序执行序列化,(即使相同类型的软中断也有可能有两个实例在同时执行)所以所有的共享数据都需要合适的锁。
    如果进程上下文和一个下半部共享数据,在访问这些数据之前,你需要 屏蔽禁止下半部的处理并得到锁的使用权。所做的这些是为了本地和SMP的保护并且防止死锁的出现。
    如果中断上下文和一个下半部共享数据,在访问数据之前,你需要 屏蔽禁止中断并得到锁的使用权。这所做的这些也是为了本地和SMP的保护并且防止死锁的出现。
    任何在工作队列中被共享的数据也需要使用锁机制。其中有关锁的要点和在一般内核代码中的没什么区别,因为工作队列本来就是在进程上下文中执行的。
    在第七章里,我们会揭示锁的奥妙。而在第八章中,我们将讲述内核的加锁原语。这两个章节会描述如何保护下半部使用的数据。
禁止下半部
一般单纯禁止下半部的处理是不够的。为了保证共享数据的安全,更常见的做法是先得到一个锁然后再禁止下半部的处理。驱动程序中通常使用的都是这种方法,在第八章会详细介绍。然而,如果你编写的是内核的核心代码,你也可能仅需要禁止下半部就可以了。
    如果需要禁止所有的下半部处理(明确点说,就是所有的软中断和所有的tasklets),可以调用local_bh_diasble()函数。允许下半部进行处理,可以调用local_bh_enable()函数。没错,这些函数的命名也有问题;可是既然BH接口早就让位给软中断了,那么谁又会闲极无聊去改这些名字呢。表6.4是这些函数的一份摘要
Table 6.4 下半部机制控制函数的清单
函数
描述
void local_bh_diasble()
禁止本地处理器的软中断和tasklet的处理
void local_bh_enable()
允许激活本地处理器的软中断和tasklet的处理
这些函数有可能被嵌套使用——最后被调用的local_bh_enable()最终激活下半部开始。函数通过preempt_count(很有意思,还是这个计数器,内核抢占的时候用的也是它) [8] 维持每个进程维护一个计数器。当计数器变为 0时,下半部 才能够被 处理才是可能的。因为下半部的处理已经被禁止,所以local_bh_enable()还需要检查所有现存的待处理的下半部并执行它们。
    这些函数与硬件体系结构相关,它们位于<asm/softirq.h>中,通常由一些复杂的宏实现。下面是为那些好奇的人准备的C语言的近似描述:
 
/*
* 通过增加 preempt_count 禁止本地下半部disable local bottom halves by incrementing the preempt_count
*/
void local_bh_disable(void)
{
    struct thread_info *t = current_thread_info();
    t->preempt_count += SOFTIRQ_OFFSET;
}
 
/*
* 减少 decrement the preempt_count – 如果该技术跌到 0 将导致自动激活下 半部
*
* 执行挂起的下半部optionally run any bottom halves that are pending
*/
void local_bh_enable(void)
{
    struct thread_info *t = current_thread_info();
 
    t->preempt_count -= SOFTIRQ_OFFSET;
   
    /*
* is preempt_count 是否为 0 ,另外是否有 挂起 的下半部 ,如果 都满足,则执行待执
* 行的下半部*/
if (runlikely(!t->preempt_count && softirq_pending(smp_processor_id())))
        do_softirq();
}
这些函数并不能禁止工作队列的执行。因为工作队列是在进程上下文中运行的,不会涉及异步执行的问题,所以也就没有必要禁止它们执行。由于软中断和tasklet是异步发生的(就是说,在中断处理返回的时候),所以,内核代码必须禁止它们。另一方面,对工作队列来说,它保护共享数据所做的工作和其它任何进程上下文中所做的都差不多。第七和第八章将揭示其中的细节。
 
 
 


[1] 由于不同的 BH 在运行时依靠全局同步机制,所以把它们转成软中断和 tasklets 花费了相当大的功夫直到 2.5 才最终完成。
 
[2] 它们和进程没有一点关系。可以把一个 tasklet 当作一个简单易用的软中断。
[3] 大部分驱动程序都使用 tasklets 来实现它们的下半部。我们将在下一节看到, Tasklets 是用软中断实现的。
 
[4] 实际上在执行此步操作时需要屏 禁止本 地中断。但在这个简化版本中被省略了。如果中断不被屏蔽,在保存位图和清除它的间隙,可能会有一个新的软中断被唤起(它自然也就会等待处理)。这可能会造成对此待处理的位进行不应该的清 0
[5] 此处又是一个命名混乱的实例。为什么软中断是唤醒而 tasklets 是调度呢?谁能说得清?两个词其实都表示将此下半部设置为待执行状态以便稍后执行。
 
[6] 实际上,接口降低对执行的序列化保障能够提高性能,但却难于编程。移植一个 BH tasklet ,需要仔细的斟酌:代码与其它的 tasklets 同时执行时是否安全?不过,当最终完成这样的移植以后,性能上的提高会使这些额外工作物有所 值。
[7] 下半部的各种命名简直可以算得上是迷惑内核开发新手的杀手锏了。
 
[8] 实际上,中断和下半部子系统都用到了这个计数器。其实,在 Linux 中,这个每个进程一个的计数器实际上就代表着进程的原子性。在类似调试 sleeping-while-atomic 之类的错误时,这种做法已被证明是非常有效的。
 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值