分类: LINUX
2010-05-15 11:28:50
软中断和小任务机制
摘要:通常CPU在执行中断例程时,为了防止中断嵌套,通常都是关闭对中断响应。但是如果中断处理如果涉及到处理过程复杂甚至是等待I/O操作,那么对应的关中断时间会大大延长,同时使得CPU丢失许多处理其他外部事件的能力。文中介绍的在LINUX系统中实现的软中断(sotfirq)和小任务(tasklet)机制就是对传统中断处理方式的一种改进。
0. 引言
中断处理是任何一个现代操作系统的重要组成部分,它使得高速CPU与低速外设之间取得良好的速度匹配,从而在一定程度上提高了CPU的利用率。然而随着应用环境的多样性,传统的中断处理的局限性渐渐出现:
1. 由于中断发生的随机性和程序执行的异步性,中断响应会打断其他代码执行。
2. 通常中断处理都是在关中断方式下进行,这样CPU就失去了响应其他中断能力
3. 通常中断都是与硬件操作有关CPU速度较快,有较高的时限要求,而中断不是在进程上下文中,不能进行阻塞和休眠。
针对于这种情况在LINUX系统里就把中断处理认为划分为两个阶段,前一部分主要是针对硬件做出快速响应,至于更多的处理工作留为下一部分处理,从而在最大限度减少关中断时间。这里”下一部分”就是本文描述的软中断处理。
软中断是相对于中断处理的快速硬件处理部分而言的。早期的LINUX版本中把中断的第二部分处理机制称作BH(bottom half)机制,它提供了一种静态的链表,通过第一部分处理后,置相关标志告诉后面如何处理。BH链表是全局性的,在不同的CPU上仍然不能并发执行。与早期的BH机制一样,软中断仍然采用的是静态注册的方法,并且是全局数组模式。正是因为如此,系统中直接使用软中断处理的模块必须是对时限和性能要求比较苛刻的模块(网络和SCSI)。而其他的都借助于小任务机制(tasklet)实现,从而达到并发执行的目的。
1.1小任务(tasklet)机制
正是基于传统的软中断效率低下和在不同CPU上并发执行难度较大考虑,最新的内核中出现了现在的基于软中断的tasklet_let小任务机制。它在软中断的基础之上提供了一种动态的注册方法,允许用户动态插入和删除tasklet,这样在性能和易用性角度都得到了满足。
static struct softirq_action softirq_vec[32];
softirq_vec内部元素定义为:
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
这里定义了32个静态函数指针实际只使用7个枚举如下
enum
{
HI_SOFTIRQ=0, /*高优先级tasklet*/
TIMER_SOFTIRQ, /* 时钟软中断 */
NET_TX_SOFTIRQ, , /* 网络发送软中断 */
NET_RX_SOFTIRQ, /* 网络接收软中断 */
SCSI_SOFTIRQ, /* scsi总线软中断 */
TASKLET_SOFTIRQ /*低优先级tasklet*/
};
这里区分了两种级别的tasklet,其实本质上没有区别,只不过在软中断在遍历执行函数时候总是从softirq_vec[0]开始的,从而虚拟出其类似的高优先级hi_tasklet。每个软件中断向量表第一级下挂处理函数指针目前共有7个,其中针对tasklet的主要有两个tasklet_action和tasklet_hi_action由函数softirq_init挂接见图1中①。这两个函数指针同其他几个主要区别是这两个在执行具体处理时候,由于在每个CPU上都对应了两个静态处理链表task_hi_vec[nr_cpu]和task_vec[nr_cpu]。在执行小任务机制时候在关闭本地CPU软件中断,而不影响其他CPU上响应软件中断。从而实现多CPU下并发执行,而其他几个在同一时刻只可以在一个CPU上执行。
小任务链表
tasklet_head* tasklet_vec[cpu]和tasklet_head* tasklet_hi_vec[cpu]
这是在每个CPU上建立的.每个元素是一个相对本地CPU在处理tasklet时的头指针tasklet_head,指向的类型是tasklet_struct,定义如下:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
其中tasklet_head*就是指向每一个具体的tasklet,在tasklet_schedule调用时候把一个tasklet_struct挂入当前CPU的tasklet_vec[cpu]或者tasklet_hi_vec[cpu]。见图1中②。
。在tasklet_init时候再把用户针对某一个特定tasklet回调处理挂接入其函数指针func图1中③。
初始化softirq时系统将两个tasklet入口函数tasklet_action和tasklet_hi_action挂入软中断处理入口见图1中①:
综合上面的分析,可以得出从softirq静态注册到用户tasklet动态注册完整流程见 图 1。
从上面的描述和图1可知,本质上,tasklet机制是借助于sotfirq实现,因此对于tasklet调度其实是执行sotfirq的一部分,也就是说总是从执行软中断开始,才可以遍历到自己注册的tasklet,如下图2。
图2
从图3可以看到, 在执行软中断服务例程时候,本CPU硬件中断都是打开的(local_irq_enable),而本CPU软件中断都是关闭的(local_bh_disable)从而实现不同tasklet之间并发,而不影响当前CPU对于其他硬件中断的响应。
同时在执行软中断时候,系统专门激活了一个名叫ksotfirq的线程,这个线程专门用来处理softirq。当发现在轮询多次执行软中断处理函数后,仍然发现有软中断未处理,此时系统将自动激活这个线程,等待在下一次调度时候再继续执行sotfirq,从而避免在短时间内sotfirq占有资源过多。
通常我们不能直接使用或者定义软中断,因为软中断向量表在系统初始化中就已经确定。如果需要相关类似功能就需要实现自己的tasklet,挂在tasklet链表上等待调度,运用tasklet框架机制实现相关功能。Tasklet机制描述起来比较复杂,但是实际使用则相对简单的多。比如实现一个名称为my_tasklet的tasklet,其处理函数为my_tasklet_handler, 则图3描述了一个完整的实现流程:
图3
小任务Tasklet机制在软中断softirq的基础上把softirq的单一静态注册处理函数改进为为动态链表的函数指针挂接,从而使得tasklet机制实现和处理更加灵活。正是由于在每个CPU上建立了tasklet链表指针,使得一个tasklet可以和其他tasklet并发,但是同一个tasklet却是串行化。而且对于同一个tasklet在未调度之前无论多少次请求,系统只会执行一次,这一点需要注意。同时由于执行sotfirq期间硬件中断是打开的,所以大大提高了系统对中断的快速响应能力。