
图3.8 中断的分割
中断服务例程一般都是在中断请求关闭的条件下执行的,以避免嵌套而使中断控制复杂化。但是,中断是一个随机事件,它随时会到来,如果关中断的时间太长,CPU就不能及时响应其他的中断请求,从而造成中断的丢失。因此,内核的目标就是尽可能快的处理完中断请求,尽其所能把更多的处理向后推迟。例如,假设一个数据块已经达到了网线,当中断控制器接受到这个中断请求信号时,Linux内核只是简单地标志数据到来了,然后让处理器恢复到它以前运行的状态,其余的处理稍后再进行(如把数据移入一个缓冲区,接受数据的进程就可以在缓冲区找到数据)。因此,内核把中断处理分为两部分:前半部分(top half)和后半部分(bottom half),前半部分内核立即执行,而后半部分留着稍后处理,如图3.8所示:
首先,一个快速的“前半部分”来处理硬件发出的请求,它必须在一个新的中断产生之前终止。通常地,除了在设备和一些内存缓冲区(如果你的设备用到了DMA,就不止这些)之间移动或传送数据,确定硬件是否处于健全的状态之外,这一部分做的工作很少。
然后,就让一些与中断处理相关的有限个函数作为 “后半部分”来运行:
· 允许一个普通的内核函数,而不仅仅是服务于中断的一个函数,能以后半部分的身份来运行。
· 允许几个内核函数合在一起作为一个后半部分来运行。
后半部分运行时是允许中断请求的,而前半部分运行时是关中断的,这是二者之间的主要区别。
3.5.2 实现机制
Linux内核为将中断服务分为两部分提供了方便,并设立了相应的机制。在以前的内核中,这个机制就叫bottom half(简称bh),但在2.4版中有了新的发展和推广,叫做软中断(softirq)机制。
1.Bh机制
以前内核中的Bh机制设置了一个函数指针数组bh_base[],它把所有的后半部分都组织起来,其大小为32,数组中的每一项就是一个后半部分,即一个bh 函数。同时,又设置了两个32位无符号整数bh_active和bh_mask,每个无符号整数中的一位对应着bh_base[]中的一个元素,如图3.9所示:

