1 概述
从linux1.x版本开始,中断处理程序从概念上被分为上半部分(top half)和下半部分(bottom half)。在中断发生时上半部分的处理过程立即执行,因为它是完全屏蔽中断的,所以要快,否则其它的中断就得不到及时的处理。但是下半部分(如果有的话)几乎做了中断处理程序所有的事情,可以推迟执行。内核把上半部分和下半部分作为独立的函数来处理,上半部分的功能就是“登记中断”,决定其相关的下半部分是否需要执行。需要立即执行的部分必须位于上半部分,而可以推迟的部分可能属于下半部分。下半部分的任务就是执行与中断处理密切相关但上半部分本身不执行的工作,如查看设备以获得产生中断的时间信息,并根据这些信息(一般通过读设备上的寄存器得来)进行相应的处理。从这里我们可以看出下半部分其实是上半部分引起的,例如当打印机端口产生一个中断时,其中断处理程序会立即执行相关的上半部分,上半部分就会产生一个软中断(下半部分的一种)并送到操作系统内核里,这样内核就会根据这个软中断唤醒睡眠的打印机任务队列中的处理进程。
它们最大的不同是上半部分不可中断,而下半部分可中断。在理想的情况下,最好是中断处理程序上半部分将所有工作都交给下半部分执行,这样的话在中断处理程序上半部分中完成的工作就很少,也就能尽可能快地返回。但是,中断处理程序上半部分一定要完成一些工作,例如,通过操作硬件对中断的到达进行确认,还有一些从硬件拷贝数据等对时间比较敏感的工作。剩下的其他工作都可由下半部分执行。
对于上半部分和下半部分之间的划分没有严格的规则,靠驱动程序开发人员自己的编程习惯来划分,但是还是有一些习惯供参考:
Ⅰ.如果该任务对时间比较敏感,将其放在上半部中执行。
Ⅱ.如果该任务和硬件相关,一般放在上半部中执行。
Ⅲ.如果该任务要保证不被其他中断打断,放在上半部中执行(因为这是系统关中断)。
Ⅳ.其他不太紧急的任务, 一般考虑在下半部执行。
下半部分并不需要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太忙并且中断恢复后执行就可以了。通常下半部分在中断处理程序一返回就会马上运行,下半部分执行的关键在于当它们运行的时候,允许响应所有的中断。下半部分是一种推后执行任务,它将某些不那么紧迫的任务推迟到系统更方便的时刻运行。内核中实现下半部的手段不断演化,目前已经从最原始的BH(bottom half)衍生出BH(在2.5中去除)、软中断(softirq在2.3引入)、tasklet(在2.3引入)、工作队列(work queue在2.5引入)。这里主要分析讨论后三种方式。
2 linux-2.4、2.6中断处理程序下半部分的比较
2.1下半部分调度时机的比较:
linux下半部分最直接的调用是当硬中断执行完后,迅速调用do_softirq()函数来执行软中断(见下面的代码),这样,被硬中断标注的软中断能得以迅速执行。当然,不是每次调用都能成功。
2.4.6版本以前在从系统调用和异常返回前都要检查系统内是否有下半部分要执行;2.4.6版本的内核到现在,只有中断处理主函数do_irq()中才调用下半部分处理函数,而在系统调用和异常返回前不再检测系统内是否有挂起的下半部分。也就是说,只有硬件中断发生了,下半部分处理函数才得到调用。这些我们可以从下面的源代码中看出:
(2.4.0-2.4.5内核 arch/i386/kernel/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
testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx #else
movl SYMBOL_NAME(irq_stat),%ecx
testl SYMBOL_NAME(irq_stat)+4,%ecx
#endif
jne handle_softirq
ret_from_exception:
#ifdef CONFIG_SMP
GET_CURRENT(%ebx)
movl processor(%ebx),%eax
shll $CONFIG_X86_L1_CACHE_SHIFT,%eax
movl SYMBOL_NAME(irq_stat)(,%eax),%ecx testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx
#else
movl SYMBOL_NAME(irq_stat),%ecx
testl SYMBOL_NAME(irq_stat)+4,%ecx
#endif
jne handle_softirq
handle_softirq:
call SYMBOL_NAME(do_softirq)
jmp ret_from_intr
其中的irq_stat, irq_stat +4 对应的就是字段__softirq_active和__softirq_mask,它们定义在asm-i386/hardirq.h文件中。
__softirq_active和__softirq_mask就是用于触发和控制软中断的成员变量。
①__softirq_active变量:32位的无符号整数,表示软中断向量0~31的状态。如果bit[i](0≤i≤31)为1,则表示软中断向量i在某个CPU上已经被触发而处于active状态;为0表示处于非活跃状态。
②__softirq_mask变量:32位的无符号整数,软中断向量的屏蔽掩码。如果bit[i](0≤i≤31)为1,则表示使能(enable)软中断向量i,为0表示该软中断向量被禁止(disabled)。
(2.4.0-2.4.5 内核 arch/i386/kernel/irq.c)(中断处理程序返回前检测是否有挂起的下半部分)
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
{
…
if (softirq_active(cpu) & softirq_mask(cpu))
do_softirq();
return 1;
}
软中断softirq不象硬件中断那样是由硬件中断信号触发执行的,所以也不同于硬件中断那样时随时都能够被执行,笼统来讲软中断会在内核处理任务完毕后返回用户级程序前得到处理机会。2.4.6版本以前每次调用完硬中断后都马上调用软中断,在系统调用、异常返回前以及schedule调度也都要检测是否有挂起的软中断,有则执行。原因主要有以下几方面的:
(1)在系统调用或者异常处理中同样可以标注软中断,这样它们在返回前就能得以迅速执行
(2)前面提到,有些软中断要延迟到下次陷入内核才能执行,系统调用和异常都陷入内核,所以可以尽早的把软中断处理掉
(3)如果在异常或者系统调用中发生中断,那么前面提到,可能还会有一些软中断没有处理,在这两个地方做一个补救工作,尽量避免到下次陷入内核才处理这些软中断。
(2.4.6 内核 arch/i386/kernel/entry.s)(2.4.6版本以后系统调用、异常返回前没有检测是否有挂起的下半部分)
ENTRY (ret_from_sys_call)
cli
cmpl $0,need_resched(%ebx)
jne reschedule
cmpl $0,sigpending(%ebx)
jne signal_return
…
ret_from_exception:
movl EFLAGS(%esp),%eax
movb CS(%esp),%al
testl $(VM_MASK | 3),%eax
jne ret_from_sys_call
jmp restore_all
(2.4.6-- 内核 arch/i386/kernel/irq.c)(中断处理程序返回前检测是否有挂起的下半部分)
asmlinkage unsigned int do_irq(struct pt_regs regs) {
…
if(!action)
goto out;
…
out:
desc->handler->end(irq);
spin_unlock(&desc->lock);
if(softirq_pending(cpu))
do_softirq();
return 1;
}
2.6版本中待处理的软中断在下面三个地方会被检测和执行:
Ⅰ.在处理完一个硬件中断以后
Ⅱ.在ksoftirqd内核线程中
Ⅲ.在那些显示检查和执行待处理的软中断的代码中,如网络子系统中
2.2 系统中do_softirq()函数执行上的不同:
2.4版本和2.6版本都是在响应硬件中断以后的do_irq()函数中执行下半部,两者略有差别(2.6版本是在do_irq()函数先调用了irq_exit()函数,而该函数才真正调用了do_softirq()函数):
asmlinkage unsigned int do_irq() {
…
irq_exit();
…}
#define irq_exit() (include/asm-i386/hardirq.h)
do{ …
if (!in_interrupt() && softirq_pending(smp_processor_id()))
do_softirq();
…}while(0)
总之:2.4版本以后软中断执行的共同之处就是都在do_irq()函数中调用do_softirq()函数,但do_softirq()函数执行时,2.4.6版本之前和其之后就又有了很大的区别,我们可以从下面的代码中看出:
(2.4.6内核 kernel/softirq.c)
asmlinkage void do_softirq()
{ int cpu=smp_processor_id();
_u32 pending;
…
restart:
…
h=softirq_vec;
do{
if(pending&1)
h->action(h);
h++;
pending>>=1;
}while(pending)
local_irq_disable();
pending=softirq_pending(cpu);
if(pending)
goto restart;
…}
(2.4.7内核 kernel/softirq.c)
asmlinkage void do_softirq()
{ …
pending=softirq_pending(cpu);
if(pending&mask)
mask&=~pending;
goto restart;
}
local_bh_enable();
if(pending)
wakeup_softirqd(cpu);
}
static inline void wakeup_softirqd(unsigned cpu)
{
struct task_struct * tsk = ksoftirqd_task(cpu);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
从源代码中我们可以看出2.4.6版本以后从系统调用、异常返回前已经不再检测是否有软中断要执行,只在响应硬件中断以后do_irq()函数中的do_softirq()函数检测是否有挂起的软中断,有则执行相应的处理程序;但是为了考虑软中断执行的效率,2.4.6版本以后有了执行软中断的后台线程ksoftirqd,它运行软中断处理函数do_softirq()。只要有待处理的软中断,ksoftirqd就会调用do_softirq()函数去处理它们。通过重复执行这样的操作,重新触发的软中断也会被执行。
3 下半部分的几种主要实现机制的比较:
3.1 softirq机制
软中断支持SMP,同一个softirq可以在不同的CPU上同时运行,softirq必须是可重入的。软中断是在编译期间静态分配的,它不像tasklet那样能被动态的注册或去除。kernel/softirq.c中定义了一个包含32个softirq_action结构体的数组。每个被注册的软中断都占据该数组的一项。因此最多可能有32个软中断。2.4版本的内核定义了四个软中断:HI_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、TASKLET_SOFTIRQ;2.6版本的内核中定义了六个软中断:HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、SCSI_SOFTIRQ、TASKLET_SOFTIRQ。
一般情况下,在硬件中断处理程序后都会试图调用do_softirq()函数,每个CPU都是通过执行这个函数来执行软中断服务的。由于软中断不能进入硬中断部分,且同一个CPU上软中断的执行是串行的,即不允许嵌套,因此,do_softirq()函数一开始就检查当前CPU是否已经正出在中断服务中,如果是则 do_softirq()函数立即返回。这是由do_softirq()函数中的 if (in_interrupt()) return; 保证的。其中
#define in_interrupt() ({int _cpu=smp_processor_id(); \
(local_irq_count(_cpu)+local_bh_count(_cpu)!=0); }) (asm-i386/hardirq.h)
3.2 tasklet机制
引入tasklet,最主要的是考虑支持SMP,提高SMP多个cpu的利用率;不同的tasklet可以在不同的cpu上运行。tasklet可以理解为softirq的派生,所以它的调度时机和软中断一样。对于内核中需要延迟执行的多数任务都可以用tasklet来完成,由于同类tasklet本身已经进行了同步保护,所以使用tasklet比软中断要简单的多,而且效率也不错。tasklet把任务延迟到安全时间执行的一种方式,在中断期间运行,即使被调度多次,tasklet也只运行一次,不过tasklet可以在SMP系统上和其他不同的tasklet并行运行。在SMP系统上,tasklet还被确保在第一个调度它的CPU上运行,因为这样可以提供更好的高速缓存行为,从而提高性能。
与一般的软中断不同,某一段tasklet代码在某个时刻只能在一个CPU上运行,但不同的tasklet代码在同一时刻可以在多个CPU上并发地执行。Kernel/softirq.c中用tasklet_trylock()宏试图对当前要执行的tasklet(由指针t所指向)进行加锁,如果加锁成功(当前没有任何其他CPU正在执行这个tasklet),则用原子读函数atomic_read()进一步判断count成员的值。如果count为0,说明这个tasklet是允许执行的。如果tasklet_trylock()宏加锁不成功,或者因为当前tasklet的count值非0而不允许执行时,我们必须将这个tasklet重新放回到当前CPU的tasklet队列中,以留待这个CPU下次服务软中断向量TASKLET_SOFTIRQ时再执行。为此进行这样几步操作:(1)先关 CPU中断,以保证下面操作的原子性。(2)把这个tasklet重新放回到当前CPU的tasklet队列的首部;(3)调用__cpu_raise_softirq()函数在当前CPU上再触发一次软中断请求TASKLET_SOFTIRQ;(4)开中断。
软中断和tasklet都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和tasklet不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。也正是由于它们运行在中断上下文中,所以它们在同一个CPU上的执行是串行的,这样就不利于实时多媒体任务的优先处理。
3.3 工作队列
工作队列是Linux 2.6 内核中新增加的一种下半部机制。它与其它几种下半部分机制最大的区别就是它可以把工作推后,交由一个内核线程--工作者线程(内核线程)去执行。内核线程只在内核空间运行,没有自己的用户空间,它和普通进程一样可以被调度,也可以被抢占。该工作队列总是会在进程上下文执行。缺省的工作者线程叫做events/n,n是处理器的编号。如果要在工作者线程中执行大量的处理操作时,可以创建属于自己的工作者线程。这样,通过工作队列执行的代码能占尽进程上下文的所有优势,最重要的就是工作队列允许重新调度甚至是睡眠。
由于softirq和tasklet在同一个CPU上的串行执行,不利于多媒体实时任务和其它要求严格的任务的处理。在有些系统中采用了新的工作队列机制取代软中断机制来完成网络接收中断后的推后处理工作。通过由具有最高实时优先级的工作者线程来处理实时多媒体任务或其它要求较高的任务,而由优先级次高的工作者线程来处理其他的非实时数据业务。Linux 2.6 内核的调度系统采用了内核抢占和O(1)调度,能够满足软实时的要求,因此几乎总能保证处理实时多媒体任务或要求较高任务的工作者线程优先执行。这样,就保证了多媒体实时任务或要求较高任务得到优先的处理。
工作队列靠内核线程来运行,可能会引起上下文切换(当任务睡眠、阻塞需要重新调度时),这样它造成的开销也比较大。
4 结束语
基于上面的分析我们可知,在各种不同的下半部实现机制中,tasklet基于软中断实现,其实是一种在性能和易用性之间寻求平衡的机制,它和软中断相似。对于大部分下半部处理来说,tasklet就够了,但从设计的角度考虑,软中断提供的执行序列化的保障最少,所以软中断处理函数必须采取一些确保共享数据安全的措施,因为多个软中断可能在不同的处理器上同时执行。如果代码本身的安全工作做得足够好或对于时间要求严格和执行频率高的应用(如网络子系统),软中断是个不错的选择。如果代码重入考虑的不充分,选择tasklet,它的接口简单,实现起来也简单。如果需要把任务推后到进程上下文中完成,任务可能会睡眠、阻塞、重新调度等,只能选择工作队列。
通过详细了分析linux-2.4、2.6中断处理程序下半部分的调度时机和各种实现机制的优缺点,就能在驱动程序编程中选择简单、有效的下半部分实现机制。只有充分理解linux-2.4、2.6中断处理程序下半部分实现机制的不同,才能在驱动程序的开发中发挥优势,为内核编程打下坚实的基础。
阅读(1287) | 评论(0) | 转发(0) |