上一节介绍了 Linux 中的“中断”机制,明白了 Linux 内核在中断机制的设计中陷入了“两难”的境地:一方面希望中断处理程序有能力做足够多的工作,另一方面又希望中断处理程序能够尽快完成,以避免被中断打断的代码段停止时间过长,影响整个系统的效率。
而且,中断处理程序不在进程上下文中运行,所以它们不能阻塞,寻常的调度设计用不上,这也限制了中断处理程序的发挥。因此,Linux 内核将一次完整的中断处理分为“上半部”和“下半部”两部分。上一节介绍的中断处理程序属于“上半部”,本节介绍一下“下半部”。
下半部处理中断的哪些工作呢?
严格来说,中断的“上半部”和“下半部”工作并没有严格的界限,如果合适,应尽可能将工作放在下半部中,因为我们总是希望上半部中断处理程序尽快完成。
那么,哪些工作不适合放在下半部,而应该放在上半部处理呢?要回答这个问题,首先应该明白的是:Linux 内核将中断处理分为上下半部的初衷就是为了尽可能的避免影响整个系统的效率。
当硬件中断发生时,最先开始着手处理中断的是中断处理程序,也即上半部。这也就是说,对时间非常敏感的工作,例如对发生中断的硬件做出应答,从硬件设备拷贝数据到内存等,只有放在上半部处理,才能获得最佳的效率。总结一下,如果:
- 工作对时间非常敏感
- 工作和硬件相关
- 工作要保证不被其他中断打断
则应将其放入上半部,剩下的其他工作,都应该尽量放在下半部中。
Linux 内核何时执行下半部呢?
上一节粗略的提到“下半部的工作则可以暂缓到 cpu 没那么忙的时候再做”,那么到底放到以后什么时候执行呢?其实只要将下半部的工作推迟到中断恢复后执行就可以了,这个时候 Linux 内核允许响应所有中断,通常下半部在上半部中断处理程序返回后就可以立即执行了。
事实上,不仅仅是 Linux ,很多其他操作系统也将一次完整的中断处理分为两部分,一部分简单快速,执行的时候禁止一些或者全部中断,下半部则稍后执行,并且执行期间可以响应所有中断。
这里说的“稍后执行”其实就是指“不是立刻”而已,在允许响应中断后就可以投入执行了。
我手头的这个版本的 Linux 内核源代码,提供了三种不同形式的下半部实现机制:
- 软中断
- tasklet
- 工作队列
本节详细分析一下 Linux 内核软中断的设计与C语言代码实现。
软中断
从名字就能看出,软中断机制与硬件的中断机制是类似的。软中断是在编译期间就静态分配好的,不能动态的注册或者注销,为什么呢?请看下面这句 C语言代码:
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
软中断信息都记录在 static 型的全局变量 softirq_vec 里,而 softirq_vec 是一个 32 维的数组,数组就是在编译期间静态分配的,不能再被动态分配和注销。softirq_action 的 C 语言定义如下:
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
action 成员是一个函数指针,它指向软中断处理函数,data 成员则是对应的参数。细心的朋友应该发现了,action 接受的参数也是 struct softirq_action 指针型的,这个小技巧可以确保以后再往 struct softirq_action 中添加新成员时,无需对所有软中断处理程序都做修改。
软中断的C语言代码实现
使用软中断机制之前,需要调用 open_softirq() 函数注册软中断处理程序,它的C语言代码如下:
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}
open_softirq() 函数接收三个参数,第一个参数表示软中断索引,剩下两个参数分别是软中断处理函数指针和其对应的参数。open_softirq() 函数的功能也很简单,就是将接收到的参数赋值给 softirq_vec 而已。
注册好软中断处理程序之后,若想执行之,需要像硬件产生中断那样,模拟出一个“中断”(即软中断)以触发软中断处理程序,这一过程 Linux 内核是通过 raise_softirq() 函数实现的,该函数的C语言代码如下:
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
核心是 raise_softirq_irqoff() 函数,继续跟踪,它的C语言代码如下:
#define or_softirq_pending(x) (local_softirq_pending() |= (x))
#define __raise_softirq_irqoff(nr)
do { or_softirq_pending(1UL << (nr)); } while (0)
发现其实 raise_softirq() 函数的核心动作就是设置全局变量 local_softirq_pending 的某一位。
现在我们也知道 Linux 内核是如何模拟出“软中断”触发软中断处理程序的了,那么触发后,软中断处理程序是如何运行的呢?这就是 do_softirq() 函数的工作了,它的C语言代码如下:
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags);
pending = local_softirq_pending();
if (pending)
__do_softirq();
local_irq_restore(flags);
}
到这里,我们就明白 raise_softirq() 函数设置 local_softirq_pending 标记位就能执行软中断处理程序的原因了,因为local_softirq_pending 是一个 32 bit 的变量,每一个 bit 都对应着一个软中断处理程序。do_softirq() 函数只会执行设置过标记位的(被模拟出的“中断”触发过的)软中断处理程序。do_softirq() 函数的核心动作由 __do_softirq()
函数完成,它的核心部分C语言代码如下:
不难看出,__do_softirq()
函数会遍历执行所有被触发过的软中断处理程序。到这里,我们就粗略的明白了 Linux 内核中关于中断下半部软中断的设计与 C 语言代码实现了。至于 tasklet 和工作队列部分,下一节再说了。