图3.9 bh机制示意图
在2.4以前的内核中,每次执行完do_IRQ()中的中断服务例程以后,以及每次系统调用结束之前,就在一个叫do_bottom_half()的函数中执行相应的bh函数。
在do_bottom_half()中对bh函数的执行是在关中断的情况下进行的,也就是说对bh的执行进行了严格的“串行化”,这种方式简化了bh的设计,这是因为,对单CPU来说,bh 函数的执行可以不嵌套;而对于多CPU来说,在同一时间内最多只允许一个CPU执行bh函数。
这种简化了的设计在一定程度上保证了从单CPU到多CPU SMP结构的平稳过渡,但随着时间的推移,就会发现这样的处理对于SMP的性能有不利的影响。因为,当系统中有很多个bh函数需要执行时,bh函数的“串行化”却只能使一个CPU执行一个bh函数,其它CPU即使空闲,也不能执行其它的bh函数。由此可以看出,bh函数的串行化是针对所有CPU的,根本发挥不出多CPU的优势。
那么,在新内核的设计中,是改进bh机制还是抛弃bh机制,建立一种新的机制?2.4选择了一种折中的办法,继续保留bh机制,另外增加一种或几种机制,并把它们纳入一个统一的框架中,这就是2.4内核中的软中断(softirq)机制。
2.软中断机制
软中断机制也是推迟内核函数的执行,然而,与bh函数严格地串行执行相比,软中断却在任何时候都不需要串行化。同一个软中断的两个实例完全有可能在两个CPU上同时运行。当然,在这种情况下,软中断必须是可重入的。软中断给网络部分带来的好处尤为突出,因为2.4内核中用两个软中断代替原来的一个NET_BH函数,这就使得在多处理机系统上软中断的执行更为高效。
3.Tasklet机制
另一个类似于bh的机制叫做tasklet。Tasklet建立在软中断之上,但与软中断的区别是,同一个tasklet只能运行在一个CPU上,而不同的tasklet可以同时运行在不同的CPU上。在这种情况下,tasklet就不需要是可重入的,因此,编写tasklet比编写一个软中断要容易。
Bh机制在2.4中依然存在,但不是作为一个单独的机制存在,而是建立在tasklet之上。因此,在2.4版中,设备驱动程序的开发者必须更新他们原来的驱动程序,用tasklet代替bh。
3.5.3数据结构的定义
在具体介绍软中断处理机制之前,我们先介绍一下相关的数据结构,这些数据结构大部分都在/includee/linux/interrupt.h中
1.与软中断相关的数据结构
软中断本身是一种机制,同时也是一种基本框架。在这个框架中,既包含了bh机制,也包含了tasklet机制
(1) 内核定义的软中断
| enum { HI_SOFTIRQ=0, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, TASKLET_SOFTIRQ }; |
内核中用枚举类型定义了四种类型的软中断,其中NET_TX_SOFTIRQ和NET_RX_SOFTIRQ两个软中断是专为网络操作而设的,而HI_SOFTIRQ和TASKLET_SOFTIRQ是针对bh和tasklet而设的软中断。编码的作者在源码注释中曾提到,一般情况下,不要再分配新的软中断。
(2)软中断向量
| struct softirq_action { void (*action)(struct softirq_action *); void *data; } static struct softirq_action softirq_vec[32] __cacheline_aligned; |
从定义可以看出,内核定义了32个软中断向量,每个向量指向一个函数,但实际上,内核目前只定义了上面的四个软中断,而我们后面主要用到的为HI_SOFTIRQ和TASKLET_SOFTIRQ两个软中断。
(3)软中断控制/状态结构
softirq_vec[]是个全局量,系统中每个CPU所看到的是同一个数组。但是,每个CPU各有其自己的“软中断控制/状态”结构,这些数据结构形成一个以CPU编号为下标的数组irq_stat[](定义在include/i386/hardirq.h中)
| typedef struct { unsigned int __softirq_pending; unsigned int __local_irq_count; unsigned int __local_bh_count; unsigned int __syscall_count; struct task_struct * __ksoftirqd_task; /* waitqueue is too large */ unsigned int __nmi_count; /* arch dependent */ } ____cacheline_aligned irq_cpustat_t; irq_cpustat_t irq_stat[NR_CPUS]; |
irq_stat[]数组也是一个全局量,但是各个CPU可以按其自身的编号访问相应的域。于是,内核定义了如下宏(在include/linux/irq_cpustat.h中):
#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_pending(cpu) __IRQ_STAT((cpu), __softirq_pending) #define local_irq_count(cpu) __IRQ_STAT((cpu), __local_irq_count) #define local_bh_count(cpu) __IRQ_STAT((cpu), __local_bh_count) #define syscall_count(cpu) __IRQ_STAT((cpu), __syscall_count) #define ksoftirqd_task(cpu) __IRQ_STAT((cpu), __ksoftirqd_task) /* arch dependent irq_stat fields */ #define nmi_count(cpu) __IRQ_STAT((cpu), __nmi_count) /* i386, ia64 */ |
2.与tasklet相关的数据结构
与bh函数相比,tasklet是“多序”的bh函数。内核中用tasklet_task来定义一个tasklet:
| struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; }; 从定义可以看出,tasklet_struct是一个链表结构,结构中的函数指针func指向其服务程序。内核中还定义了一个以CPU编号为下标的数组tasklet_vec[]和tasklet_hi_vec[]: struct tasklet_head { struct tasklet_struct *list; } __attribute__ ((__aligned__(SMP_CACHE_BYTES))); extern struct tasklet_head tasklet_vec[NR_CPUS]; extern struct tasklet_head tasklet_hi_vec[NR_CPUS]; 这两个数组都是tasklet_head结构数组,每个tasklet_head结构就是一个tasklet_struct结构的队列头。 |
3.与bh相关的数据结构
前面我们提到,bh建立在tasklet之上,更具体地说,对一个bh的描述也是tasklet_struct结构,只不过执行机制有所不同。因为在不同的CPU上可以同时执行不同的tasklet,而任何时刻,即使在多个CPU上,也只能有一个bh函数执行。
(1) bh的类型
enum {
TIMER_BH = 0, /* 定时器 */
TQUEUE_BH, /* 周期性任务队列 */
DIGI_BH, /* DigiBoard PC/Xe */
SERIAL_BH, /* 串行接口 */
RISCOM8_BH, /* RISCom/8 */
SPECIALIX_BH, /* Specialix IO8+ */
AURORA_BH, /* Aurora多端口卡(SPARC)*/
ESP_BH, /* Hayes ESP 串行卡 */
SCSI_BH, /* SCSI接口*/
IMMEDIATE_BH, /* 立即任务队列*/
CYCLADES_BH, /* Cyclades Cyclom-Y 串行多端口 */
CM206_BH, /* CD-ROM Philips/LMS cm206磁盘 */
JS_BH, /* 游戏杆(PC IBM)*/
MACSERIAL_BH, /* Power Macintosh 的串行端口 */
ISICOM_BH /* MultiTech的ISI卡*/
};
在给出bh定义的同时,我们也给出了解释。从定义中可以看出,有些bh与硬件设备相关,但这些硬件设备未必装在系统中,或者仅仅是针对IBM PC兼容机之外的某些平台。
(2) bh的组织结构
在2.4以前的版本中,把所有的bh用一个bh_base[]数组组织在一起,数组的每个元素指向一个bh函数:
static void (*bh_base[32])(void);
2.4版中保留了上面这种定义形式,但又定义了另外一种形式:
struct tasklet_struct bh_task_vec[32];
这也是一个有32个元素的数组,但数组的每个元素是一个tasklet_struct结构,数组的下标就是上面定义的枚举类型中的序号。
3.5.4 软中断、bh及tasklet的初始化
1.Tasklet的初始化
Tasklet的初始化是由tasklet_ init()函数完成的:
| void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data) { t->next = NULL; t->state = 0; atomic_set(&t->count, 0); t->func = func; t->data = data; } |
其中,atomic_set()为原子操作,它把t->count置为0。
2.软中断的初始化
首先通过open_softirq()函数打开软中断:
| void open_softirq(int nr, void (*action)(struct softirq_action*), void *data) { softirq_vec[nr].data = data; softirq_vec[nr].action = action; } |
然后,通过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的32个tasklet_struct,调用tasklet_init以后,它们的函数指针func全部指向bh_action()函数,也就是建立了bh的执行机制,但具体的bh函数还没有与之挂勾,就像具体的中断服务例程还没有挂入中断服务队列一样。同样,调用open_softirq()以后,软中断TASKLET_SOFTIRQ的服务例程为tasklet_action(),而软中断HI_SOFTIRQ的服务例程为tasklet_hi_action()。
3.Bh的初始化
bh的初始化是由init_bh()完成的:
| void init_bh(int nr, void (*routine)(void)) { bh_base[nr] = routine; mb(); } |
这里调用的函数mb()与CPU中执行指令的流水线有关,我们对此不进行进一步讨论。下面看一下几个具体bh的初始化(在kernel/sched.c中):
init_bh(TIMER_BH,timer_bh);
init_bh(TUEUE_BH,tqueue_bh);
init_bh(IMMEDIATE_BH,immediate_bh);
初始化以后,bh_base[TIMER_BH]处理定时器队列timer_bh,每个时钟中断都会激活TIMER_BH,在第五章将会看到,这意味着大约每隔10ms这个队列运行一次。bh_base[TUEUE_BH]处理周期性的任务队列tqueue_bh,而bh_base[IMMEDIATE_BH]通常被驱动程序所调用,请求某个设备服务的内核函数可以链接到IMMEDIATE_BH所管理的队列immediate_bh中,在该队列中排队等待。
3.5.5后半部分的执行
1.Bh的处理
当需要执行一个特定的bh函数(例如bh_base[TIMER_BH]())时,首先要提出请求,这是由mark_bh()函数完成的(在Interrupt.h中):
| static inline void mark_bh(int nr) { tasklet_hi_schedule(bh_task_vec+nr); } |
从上面的介绍我们已经知道,bh_task_vec[]每个元素为tasklet_struct结构,函数的指针func指向bh_action()。
接下来,我们来看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上,就把这个bh函数“调度”到哪个CPU上执行。另一方面,tasklet_struct代表着将要对bh函数的一次执行,在同一时间内,只能把它链入一个队列中,而不可能同时出现在多个队列中。对同一个tasklet_struct结构,如果已经对其调用了tasklet_hi_schedule()函数,而尚未得到执行,就不允许再将其链入该队列,所以标志位TASKLET_STATE_SCHED就是保证这一点的。最后,通过cpu_raise_softirq()发出软中断请求,其中的参数HI_SOFTIRQ表示bh与HI_SOFTIRQ软中断对应。
软中断HI_SOFTIRQ的服务例程为tasklet_hi_action():
| static void tasklet_hi_action(struct softirq_action *a) { int cpu = smp_processor_id(); struct tasklet_struct *list; local_irq_disable(); list = tasklet_hi_vec[cpu].list; tasklet_hi_vec[cpu].list = NULL; 临界区加锁 local_irq_enable(); while (list) { struct tasklet_struct *t = list; list = list->next; if (tasklet_trylock(t)) { if (!atomic_read(&t->count)) { if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) BUG(); t->func(t->data); tasklet_unlock(t); continue; } tasklet_unlock(t); } local_irq_disable(); t->next = tasklet_hi_vec[cpu].list; tasklet_hi_vec[cpu].list = t; __cpu_raise_softirq(cpu, HI_SOFTIRQ); local_irq_enable(); } } |
这个函数除了加锁机制以外,读起来比较容易。其中要说明的是t->func(t->data)语句,这条语句实际上就是调用bh_action()函数:
| /* BHs are serialized by spinlock global_bh_lock. t 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函数的执行又设置了两道锁。一是hardirq_trylock(),这是防止从一个硬中断内部调用bh_action()。另一道锁是spin_trylock()。这把锁就是全局量global_bh_lock,只要有一个CPU在这个锁所锁住的临界区运行,别的CPU就不能进入这个区间,所以在任何时候最多只有一个CPU在执行bh函数。至于根据bh函数的编号执行相应的函数,那就比较容易理解了。
2.软中断的执行
内核每当在do_IRQ()中执行完一个中断请求队列中的中断服务例程以后,都要检查是否有软中断请求在等待执行。下面是do_IRQ()中的一条语句:
| if (softirq_pending(cpu)) do_softirq(); 在检测到软中断请求以后,就要通过do_softirq()执行软中断服务例程,其代码在/kernel/softirq.c中: smlinkage void do_softirq() { int cpu = smp_processor_id(); __u32 pending; long flags; __u32 mask; if (in_interrupt()) return; local_irq_save(flags);/*把eflags寄存器的内容保存在flags变量中*/ pending = softirq_pending(cpu); if (pending) { struct softirq_action *h; mask = ~pending; local_bh_disable(); estart: /* Reset the pending bitmask before enabling irqs */ softirq_pending(cpu) = 0; local_irq_enable(); /*开中断*/ h = softirq_vec; do { if (pending & 1) h->action(h); h++; pending >>= 1; } while (pending); ocal_irq_disable(); / *关中断*/ pending = softirq_pending(cpu); if (pending & mask) { mask &= ~pending; goto restart; } __local_bh_enable(); if (pending) wakeup_softirqd(cpu); } local_irq_restore(flags); /*恢复eflags寄存器的内容*/ } |
从do_softirq()的代码可以看出,使CPU不能执行软中断服务例程的“关卡”只有一个,那就是in_interrupt(),这个宏限制了软中断服务例程既不能在一个硬中断服务例程内部执行,也不能在一个软中断服务例程内部执行(即嵌套)。但这个函数并没有对中断服务例程的执行进行“串行化”限制。这也就是说,不同的CPU可以同时进入对软中断服务例程的执行,每个CPU分别执行各自所请求的软中断服务。从这个意义上说,软中断服务例程的执行是“并发的”、多序的。但是,这些软中断服务例程的设计和实现必须十分小心,不能让它们相互干扰(例如通过共享的全局变量)。
从前面对软中断数据结构的介绍可以知道,尽管内核最多可以处理32个软中断,但目前只定义了四个软中断。在对软中断进行初始化时,soft_Init()函数只初始化了两个软中断TASKLET_SOFTIRQ和HI_SOFTIRQ,这两个软中断对应的服务例程为tasklet_action()和tasklet_hi_action()。因此,do_softirq()中的do_while循环实际上是调用这两个函数。前面已经给出了tasklet_hi_action()的源代码,而tasklet_action()的代码与其基本一样,在此不再给出。
3.5.6 把bh移植到tasklet
在Linux2.2中,对中断的后半部分处理只提供了bh机制,而在2.4中新增加了两种机制:软中断和tasklet。通过上面的介绍我们知道,同一个软中断服务例程可以同时在不同的CPU上运行。为了提高SMP的性能,软中断现在主要用在网络子系统中。多个tasklet可以在多个不同的CPU上运行,但一个CPU一次只能处理一个tasklet。Bh由内核进行了串行化处理,也就是在SPM环境中,某一时刻,一个bh函数只能由一个CPU来执行。如果要把Linux2.2中的bh移植到2.4的tasklet,请按下面方法进行:
1.Linux2.4中对bh的处理
假设一个bh为FOO_BH(FOO表示任意一个),其处理函数为foo_bh,则
(1)处理函数的原型为: void foo_bh(void);
(2)通过init_bh(FOO_BH, foo_bh)函数对foo_bh进行初始化
(3)通过mark_bh(FOO_BH)函数提出对foo_bh()的执行请求。
2.把bh移植到tasklet
(1)处理函数的原型为:void foo_bh(unsigned long data);
(2)通过宏 DECLARE_TASKLET_DISABLED(foo_tasklet, foo_bh, 0) 或
struct tasklet_struct foo_tasklet;
tasklet_init(&foo_tasklet, foo_bh, 0);
tasklet_disable(&foo_tasklet);
对foo_tasklet进行初始化
(3)通过
tasklet_enable(&foo_tasklet);
tasklet_schedule(&foo_tasklet)
对foo_tas


