7.tasklet
如前一小节所述,判断一个类型的软中断处理函数是否可以执行的条件是软中断挂起位图__softirq_pending的相应bit是否被置1。在多处理器系统中,每个处理器拥有各自的软中断挂起位图__softirq_pending变量。由于执行软中断过程中是开启本地处理器的硬件中断并禁止本地处理器的软中断。因此,如果同一个软中断在执行过程中又被触发,那么另外一个处理器就有机会同时执行该软中断处理程序,这有助于提高软中断效率,但对于软中断处理函数的设计必须是完全可重入且线程安全的,对临界区做必要的加锁保护,从而增加了代码设计复杂度。
tasklet是另一种中断下半部的实现方式。tasklet是通过软中断实现的,其本质上就是软中断,不过它的代码设计复杂度要比软中断低,接口更简单。因此除非对性能要求很高像网络系统那样,否则实现中断下半部的方式一般选择tasklet。
tasklet_struct结构体用于描述tasklet,其定义如下:
-
<include/linux/interrupt.h>
-
struct tasklet_struct
-
{
-
struct tasklet_struct *next; //指向链表中下一个tasklet结构体
-
unsigned long state; //tasklet状态
-
atomic_t count; //引用计数器
-
void (*func)(unsigned long); //tasklet处理函数
-
unsigned long data; //tasklet处理函数的参数
-
};
其中state有2种可能的状态,分别是TASKLET_STATE_SCHED和TASKLET_STATE_RUN。TASKLET_STATE_SCHED表示tasklet已被调度等待执行;TASKLET_STATE_RUN表示该tasklet正在执行,该状态仅在多处理器系统中有效。
count是原子的引用计数器,用于禁止已调度的tasklet。如果count值不为0,则tasklet被禁止,不允许执行;只有当它为0时,tasklet才被激活,并且被设置为TASKLET_STATE_SCHED状态时,该tasklet才能够执行。
func函数指针指向tasklet的处理函数,data是给处理函数传递的参数。
内核中有两种创建tasklet的方式:静态创建和动态创建。内核提供了如下两个宏用于静态创建tasklet:
-
<include/linux/interrupt.h>
-
#define DECLARE_TASKLET(name, func, data) \
-
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
-
-
#define DECLARE_TASKLET_DISABLED(name, func, data) \
-
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
这两个宏的唯一区别就是count引用计数的初值设置。前一个宏把count设置为0,表示该tasklet处于激活状态;后一个宏把count设置为1,表示该tasklet处于禁止状态。
在内核中也可以通过一个tasklet指针动态创建一个tasklet结构,并使用如下接口来初始化一个tasklet:
-
<kernel/softirq.c>
-
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;
-
}
开发人员在定义并初始化一个tasklet实例后,必须将其注册到系统后才有机会被执行。内核中维护了两个per-CPU型的tasklet管理链表tasklet_vec和tasklet_hi_vec,分别定义如下:
-
<kernel/softirq.c>
-
struct tasklet_head
-
{
-
struct tasklet_struct *head;
-
struct tasklet_struct **tail;
-
};
-
-
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
-
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
还记得前面介绍软中断时提到的枚举类型吗?其中HI_SOFTIRQ和TASKLET_SOFTIRQ用于实现tasklet,所以tasklet_hi_vec与tasklet_vec的唯一区别就在于tasklet_hi_vec对应HI_SOFTIRQ而tasklet_vec对应TASKLET_SOFTIRQ。在执行软中断时,HI_SOFTIRQ优先于TASKLET_SOFTIRQ执行。所以开发人员可以根据优先级的需求来决定自己创建的tasklet注册到tasklet_hi_vec还是tasklet_vec中。
由于tasklet_hi_vec和tasklet_vec非常类似,所以接下来仅分析与tasklet_vec相关的内容。内核中提供了注册tasklet的接口:
-
<include/linux/interrupt.h>
-
static inline void tasklet_schedule(struct tasklet_struct *t)
-
{
-
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
-
__tasklet_schedule(t);
-
}
-
-
void __tasklet_schedule(struct tasklet_struct *t)
-
{
-
unsigned long flags;
-
// 为了避免多个内核控制路径共同修改tasklet_vec,必须禁止本地cpu中断
-
local_irq_save(flags);
-
t->next = NULL;
-
//将待注册tasklet实例添加到tasklet_vec管理链表
-
*__get_cpu_var(tasklet_vec).tail = t;
-
__get_cpu_var(tasklet_vec).tail = &(t->next);
-
//激活TASKLET_SOFTIRQ软中断,确保下次调用do_softirq时执行该tasklet
-
raise_softirq_irqoff(TASKLET_SOFTIRQ);
-
local_irq_restore(flags);//开启本地cpu中断
-
}
tasklet_schedule首先检查待注册tasklet的状态字段是否已经被设置为TASKLET_STATE_SCHED。如果是,则表示该tasklet已经被注册,不需要重新注册;如果不是,则将该tasklet的状态字段设置为TASKLET_STATE_SCHED并调用__tasklet_schedule将tasklet结构体实例添加到tasklet_vec管理链表中。
由于tasklet是基于软中断实现的,因此要执行tasklet需要一个软中断处理函数来调用tasklet的处理函数。在内核启动过程中调用了softirq_init函数来完成一些软中断相关的初始化工作:
-
<kernel/softirq.c>
-
void __init softirq_init(void)
-
{
-
int cpu;
-
-
for_each_possible_cpu(cpu) {
-
int i;
-
per_cpu(tasklet_vec, cpu).tail =
-
&per_cpu(tasklet_vec, cpu).head; //初始化tasklet_vec链表
-
per_cpu(tasklet_hi_vec, cpu).tail =
-
&per_cpu(tasklet_hi_vec, cpu).head; //初始化tasklet_hi_vec链表
-
for (i = 0; i < NR_SOFTIRQS; i++)
-
INIT_LIST_HEAD(&per_cpu(softirq_work_list[i], cpu));
-
}
-
-
register_hotcpu_notifier(&remote_softirq_cpu_notifier);
-
-
open_softirq(TASKLET_SOFTIRQ, tasklet_action); //注册tasklet的软中断处理函数
-
open_softirq(HI_SOFTIRQ, tasklet_hi_action); //注册高优先级tasklet的软中断处理函数
-
}
以tasklet_action为例,分析如何执行已注册到tasklet_vec中的tasklet。
-
static void tasklet_action(struct softirq_action *a)
-
{
-
struct tasklet_struct *list;
-
-
/*由于软中断处理函数是在开中断环境下执行的,所以为了避免多个内核控制路径修改
-
*tasklet_vec,此处要禁止本地cpu中断。
-
*/
-
local_irq_disable();
-
list = __get_cpu_var(tasklet_vec).head;//使用局部变量list保存tasklet_vec链表表头指针
-
__get_cpu_var(tasklet_vec).head = NULL;//将tasklet_vec链表清空
-
__get_cpu_var(tasklet_vec).tail = &__get_cpu_var(tasklet_vec).head;
-
local_irq_enable();//开启本地cpu中断
-
-
//遍历tasklet_vec链表
-
while (list) {
-
struct tasklet_struct *t = list;
-
-
list = list->next;
-
/*tasklet_action作为一个软中断处理函数,在多处理器系统中可能在不同的cpu上
-
*同时运行。虽然TASKLET_STATE_SCHED标志能够阻止同一个tasklet被多次注册,
-
*但当执行tasklet处理函数之前会清除该标志。如果在清除TASKLET_STATE_SCHED
-
*标志后系统发生了中断,该中断由另一个cpu处理并且在其中断处理函数中再次
-
*注册该tasklet到这个cpu的tasklet_vec链表中。这就有可能造成同一个tasklet
-
*的处理函数在不同的cpu上同时执行。如果这样的话就与软中断一样了,失去了
-
*tasklet的意义了。因此为了避免这种情况发生,在多处理器系统中为tasklet添
-
*加了一个额外的状态TASKLET_STATE_RUN,表示当前tasklet是否正在执行。如果
-
*tasklet没有正在执行,则tasklet_trylock返回真并且将tasklet的state设置为
-
*TASKLET_STATE_RUN状态,保证其他cpu不会有机会执行tasklet处理函数。这种
-
*功能类似于加锁,保证tasklet即使在多处理器系统中也能串行化执行,即同一
-
*时刻同一个tasklet只能在一个cpu上执行。因此在实现tasklet处理函数时不必
-
*考虑多个cpu并发执行的问题,降低了程序设计复杂度,这是tasklet相对于软
-
*中断的优势。
-
*/
-
if (tasklet_trylock(t)) {
-
//判断count是否为0,如果为0,则表示该tasklet没有被禁止可以执行。
-
if (!atomic_read(&t->count)) {
-
/*测试该tasklet是否被设置为TASKLET_STATE_SCHED ,每一个注册的
-
*tasklet都会被设置为TASKLET_STATE_SCHED,如果被测试出该tasklet没
-
*有设置TASKLET_STATE_SCHED,则说明tasklet_action 正在试图执行一个
-
*没有被注册的tasklet,这显然是个BUG。如果测试已设置了
-
*TASKLET_STATE_SCHED状态,则清除TASKLET_STATE_SCHED 。这样做的
-
*目的是保证tasklet注册一次执行一次,执行完后就从tasklet_vec 链表消
-
*失,如果想要再执行必须重新调用tasklet_schedule 函数再次注册该
-
*tasklet。
-
*/
-
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
-
BUG();
-
t->func(t->data); //执行tasklet处理函数
-
tasklet_unlock(t); //清除tasklet的TASKLET_STATE_RUN状态
-
continue; //跳到循环开始处继续执行下一个tasklet
-
}
-
//如果count不为0,则表示该tasklet被禁止,执行下面的代码
-
tasklet_unlock(t);
-
}
-
-
local_irq_disable();//禁止本地cpu中断,原因同上。
-
/*将已被注册但没有得到执行的tasklet重新放到已清空的tasklet_vec链表中,以
-
*免造成tasklet丢失。
-
*/
-
t->next = NULL;
-
*__get_cpu_var(tasklet_vec).tail = t;
-
__get_cpu_var(tasklet_vec).tail = &(t->next);
-
__raise_softirq_irqoff(TASKLET_SOFTIRQ); //触发TASKLET_SOFTIRQ软中断
-
local_irq_enable(); //开始本地cpu中断。
-
}
-
}
参考资料:
[1] Andrew N.Sloss, Dominic Symes, Chris Wright. ARM嵌入式系统开发——软件设计与优化. 沈建华, 译.
[2] Daniel P.Bovet, Marco Cesati. 深入理解Linux内核(第三版). 陈莉君, 张琼声, 张宏伟, 译.
[3] Pobert Love. Linux内核设计与实现. 陈莉君, 康华, 张波, 译.
[4] Wolfgang Mauerer. 深入Linux内核架构. 郭旭, 译.
[5] 陈学松. 深入Linux设备驱动程序内核机制.
[6] ARM Architecture Reference Manual
[7] The ARM-THUMB Procedure Call Standard
阅读(1846) | 评论(0) | 转发(0) |