上一节较为详细的讨论了 Linux 内核中的“软中断”机制,通过这种模拟硬件中断的设计,中断处理程序可以设计的尽可能小而快,而将余下的较为复杂的工作放入“稍后”执行的软中断中。
不过,软中断在不同的处理器上可以同时运行多个,所以任何共享数据都需要做好严格的同步管理,如果通过互斥的加锁方式来防止它自身的并发执行,那么使用软中断就没有意义了。
因此,大部分软中断处理程序,一般都不显式的加锁,而是通过一些技巧,例如采取单处理器数据的方式来实现数据共享。既然如此,Linux 内核倒不如统一提供一个机制用于避免使用显式加锁同步共享数据了,事实上,内核的确提供了 tasklet 机制。
Linux 内核的 tasklet 机制
tasklet 本质上也是软中断,只不过同一个处理程序的多个实例不能在多个处理器上同时运行。tasklet 有两类软中断代表:HI_SOFTIRQ 和 TASKLET_SOFTIRQ,它俩的唯一区别在于前者类型的软中断先于后者类型的软中断执行。
还记得吗,上一节提到软中断的信息存储在数组 softirq_vec[32] 里,do_softirq() 函数是按照先后顺序执行 softirq_vec 存储的软中断处理函数的,所以数组 softirq_vec 的索引实际上也是一种形式的“优先级”。
Linux 内核中 tasklet 的数据结构由结构体 tasklet_struct 给出,它的 C语言代码如下,请看:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
容易看出,这是一个链表结构,链表中的每一个成员代表一个不同的 tasklet。其中 func 成员指向该类型 tasklet 的处理函数,data 是该函数的参数。只有 count 成员为 0 时,该 tasklet 才有可能被激活。
state 则在 0、TASKLET_STATE_SCHED 和 TASKLET_STATE_RUN 之间取值,TASKLET_STATE_SCHED 标志位表示该 tasklet 已准备好投入运行,TASKLET_STATE_RUN 则表示该 tasklet 已被投入运行,处理器在执行 func 之前会检测该标志位,防止同一类型的 tasklet 同时在多个处理器上被执行。
tasklet 的调度
已调度的 tasklet 存放在两个但处理器数据结构 tasklet_vec 和 tasklet_hi_vec 中,Linux 内核是如下定义的,请看C语言代码如下:
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec) = { NULL };
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec) = { NULL };
DEFINE_PER_CPU 宏的 C语言代码如下:
因为 tasklet_vec 和 tasklet_hi_vec 非常相似,只是优先级不同而已,所以下文主要以 tasklet_vec 的相关设计和实现为例做分析,tasklet_hi_vec 的分析是类似的。
tasklet 的调度主要由 tasklet_schedule() 函数实现,它的 C语言代码如下,请看:
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
显然,tasklet_schedule() 函数首先检查 tasklet 的 state 标志位,如果是 TASKLET_STATE_SCHED,说明该 tasklet 已经被调度过了,直接就返回了。否则,设置 state 标志位为 TASKLET_STATE_SCHED,并且执行 __tasklet_schedule
函数,它的C语言代码如下,请看:
void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);
t->next = NULL;
*__get_cpu_var(tasklet_vec).tail = t;
__get_cpu_var(tasklet_vec).tail = &(t->next);
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
容易看出,其实 __tasklet_schedule
函数的核心代码就是将 t 加入到 tasklet_vec 中。前面提到,Linux 内核中的 tasklet 本质就是一种“软中断”,所以 __tasklet_schedule
函数最后触发了“软中断”TASKLET_SOFTIRQ,由软中断做进一步的处理。
运行 tasklet 处理函数
现在知道 Linux 内核是如何调度 tasklet 的了,那内核是如何执行 tasklet 的处理函数的呢?这一工作其实是由 tasklet_action() 函数完成的,它的C语言代码如下,请看:
396 static void tasklet_action(struct softirq_action *a)
- 397 {
| 398 struct tasklet_struct *list;
| 399
| 400 local_irq_disable();
| 401 list = __get_cpu_var(tasklet_vec).head;
| 402 __get_cpu_var(tasklet_vec).head = NULL;
| 403 __get_cpu_var(tasklet_vec).tail = &__get_cpu_var(tasklet_vec).head;
| 404 local_irq_enable();
| 405
|- 406 while (list) {
|| 407 struct tasklet_struct *t = list;
|| 408
|| 409 list = list->next;
|| 410
||- 411 if (tasklet_trylock(t)) {
23- 412 if (!atomic_read(&t->count)) {
234 413 if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
234 414 BUG();
234 415 t->func(t->data);
234 416 tasklet_unlock(t);
234 417 continue;
234 418 }
||| 419 tasklet_unlock(t);
||| 420 }
|| 421
|| 422 local_irq_disable();
|| 423 t->next = NULL;
|| 424 *__get_cpu_var(tasklet_vec).tail = t;
|| 425 __get_cpu_var(tasklet_vec).tail = &(t->next);
|| 426 __raise_softirq_irqoff(TASKLET_SOFTIRQ);
|| 427 local_irq_enable();
|| 428 }
| 429 }
到这里就非常清楚了,tasklet_action() 函数从 tasklet_vec 中取出数据,然后判断 count 和 state 是否符合执行条件,如果符合,则通过 while 遍历整个链表记录的 tasklet。
那么 tasklet_action() 函数什么时候会运行呢?按照前面说的,tasklet 本质上也是一种“软中断”,那么 tasklet_action() 函数肯定也被注册到 softiqr_vec 了,查看相关调用也的确如此,请看下面的 C语言代码:
495 void __init softirq_init(void)
- 496 {
| 497 int cpu;
| 498
|- 499 for_each_possible_cpu(cpu) {
|| 500 per_cpu(tasklet_vec, cpu).tail =
|| 501 &per_cpu(tasklet_vec, cpu).head;
|| 502 per_cpu(tasklet_hi_vec, cpu).tail =
|| 503 &per_cpu(tasklet_hi_vec, cpu).head;
|| 504 }
| 505
| 506 open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
| 507 open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
| 508 }
这么一来,就一切都通了: __tasklet_schedule
函数将需要调度的 tasklet 放入 tasklet_vec 中,并且触发“软中断”TASKLET_SOFTIRQ。接着,软中断会执行 tasklet_action() 函数从 tasklet_vec 中提取已被调度的 tasklet 投入运行。
tasklet 处理函数
现在知道了 Linux 内核中的 tasklet 是如何被调度和执行的了,显然,tasklet 处理函数的原型如下,请看C语言代码:
void tasklet_handler(unsigned long data);
需要说明的是,tasklet 是依赖软中断实现的,而软中断仍然处于中断上下文中,因此 tasklet 虽然允许响应中断,但是和软中断一样不能睡眠,因此我们不能在 tasklet_handler() 中使用信号量或者其他形式的阻塞函数。