8.3 tasklet
tasklet是利用软中断实现的一种下半部机制。它和进程没有任何关系。tasklet和软中断在本质上和相似,行为表现也相近,但是,它的接口更简单,锁保护也要求较低。
选择到底是用软中断还是tasklet其实很简单:通常应该用tasklet。软中断的使用者屈指可数,只在执行频率很高和连续性要求很高的情况下才需要使用。而tasklet用途广泛。大多数情况下用tasklet效果都不错,而且它们还非常容易使用。
8.3.1 tasklet的实现
tasklet是通过软中断实现的,本身也是软中断。tasklet由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。这两者之间唯一的实际区别在于,HI_SOFTIRQ类型的软中断先于TASKLET_SOFTIRQ类型的软中断执行。
1、tasklet结构体
tasklet由tasklet_struct结构体表示。每个结构体单独代表一个tasklet,在<linux/interrupt.h>中定义为:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);//tasklet处理函数
unsigned long data;// 给tasklet处理函数的参数
};
结构体中的func成员是tasklet的处理程序,data是它唯一的参数。
state成员只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。TASKLET_STATE_SCHED表明tasklet已被调度,正准备投入运行,TASKLET_STATE_RUN表明该tasklet正在运行。TASKLET_STATE_RUN只有在多处理器的系统上才会作为一种优化来使用,单处理器系统任何时候都清楚单个tasklet是不是正在运行。
count成员是tasklet的引用计数器。如果它不为0,则tasklet被禁止,不允许执行;只有当它为0时,tasklet才被激活,并且被设置为挂起状态时,该tasklet才能够执行。
2、调度tasklet
已调度的tasklet存放在两个单处理器数据结构:tasklet_vec和tasklet_hi_vec。这两个数据结构都是由tasklet_struct结构体构成的链表。链表中的每个tasklet_struct代表一个不同的tasklet。
/*
* Tasklets
*/
struct tasklet_head
{
struct tasklet_struct *head;
struct tasklet_struct **tail;
};
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);// 普通的tasklet
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);// 高优先级的tasklet
tasklet由tasklet_schedule()和tasklet_hi_schedule()进行调度,它们接受一个指向tasklet_struct结构的指针作为参数。
8.3.2 使用tasklet
大多数情况下,为了控制一个寻常的硬件设备,tasklet机制都是实现自己的下半部的最佳选择。tasklet可以动态创建,使用方便,执行起来也还算快。
1、声明tasklet
既可以静态地创建tasklet,也可以动态地创建tasklet。选择哪种方式取决于到底是有一个对tasklet的直接引用还是间接引用。如果准备静态地创建一个tasklet,使用<linux/interrupt.h>中定义的两个宏中的一个:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
#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 }
这两个宏都能根据给定的名称静态地创建一个tasklet_struct结构。当该tasklet被调度以后,给定的函数func会被执行,它的参数由data给出。这两个宏之间的区别在于引用计数器的初始值设置不同。DECLARE_TASKLET宏把创建的tasklet的引用计数器设置为0,该tasklet处于激活状态。DECLARE_TASKLET_DISABLED宏把引用计数器设置为1,所以该tasklet处于禁止状态。例如:
DECLARE_TASKLET(my_tasklet, mytasklet_handler, dev);
等价于
struct tasklet_struct my_tasklet= { NULL, 0, ATOMIC_INIT(0), mytasklet_handler, dev};
这就创建一个名为my_tasklet,处理程序为mytasklet_handler并且是已被激活的tasklet。当处理程序被调用时,dev就会被传递给它。
还可以通过将一个间接引用赋给一个动态创建tasklet_struct结构的方式来初始化一个tasklet_init():
<linux/interrupt.h>
extern void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);//动态创建
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;
}
EXPORT_SYMBOL(tasklet_init);
2、编写tasklet处理程序
tasklet处理程序必须符合规定的函数类型:
void tasklet_handler(unsigned long data)
因为tasklet是靠软中断实现,所以tasklet不能睡眠。这意味着不能在tasklet中使用信号量或者阻塞式的函数。由于tasklet运行时允许响应中断,所以必须做好预防工作,如果tasklet和中断处理程序之间共享了某些数据的话。两个相同的tasklet决不会同时执行,这点和软中断不同——尽管两个不同的tasklet可以在两个处理器上同时执行。如果自己的tasklet和其他的tasklet或者是软中断共享了数据,必须进行适当地锁保护。
3、调度tasklet
通过调用tasklet_schedule()并传递给它相应的tasklet_struct的指针,该tasklet就会被调度以便执行:
tasklet_schedule(&my_tasklet);//把my_tasklet标记为挂起
在tasklet被调度以后,只要有机会它就会尽可能早地运行。在它还没有得到运行机会之前,如果有一个相同的tasklet又被调度了,那么它仍然只会运行一次。而如果这时它已经开始运行了,比如说在另外一个处理器上,那么这个新的tasklet会被重新调度并再次运行。作为一种优化措施,一个tasklet总在调度它的处理器上执行——这是希望能更好地利用处理器的高速缓存。
通过调用tasklet_disable()来禁止某个指定的tasklet。如果该tasklet当前正在执行,这个函数会等到它执行完毕再返回。也可以调用tasklet_disable_nosync()来禁止指定的tasklet,不过它无须在返回前等待tasklet执行完毕。这么做不太安全,因为无法估计该tasklet是否仍在执行。调用tasklet_enable()可以激活一个tasklet,如果希望激活DECLARE_TASKLET_DISABLED宏创建的tasklet,也得调用这个函数,如:
tasklet_disable(&my_tasklet);//tasklet现在被禁止
tasklet_enable(&my_tasklet);//tasklet现在被激活
通过调用tasklet_kill()从挂起的队列中去掉一个tasklet。定义在<linux/interrupt.h>文件中,
void tasklet_kill(struct tasklet_struct *t);
实现在kernel/softirq.c文件中。
void tasklet_kill(struct tasklet_struct *t)
{
if (in_interrupt())
printk("Attempt to kill tasklet from interrupt\n");
while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
do {
yield();
} while (test_bit(TASKLET_STATE_SCHED, &t->state));
}
tasklet_unlock_wait(t);
clear_bit(TASKLET_STATE_SCHED, &t->state);
}
EXPORT_SYMBOL(tasklet_kill);
该函数的参数是一个指向某个tasklet的tasklet_struct的指针。在处理一个经常重新调度它自身的tasklet时,从挂起的队列中移去已调度的tasklet会很有用。这个函数首先等待该tasklet执行完毕,然后再将它移去。由于该函数可能会引起休眠,所以禁止在中断上下文中使用。
4、ksoftirqd
每个处理器都有一组辅助处理软中断的内核线程。当内核中出现大量软中断时,这些内核线程就会辅助处理它们。