1. 下半部
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。比如,如果上半部把数据从硬件拷贝到内存,那么当然应该在下半部中处理它们。如何决定什么任务在哪部分完成取决于驱动程序开发者自己的判断。注意,中断处理程序会异步执行,并且在最好的情况下它会锁定当前的中断线。这里有些建议:
1) 如果一个任务队时间非常敏感,将其放在中断处理程序中执行
2) 如果一个任务和硬件相关,将其放在中断处理程序中执行
3) 如果一个任务要保证不被其他中断打断,将其放在中断处理程序中执行
4) 其他所有任务。考虑放在下半部执行
上半部简单快速,执行的时候禁止一些或者全部中断。下半部稍后执行,而且执行期间尅响应所有的中断。
1.1. 下半部环境
上半部只有一种实现方法,就是中断处理程序。而下半部有多种机制实现。
最早的Linux只提供“bottom half”机制,耶被称为“BH”。它提供了一个静态创建、由32个bottom half组成的链表。每个BH都会在全局返回内进行同步。即使分属于不同的处理器也不允许任何两个bottom half同时执行。缺点,不过灵活,性能不高。
任务机制(task queue)机制来实现工作的推后执行,并用它来代替BH机制。内核定义了一组队列,其中每个队列都包含一个由等待调用的函数组成链表。但是,对于性能要求较高的子系统,像网络部分、这种机制就不能胜任。
2.3内核中引入了软中断(softirqs)和tasklet两种机制。软中断是一组静态定义的下半部接口,有32个,可以在所有处理器上同时执行——即使两个类型相同的也可以。而tasklet是一种基于软中断实现的灵活性强、动态创建的下半部实现机制。两个不同类型的tasklet可以在不同的处理器上同时执行,但是相同类型的tasklet不能同时执行。
2.5内核BH接口被弃置了,任务队列耶被工作队列取代了。所以,在2.6内核中,只有软中断、tasklet和工作队列。当然,还有内核定时器。
2. 软中断
软中断使用得比较少;而tasklet是下半部更常用的一种形式。软中断的代码位于kernel/softirq.c中。
2.1. 软中断的实现
软中断是在编译期间静态分配的。它由softirq_action结构体表示,定义在<linux/inerttupt.h>中。
struct softirq_action
{
void (*action) (struct softirq_action *);
void *data;
};
kernel/softirq.c中定义了一个32个该结构体的数组:
static struct softirq_action softirq_vec[32];
2.1.1. 软中断处理程序
函数声明如下:
void softirq_handler(struct soffirq_action *);
当内核运行一个软中断处理程序时,它就会执行这个action函数,其唯一的参数就是为指向相应softirq_action结构体的指针。例如,如果my_action指向softirq_vec数组的某项,内核就会这样调用软中断:
my_action->action(my_action);
一个软中断不会抢占另外一个软中断。实际上,唯一可以抢占软中断的是中断处理程序。其他的软中断,甚至是相同类型的软中断,可以在其他处理器上同时执行。
2.1.2. 执行软中断
一个注册的软中断必须在被标记后才会执行。这被称作触发软中断(raising the softirq)。通常,中断处理程序会在返回前标记它的软中断。在合适的时刻,待处理的会被检查和执行:
1) 从一个硬件中断代码返回时
2) 在ksoftirq内核线程中
3) 在那些显式检查和执行待处理的软中断代码中,如网络子系统
软中断都要在do_softirq中执行,其核心代码如下:
u32 pending = softirq_pending(cpu);
if (pending){
struct softirq_action *h= softirq_vec;
softirq_pending(cpu) = 0;
do {
if (pending &1)
h->action(h);
h++;
pending >>= 1;
}while(pending);
}
具体要做的包括:
1) 用局部变量pending保存softirq_pending宏返回的值。它是待处理的软中断的32位位图。如果第n位被置为1,那么第n位对应类型的软中断等待处理
2) 现在待处理的软中断位图已经保存,可以将实际的软中断位图清零了
3) 将指针h指向softirq_vec的第一项
4) 如果pending的第一位被置为1,h->action(h)被调用
5)指针加一
6) 位掩码右移一位
7) 现在指针h指向数组的第二项,重复上述操作
8) 一直重复下去,知道pending为0,表明已经没有待处理的软中断了
2.1.3. 使用软中断
软中断保留给系统中对时间要求最严格的下半部使用,目前,只有两个子系统(网络和SCSI)直接使用软中断。而内核定时器和tasklet都是建立在软中断上的。
1) 分配索引
在编译期间,可以通过<linux/interrupt.h>中定义了一个枚举类型来静态声明软中断。
tasklet |
优先级 |
软中断描述 |
HI_SOFTIRQ |
0 |
优先级高的tasklet |
TIMER_SOFTIRQ |
1 |
定时器的下半部 |
NET_TX__SOFTIRQ |
2 |
发送网络数据包 |
NET_RX__SOFTIRQ |
3 |
接收网络数据包 |
SCSI__SOFTIRQ |
4 |
SCSI的下半部 |
TASKLET_SOFTIRQ |
5 |
tasklet |
建立一个新的软中断必须在此枚举类型中加入新的项,注意顺序。习惯上,HI_SOFTIRQ是第一项,而TASKLET_SOFTIRQ是最后一项。
2) 注册你的处理程序
在运行时地调用open_softirq函数注册软中断处理程序,该函数有三个参数:软中断的索引号、处理函数和data域存放的数值。例如,网络子系统注册方式:
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。
3) 触发你的软中断
raise_softirq函数可以将一个软中断设置为挂起状态,让它在下次调用do_softirq函数时投入运行。例如,网络子系统调用如下接口:
raise_softirq(NET_TX_SOFTIRQ);
该函数在触发一个软中断之前先要禁止中断,触发后恢复原来的状态。如果中断本来就已经被禁止了,可以调用raise_softirq_irqoff函数来优化性能。
raise_softirq_irqoff (NET_TX_SOFTIRQ);
3. tasklet
tasklet是利用软中断实现的一种下半部机制。
3.1. tasklet的实现
我们知道,tasklet由两类软中断代表:HI_SOFTIRQ 和TASKLET_SOFTIRQ。
3.1.1 tasklet结构体
struct tasklet_struct{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
该结构体位于文件<linux/interrupt.h>中。成员func域是tasklet的处理程序,data是它唯一的参数。
成员state只能在0,TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。TASKLET_STATE_SCHED表明tasklet已经被调度,正准备投入运行,TASKLET_STATE_RUN表明该tasklet正在运行。
成员count是tasklet的引用计数。如果它不为0,则tasklet被禁止,不允许执行;只有当它为0时,tasklet才被激活,并且在被设置为挂起状态时,该tasklet才能够执行。
3.1.2. 调度tasklet
已经调度的tasklet存放在两个单处理器数据结构:tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)中。
tasklet由tasklet_schedule和tasklet_hi_schedule函数进行调度,它们接受一个指向tasklet_struct结构的指针作为参数。区别在于一个用于TASKLET_SOFTIRQ而一个用于HI_SOFTIRQ。先看tasklet_schedule函数的实现细节:
1) 检查tasklet的状态是否为TASKLET_STATE_SCHED。如果是,说明tasklet已经被调度过了,函数立即返回。
2) 保存中断状态,然后禁止本地中断。保证数据不会弄乱
3) 把需要调度的tasklet加到每个处理器一个的tasklet_vec或tasklet_hi_vec链表
4) 唤起TASKLET_SOFTIRQ和HI_SOFTIRQ软中断,这样在下一次调用do_softirq时就会执行该tasklet。
5) 恢复中断到原来状态并返回。
因为ASKLET_SOFTIRQ和HI_SOFTIRQ已经被触发了,所以do_softirq会执行相应的软中断处理程序。而这两个处理程序,tasklet_action和tasklet_hi_action,就是tasklet处理的核心。如下:
1) 禁止中断,并在当前处理器检索tasklet_vec或tasklet_hi_vec链表。
2) 将当前处理器上的该链表设置为NULL,达到清空的效果
3) 允许响应中断。
4) 循环遍历获得链表上的每一个待处理的tasklet。
5) 如果是多处理器系统,通过检查TASKLET_STATE_RUN状态标志来判断这个tasklet释放正在其他处理器上运行。如果它正在运行,那么现在就不要执行,跳到下一个待处理的tasklet去。
6) 如果当前这个tasklet没有执行,将其状态设置为TASKLET_STATE_RUN,这样别的处理器就不会再去执行它了。
7) 检查count是否为0,确保tasklet没有被禁止,如果tasklet被禁止,则跳到下一个挂起的tasklet去
8) 执行tasklet处理程序。
9) tasklet运行完毕,清除tasklet的state域的TASKLET_STATE_RUN状态标志
10) 重复执行下一个tasklet,直至没有剩余的等待处理的tasklet。
终上所述,所有的tasklet都通过重复运用TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断来实现。当一个tasklet被调度时,内核会唤醒这两个软中断中断一个。随后,该软中断会被特定的函数处理,执行所有已经调度的tasklet。这个函数保证同一时刻只有一个给定类型的tasklet会被执行。
3.2. 使用tasklet
tasklet可以静态创建,也可以动态创建,使用方便,执行起来也算快。
3.2.1. 声明tasklet
如果你准备静态创建一个tasklet(直接引用),使用<linux/interrupt.h>中定义的两个宏的一个:
DECLARE_TASKLET(name,func,data);
DECLARE_TASKLET_DISABLED(name, func,data);
当该tasklet被调度以后,给定的函数func就会执行,它的参数由data给出。这两个宏的区别是引用计数器的初始值设置不同,前者设置为0,该tasklet处于激活状态,后者设置为1,tasklet处于禁止状态。例如
DECLARE_TASKLET(my_tasklet,my_tasklet_handler,dev);
动态创建一个tasklet(间接引用),使用下面接口:
tasklet_init(t, tasklet_handler,dev);
3.2.2. tasklet处理程序
tasklet处理程序必须符合规定的函数类型:
void tasklet_handler(unsigned long data);
因为是靠软中断来实现,所以tasklet不能睡眠,这就意味着不能在tasklet中使用信号量或其他非阻塞式函数。两个相同的tasklet决不会同时执行。
3.2.3. 调度tasklet
通过调用tasklet_schedule函数传递它相应的tasklet_struct的指针,如下:
tasklet_schedule(&my_tasklet);
在tasklet被调度后,只要有机会它就会尽可能早地运行。如果在它还没有执行前又被调度了,它仍然只会运行一次。如果这时它已经运行了,比如说在另外一个处理器上,那么这个新的tasklet会被重新调度并再次运行。
可以调用tasklet_disable函数来禁止某个指定的tasklet。如果该tasklet当前正在执行,这个函数会等到它执行完毕再返回。也可以调用tasklet_disable_nosync函数,无需等待tasklet执行完毕。这往往不太安全,因为无法知道tasklet是否仍在执行。调用tasklet_enable可以激活一个tasklet。
可以调用tasklet_kill函数从挂起的队列中去掉一个tasklet。该函数的参数是一个指向某个tasklet的tasklet_struct的长指针。在处理一个经常重新调度它自身的tasklet的时候,这个函数很有用。这个函数首先等待该tasklet执行完毕,然后再将它移去。当然,无法阻止在其他地方的代码重新调度该tasklet。由于该函数可能会引起休眠,所以禁止在中断上下文中使用。
3.3. ksoftirqd
每个处理器都有一组辅助处理软中断(和tasklet)的内核线程。对于软中断,被触发的频率有时可能很高,并且执行的时候,软中断本身也会触发自己(网络子系统),那么就会导致用户空间进程无法获得足够的处理器时间,因而处于饥饿状态。有两种最容易想到的方案:一是只要还有被触发并等待处理的软中断,本次执行就要负责处理,重新触发的软中断也在本次执行返回前被处理;二是选择不处理重新触发的软中断,任何自行重新触发的软中断都不会马上处理,而是被放到下一个软中断时机去处理。
作为改进,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低的优先级(nice是19)运行。
每个处理器都有一个这样的线程。所有的线程的名字都叫做ksoftirq/n,区别在于n,它对应的是处理器的编号。一旦线程被初始化,就执行这样的死循环:
for(;;){
if(!softirq_pending(cpu))
schedule();
set_current_state(TASK_RUNNING);
while(softirq_pending(cpu)){
do_softirq();
if(need_resched())
schedule();
}
set_current_state(TASK_INTERRUPTIBLE);
}
只要有待处理的软中断,ksoftirq机会调用do_softirq去处理它们。通过重复执行这样的操作,重新触发的软中断也会被执行。
3.4. 老的BH机制
所有BH都是静态定义的,最多有32个。每个BH处理程序都严格地按顺序执行,不允许任何两个BH处理程序同时执行。这样做倒使同步简单,却不利于多处理器的可扩展性。