中断服务一般都是在将中断请求关闭的条件下执行的,以避免嵌套而使控制复杂化。可是,如果关中断的时间持续太长就可能因为CPU不能及时响应其他的中断请求而使中断(请求)丢失,为此,内核允许在将具体的中断服务程序挂入中断请求队列时将SA_INTERRUPT标志置成0,使这个中断服务程序在开中断的条件下执行。然而,实际的情况往往是:若是服务的全过程关中断则扩大打击面,而全称开中断则又造成不安定因素,很难取舍。一般来说,一次中盾服务的过程常常可以分成两部分。开头的部分往往是必须在关中断条件下执行的。这样才能在不受干扰的条件下原子地完成一些关键性操作。同时,这部分操作的时间性又往往很强,必须在中断请求发生后立即或至少是在一定的时间现值中完成,而且相继的多次中断请求也不能合并在一起处理。而后半部分,则通常可以、而且应该在开中断条件下执行,这样才不至于因将中断关闭过久而造成其他中断的丢失。同时,这些操作常常允许延迟到稍后才能执行,而且有可能将多次中断服务中的相关部分合并在一起处理。这里的后半部分就称为bottom half,在内核代码中常常缩写成bh。这个概念在相当程度上来自RISC系统结构。在RISC的CPU中,通常由大量的寄存器。当中断发生时,要将所有这些寄存器的内容压栈,并在返回时加以恢复,为此而付出很高的代价。所以,在RISC结构的系统中往往把中断服务分成两部分。第一部分只保存为数不多的寄存器(内容),并利用这些为数不多的寄存器来完成有限的关键性的操作,称为轻量级中断。而另一部分,那就相当于这里的bh了。虽然i386的结构主要是CISC的,面临的问题不尽相同,但前述的问题已经使bh的必要性在许多情况下变得很明显了。
linux内核为将中断服务非诚两半提供了方便,并设立了相应的机制。在以前的内核中,这个机制就称为bh。但是,在2.4版(确切的说是2.3.43)中有了新的发展和推广。
以前的内核中设置了一个函数指针数组bh_base,其大小为32,数值中的每个指针可以用来指向一个具体的bh函数。同时,又设置了两个32位无符号整数bh_active和bh_mask(2.3.38版本),每个无符号的整数中的32位对应着数组bh_base中的32个元素。
我们可以在中断与bh二者之间建立起一种类比。
- 数组bh_base相当于硬件中断机制中的数组irq_desc。不过irq_desc中的每个元素代表着一个中断通道,所以是一个中断服务程序队列。而bh_base中的每个元素却最多只能代表一个bh函数。但是,尽管如此,二者在概念上还是相同的。
- 无符号整数bh_active在概念上相当于硬件的中断请求寄存器,而bh_mask则相当于中断屏蔽寄存器。
- 需要执行一个bh函数时,就通过一个函数mark_bh将bh_active中的某一位设成1,相当于中断源发生了中断请求,而所设置的具体标志位则类似于中断向量。
- 如果相当于中断屏蔽寄存器的bh_mask中相应位也是1,即系统允许这个bh函数,那么就会在每次执行完do_IRQ中的中断服务程序后,以及每次系统调用结束之时,在一个函数do_bottom_half中执行相应的bh函数。而do_bottom_half,则类似于do_IRQ。
为了简化bh函数的设计,在do_bottom_half中也像do_IRQ中一样,把bh函数的执行严格地串行化了。这种串行化有两方面的考虑和措施:
一方面,bh函数的执行不允许嵌套。如果在执行bh函数的过程中发生中断,那么由于每次中断服务以后在do_IRQ中都要检查和处理bh函数的执行,就有可能嵌套。为此,在do_bottom_half中针对同一CPU上的嵌套执行加了锁,这样,如果进入do_bottom_half以后发现已经加了锁,就立即返回。因为这说明CPU在本次中断发生之前已经在这个函数中了。
另一方面,是在多CPU系统中,在同一时间内最多只允许一个CPU执行bh函数,以防有两个甚至更多个CPU同时来执行bh函数而互相干扰。为此在do_bottom_half中针对不同CPU同时执行bh函数也加了锁。这样,如果进入do_bottom_half以后发现这个锁已经锁上,就说明已经由CPU在执行bh函数,所以也立即返回。
这两条措施,特别是第二条措施,保证了从单CPU结构到多CPU SMP结构的平稳过渡。可是,在当时的linux内核可以在多CPU SMP结构上稳定运行以后,就慢慢发现这样的处理对于多CPU SMP结构的性能有不利的影响。原因就在于上述的第二条措施使bh函数的执行完全串行化了。当系统中有很多bh函数需要执行时,虽然系统中有多个CPU存在,却只有一个CPU这么一个独木桥。跟do_IRQ做一比较就可以发现,在do_IRQ中的串行化只是针对一个具体中断通道到的,而bh函数的串行化却是全局性的,所以是防卫过当了。既然如此,就应该考虑放宽上述的第二条措施。但是,如果放宽了这一条,就要对bh函数本身的设计和实现有更高的要求(例如对使用全局变量的互斥),而原来已经存在的bh函数显然不符合这些要求。所以,比较好的办法是保留bh。另外再增设一种或几种机制。并把它们纳入一个统一的框架中。这就是2.4版中的软中断(softirq)机制。
从字面上说softirq就是软中断,可是软中断这个词(尤其是在中文里)已经被用作信号(signal)的代名词,因为信号实际上就是以软件手段实现的中断机制。但是,另一方面,把类似于bh的机制称为软中断又确实很贴切。这一方面反映了上述bh函数与中断之间的类比,另一方面也反映了这是一种在时间要求上更为软性的中断请求。实际上,这里所体现的是层次的不同。如果说硬中断通常是外部设备对CPU的中断,那么softirq通常是硬中断服务程序对内核的中断,而信号则是由内核(或其他进程)对某个进程的中断。后面这二者都是由软件产生的软中断。所以,对软中断这次的含义要根据上下文加以区分。
下面,我们以bh函数为主线,通过阅读代码来叙述2.4版内核的软中断(softirq)机制。
系统在初始化时通过函数softirq_init对内核的软中断机制进行初始化。其代码如下:
void __init softirq_init()
{
int i;
for (i=0; i<32; i++)
tasklet_init(bh_task_vec+i, bh_action, i);
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}
软中断本身是一种机制,同时也使一个框架。在这个框架里有bh机制,这是一种特殊的软中断,也可以说是设计最保守的,但却是最简单、最安全的软中断。除此之外,还有其他的软中断,定义如下:
enum
{
HI_SOFTIRQ=0,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
TASKLET_SOFTIRQ
};
这里最值得注意的是TASKLET_SOFTIRQ,代表着一种称为tasklet的机制。也许采用tasklet这个词的原意在于表示这是一片小小的任务,但是这个词容易使人联想到task即进程而引起误会,其实这二者毫无关系。显然,NET_TX_SOFTIRQ和NET_RX_SOFTIRQ两种软中断是专为网络操作而设的,所以在softirq_init中只对TASKLET_SOFTIRQ和HI_SOFTIRQ两种软中断进行初始化。
先看bh机制的初始化。内核中为bh机制设置了一个结构数组bh_task_vec,这是tasklet_struct数据结构的数组。这种数据结构的定义也在interrupt.h中:
/* Tasklets --- multithreaded analogue of BHs.
Main feature differing them of generic softirqs: tasklet
is running only on one CPU simultaneously.
Main feature differing them of BHs: different tasklets
may be run simultaneously on different CPUs.
Properties:
* If tasklet_schedule() is called, then tasklet is guaranteed
to be executed on some cpu at least once after this.
* If the tasklet is already scheduled, but its excecution is still not
started, it will be executed only once.
* If this tasklet is already running on another CPU (or schedule is called
from tasklet itself), it is rescheduled for later.
* Tasklet is strictly serialized wrt itself, but not
wrt another tasklets. If client needs some intertask synchronization,
he makes it with spinlocks.
*/
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
代码的作者加了详细的注释,说tasklet是多序(不是多进程或多线程!)的bh函数。为什么这么说呢?因为对tasklet的串行化不像对bh函数那样严格,所以允许在不同的CPU上同时执行tasklet,但必须是不同的tasklet。一个tasklet_struct数据结构就代表这一个tasklet,结构中的函数指针func指向其服务程序。那么,为什么bh机制中要使用这种数据结构呢?这是因为bh函数的执行(并不是bh函数本身)就是作为一个tasklet来实现的,在此基础上再加上更严格的限制,就成了bh。
函数tasklet_init的代码在kernel/softirq.c中:
softirq_init=>tasklet_init
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->func = func;
t->data = data;
t->state = 0;
atomic_set(&t->count, 0);
}
在softirq_init中,对于bh的32个tasklet_struct结构调用tasklet_init以后,它们的函数指针func全都指向bh_action。
对其他软中断的初始化是通过open_softirq完成的,其代码如下:
softirq_init=>open_softirq
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
unsigned long flags;
int i;
spin_lock_irqsave(&softirq_mask_lock, flags);
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
for (i=0; i<NR_CPUS; i++)
softirq_mask(i) |= (1<<nr);
spin_unlock_irqrestore(&softirq_mask_lock, flags);
}
内核中为软中断设置了一个以软中断号为下标的数组softirq_vec,类似于中断机制的irq_desc。
static struct softirq_action softirq_vec[32] __cacheline_aligned;
这是一个softirq_action数据结构的数组,其定义为:
/* softirq mask and active fields moved to irq_cpustat_t in
* asm/hardirq.h to get better cache usage. KAO
*/
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
数组softirq_vec是个全局变量,系统中的各个CPU所看到的是同一个数组。但是,每个CPU各有其自己的软中断控制、状况结构,所以这些数据结构形成一个以CPU编号为下标的数组irq_stat。这个数组也是全局变量,但是各个CPU可以按其自身编号的访问相应的数据结构。我们把有关的定义列出于下,供读者自己阅读:
/* entry.S is sensitive to the offsets of these fields */
typedef struct {
unsigned int __softirq_active;
unsigned int __softirq_mask;
unsigned int __local_irq_count;
unsigned int __local_bh_count;
unsigned int __syscall_count;
unsigned int __nmi_count; /* arch dependent */
} ____cacheline_aligned irq_cpustat_t;
irq_cpustat_t irq_stat[NR_CPUS];
#ifdef CONFIG_SMP
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
#else
#define __IRQ_STAT(cpu, member) ((void)(cpu), irq_stat[0].member)
#endif
/* arch independent irq_stat fields */
#define softirq_active(cpu) __IRQ_STAT((cpu), __softirq_active)
#define softirq_mask(cpu) __IRQ_STAT((cpu), __softirq_mask)
数据结构中__softirq_active相当于软中断请求寄存器,__softirq_mask则相当于软中断屏蔽寄存器。函数open_softirq除把函数指针action填入softirq_vec中的相应元素外,还把所有CPU的中断屏蔽寄存器中的相应位设置成1,使这个软中断在每个CPU上都可以执行。从softirq_init中调用open_softirq把TASKLET_SOFTIRQ和HI_SOFTIRQ两个软中断的处理程序分别设置成tasklet_action和tasklet_hi_action。
内核中还有另一个以CPU编号为下标的数组tasklet_hi_vec,这是tasklet_head结构数组,每个tasklet_head结构就是一个tasklet_struct结构的队列头。
struct tasklet_head
{
struct tasklet_struct *list;
} __attribute__ ((__aligned__(SMP_CACHE_BYTES)));
struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned;
回到bh机制这个话题上。通过softirq_init只是使相应tasklet_struct结构中的函数指针func只想了bh_action,也就是建立了bh的执行机制,而具体的bh函数还没有与之挂钩,就好像具体的中断服务程序尚未挂入中断服务队列一样。具体bh函数是通过init_bh设置的。下面是取自sched_init中的一个片段:
init_bh(TIMER_BH, timer_bh);
init_bh(TQUEUE_BH, tqueue_bh);
init_bh(IMMEDIATE_BH, immediate_bh);
以用于时钟中断的bh函数timer_bh为例,其bh向量或bh编号为TIMER_BH。目前内核中已经定义的编号如下:
/* Who gets which entry in bh_base. Things which will occur most often
should come first */
enum {
TIMER_BH = 0,
TQUEUE_BH,
DIGI_BH,
SERIAL_BH,
RISCOM8_BH,
SPECIALIX_BH,
AURORA_BH,
ESP_BH,
SCSI_BH,
IMMEDIATE_BH,
CYCLADES_BH,
CM206_BH,
JS_BH,
MACSERIAL_BH,
ISICOM_BH
};
再看init_bh的代码:
void init_bh(int nr, void (*routine)(void))
{
bh_base[nr] = routine;
mb();
}
显然,这里的数组bh_base就是前述的函数指针数组。这里调用的函数mb与CPU中执行指令的流水线有关,而这并不是我们现在所关心的。
需要执行一个特定的bh函数时,可以通过一个inline函数mark_bh提出请求。读者在时钟中断博客中可以看到在do_timer中通过mark_bh(TIMER_BH);提出对timer_bh的执行请求。函数mark_bh的代码如下:
static inline void mark_bh(int nr)
{
tasklet_hi_schedule(bh_task_vec+nr);
}
如前所述,内核中为bh函数的执行设立了一个tasklet_struct结构数组bh_task_vec,这里以bh函数的编号为下标就可以找到相应的数据结构,并用其调用tasklet_hi_schedule,其代码也在同一个文件中。读者应该还记得,在bh_task_vec的每个tasklet_struct结构中,函数指针func都指向bh_action。
mark_bh=>tasklet_hi_schedule
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
int cpu = smp_processor_id();
unsigned long flags;
local_irq_save(flags);
t->next = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = t;
__cpu_raise_softirq(cpu, HI_SOFTIRQ);
local_irq_restore(flags);
}
}
这里的smp_processor_id返回当前进程所在CPU的编号,然后以此为下标从tasklet_hi_vec中找到该CPU的队列头,把参数t所指的tasklet_struct数据结构链入这个队列。由此可见,对执行bh函数的要求是在哪一个CPU上提出的,就把它调度在哪一个CPU上执行,函数名中的schedule就是这个意思,而与进程调度毫无关系。另一方面,一个tasklet_struct代表着对bh函数的一次执行,在同一时间内只能把它链入一个队列中,而不可能同时出现在多个队列中。对于同一个tasklet_struct数据结构,如果已经对其调用了tasklet_hi_schedule,而尚未得到执行,就不允许再将其链入队列,所以在数据结构中设置了一个标志位TASKLET_STATE_SCHED来保证这一点。最后,还要通过__cpu_raise_softirq正式发出软中断请求。
mark_bh=>tasklet_hi_schedule=>__cpu_raise_softirq
static inline void __cpu_raise_softirq(int cpu, int nr)
{
softirq_active(cpu) |= (1<<nr);
}
读者在前面已经看到过softirq_active的定义,它对给定CPU的软中断控制、状况结构操作,将其中softirq_active字段内的相应标志位设成1.
内核每当在do_IRQ中执行一个通道中的中断服务程序以后,以及每当从系统调用返回时,都要检查是否有软中断请求在等待执行。下面是do_IRQ中的一个片段:
if (softirq_active(cpu) & softirq_mask(cpu))
do_softirq();
另一段代码取自arch/i386/entry.S,这是在从系统调用返回时执行的:
ENTRY(ret_from_sys_call)
#ifdef CONFIG_SMP
movl processor(%ebx),%eax
shll $CONFIG_X86_L1_CACHE_SHIFT,%eax
movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask
#else
movl SYMBOL_NAME(irq_stat),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask
#endif
jne handle_softirq
handle_softirq:
call SYMBOL_NAME(do_softirq)
jmp ret_from_intr
注意,这里的processor表示task_struct数据结构中该字段的位移,所以207行是从当前进程的task_struct数据结构中取当前CPU的编号。而SYMBOL_NAME(irq_stat)(,%eax)则相当于irq_stat[cpu],并且是其中第一个字段;相应地,SYMBOL_NAME(irq_stat)+4(,%eax)相当于这个数据结构中的第二个字段,并且第一个字段必须是32位。读者不妨回过去看一下irq_cpustat_t的定义,在那里有个注释,说entry.S中的代码对这个数据结构中的字段位置敏感,就是这个意思。所以,这些汇编代码实际上与上面do_IRQ中的两行C代码时一样的。
检测到软中断请求以后,就要通过do_softirq加以执行了。其代码如下:
asmlinkage void do_softirq()
{
int cpu = smp_processor_id();
__u32 active, mask;
if (in_interrupt())
return;
local_bh_disable();
local_irq_disable();
mask = softirq_mask(cpu);
active = softirq_active(cpu) & mask;
if (active) {
struct softirq_action *h;
restart:
/* Reset active bitmask before enabling irqs */
softirq_active(cpu) &= ~active;
local_irq_enable();
h = softirq_vec;
mask &= ~active;
do {
if (active & 1)
h->action(h);
h++;
active >>= 1;
} while (active);
local_irq_disable();
active = softirq_active(cpu);
if ((active &= mask) != 0)
goto retry;
}
local_bh_enable();
/* Leave with locally disabled hard irqs. It is critical to close
* window for infinite recursion, while we help local bh count,
* it protected us. Now we are defenceless.
*/
return;
retry:
goto restart;
}
软中断服务程序既不允许在一个硬中断服务程序内部执行,也不允许在一个软中断服务程序内部执行,所以要通过一个宏操作in_interrupt加以检测,这是在include/asm-i386/hardirq.h中定义的:
/*
* Are we in an interrupt context? Either doing bottom half
* or hardware interrupt processing?
*/
#define in_interrupt() ({ int __cpu = smp_processor_id(); \
(local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })
显然,这个测试防止了软中断服务程序的嵌套,这就是前面讲的第一条串行化措施。与local_bh_disable有关的定义在include/asm-i386/softirq.h中:
#define cpu_bh_disable(cpu) do { local_bh_count(cpu)++; barrier(); } while (0)
#define cpu_bh_enable(cpu) do { barrier(); local_bh_count(cpu)--; } while (0)
#define local_bh_disable() cpu_bh_disable(smp_processor_id())
#define local_bh_enable() cpu_bh_enable(smp_processor_id())
从do_softirq的代码中可以看出,使CPU不能执行软中断服务程序的关卡只有一个,那就是in_interrupt,所以对软中断服务程序的执行并没有采取前述的第二条串行化措施。这就是说,不同的CPU可以同时进入对软中断服务程序的执行(见78行),分别执行各自所请求的软中断服务。从这个意义上,软中断服务程序的执行时并发的、多序的。但是,这些软中断服务程序的设计和实现必须十分小心,不能让它互相干扰(例如通过共享的全局变量)。至于do_softirq中其他的代码,则读者不会感到困难,我们就不多说了。
在我们这个情景中,如前所述,执行的服务程序为bh_action,其代码如下:
/* BHs are serialized by spinlock global_bh_lock.
It is still possible to make synchronize_bh() as
spin_unlock_wait(&global_bh_lock). This operation is not used
by kernel now, so that this lock is not made private only
due to wait_on_irq().
It can be removed only after auditing all the BHs.
*/
spinlock_t global_bh_lock = SPIN_LOCK_UNLOCKED;
static void bh_action(unsigned long nr)
{
int cpu = smp_processor_id();
if (!spin_trylock(&global_bh_lock))
goto resched;
if (!hardirq_trylock(cpu))
goto resched_unlock;
if (bh_base[nr])
bh_base[nr]();
hardirq_endlock(cpu);
spin_unlock(&global_bh_lock);
return;
resched_unlock:
spin_unlock(&global_bh_lock);
resched:
mark_bh(nr);
}
这里对具体bh函数的执行(见257行)又设置了两到关卡。一道是hardirq_trylock,其定义为:
#define hardirq_trylock(cpu) (local_irq_count(cpu) == 0)
与前面的in_interrupt比较下就可看出,这还是防止从一个硬中断服务程序内部调用bh_action。而另一道关卡spin_trylock就不同了,定义如下:
#define spin_trylock(lock) (!test_and_set_bit(0,(lock)))
这把锁即使全局变量global_bh_lock,只要有一个CPU在253行至260行之间运行,别的CPU就不能进入这个区间了,所以在任何时间最多只有一个CPU在执行bh函数。这就是前述的第二条串行化措施。至于根据bh函数编号执行相应的函数,那就很简单了。在我们这个情景中,具体的bh函数是timer_bh,我们将在时钟中断博客中阅读这个函数的代码。
作为对比,我们列出另一个软中断服务程序tasklet_action的代码,对着可以把它与bh_action比较,看看有哪些重要的区别。这个函数的代码定义如下:
struct tasklet_head tasklet_vec[NR_CPUS] __cacheline_aligned;
static void tasklet_action(struct softirq_action *a)
{
int cpu = smp_processor_id();
struct tasklet_struct *list;
local_irq_disable();
list = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = NULL;
local_irq_enable();
while (list != NULL) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (atomic_read(&t->count) == 0) {
clear_bit(TASKLET_STATE_SCHED, &t->state);
t->func(t->data);
/*
* talklet_trylock() uses test_and_set_bit that imply
* an mb when it returns zero, thus we need the explicit
* mb only here: while closing the critical section.
*/
#ifdef CONFIG_SMP
smp_mb__before_clear_bit();
#endif
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
local_irq_enable();
}
}
最后,软中断服务程序,报货bh函数,与常规中断服务程序的分离并不是强制性的,要根据设备驱动的具体情况(也许还有设计人员的水平)来决定。