一、简介
由于中断会打断内核中进程的正常调度运行,所以要求中断服务程序尽可能的短小精悍;但是在实际系统中,当中断到来时,要完成工作往往进行大量的耗时处理。因此期望让中断处理程序运行得快,并想让它完成的工作量多,这两个目标相互制约,诞生——顶/底半部机制,本文主要介绍tasklet的执行过程。
读者可根据情况了解以下知识:
软中断过程总结
中断机制详细总结
由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。
总结下tasklet的优点:
(1)无类型数量限制;
(2)效率高,无需循环查表;
(3)支持SMP机制;
它的特性如下:
1)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
2)多个不同类型的tasklet可以并行在多个CPU上。
3)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
二、tasklet过程分析
tasklet的主要过程如下:
1、tasklet初始化;
2、tasklet调度过程;
3、tasklet执行过程;
2.1 tasklet初始化
tasklet初始化在start_kernel()->softirq_init()中进行,初始化tasklet_vec和tasklet_hi_vec两个链表,并注册TASKLET_SOFTIRQ和HI_SOFTIRQ两个软中断。
asmlinkage __visible void __init start_kernel(void)
{
...
softirq_init();
...
}
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
tasklet描述符:
struct tasklet_struct
{
struct tasklet_struct *next;//将多个tasklet链接成单向循环链表
unsigned long state;//TASKLET_STATE_SCHED(Tasklet is scheduled for execution) TASKLET_STATE_RUN(Tasklet is running (SMP only))
atomic_t count;//0:激活tasklet 非0:禁用tasklet
void (*func)(unsigned long); //该tasklet处理程序
unsigned long data; //传递给tasklet处理函数的参数
};
tasklet链表:
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);//低优先级
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);//高优先级
两种静态初始化、一种动态初始化方法
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
//定义名字为name的非激活tasklet
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
//定义名字为name的激活tasklet
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
//动态初始化tasklet
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);//这里count为0,表示tasklet处于激活状态
t->func = func;
t->data = data;
}
2.1 tasklet调度过程
调度的相关函数:
static inline void tasklet_disable(struct tasklet_struct *t)
//函数暂时禁止给定的tasklet被tasklet_schedule调度,直到这个tasklet被再次被enable;若这个tasklet当前在运行, 这个函数忙等待直到这个tasklet退出
static inline void tasklet_enable(struct tasklet_struct *t)
//使能一个之前被disable的tasklet;若这个tasklet已经被调度, 它会很快运行。tasklet_enable和tasklet_disable必须匹配调用, 因为内核跟踪每个tasklet的"禁止次数"
static inline void tasklet_schedule(struct tasklet_struct *t)
//调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己
tasklet_hi_schedule(struct tasklet_struct *t)
//和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期.
tasklet_kill(struct tasklet_struct *t)
//确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 这个函数等待直到它执行完毕。若 tasklet 重新调度它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,如同使用 del_timer_sync
调度原理
程序在多个上下文中可以多次调度同一个tasklet执行(也可能来自多个cpu core),不过实际上该tasklet只会一次挂入首次调度到的那个cpu的tasklet链表,也就是说,即便是多次调用tasklet_schedule,实际上tasklet只会挂入一个指定CPU的tasklet队列中(而且只会挂入一次),也就是说只会调度一次执行。这是通过TASKLET_STATE_SCHED这个flag来完成的,我们可以用下面的图片来描述:
tasklet_schedule()被调用的时机大多在中断上半部中,然后将工作交给__tasklet_schedule()处理。
__tasklet_schedule()锁中断情况下插入当前taskelt到tasklet_vec中,并触发TASKLET_SOFTIRQ软中断。
tasklet_scheduler()中设置了当前tasklet的TASKLET_STATE_SCHED标志位,只要该tasklet没有被执行,那么即使驱动程序多次调用tasklet_schedule()也不起作用。
因此一旦该tasklet挂入到某个CPU的tasklet_vec后,就必须在该CPU的软中断上下文中执行,直到执行完毕并清除了TASKLET_STATE_SCHED标志位,才有机会到其他CPU上运行。
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))//置TASKLET_STATE_SCHED位,如果原来未被置位,则调用__tasklet_schedule()
__tasklet_schedule(t);
}
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);//将t挂入到tasklet_vec链表中
raise_softirq_irqoff(TASKLET_SOFTIRQ);//触发软中断
local_irq_restore(flags);
}
HI_SOFTIRQ类型的tasklet和上面基本对称,只是tasklet_vec换成了tasklet_hi_vec,TASKLET_SOFTIRQ换成了HI_SOFTIRQ。
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_hi_schedule(t);
}
void __tasklet_hi_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);
t->next = NULL;
*__this_cpu_read(tasklet_hi_vec.tail) = t;
__this_cpu_write(tasklet_hi_vec.tail, &(t->next));
raise_softirq_irqoff(HI_SOFTIRQ);
local_irq_restore(flags);
}
2.2 tasklet执行过程
软中断执行时会按照软中断状态__softirq_pending来依次执行pending状态的软中断,当执行到TASKLET_SOFTIRQ软中断时,调用tasklet_action(),HI_SOFTIRQ为tasklet_hi_action。
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable();
list = __this_cpu_read(tasklet_vec.head);//在关中断情况下读取tasklet_vec立案表头作为临时链表list
__this_cpu_write(tasklet_vec.head, NULL);//重新初始化tasklet_vec
__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
local_irq_enable();
while (list) {//开中断情况下遍历tasklet_vec链表,所以tasklet是开中断的
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {//如果返回false,表示当前tasklet已经在其他CPU上运行,这一轮将会跳过此tasklet。确保同一个tasklet只能在一个CPU上运行。
if (!atomic_read(&t->count)) {//表示当前tasklet处于激活状态
if (!test_and_clear_bit(TASKLET_STATE_SCHED,
&t->state))//清TASKLET_STATE_SCHED位;如果原来没有被置位,则返回0,触发BUG()。
BUG();
t->func(t->data);//执行当前tasklet处理函数
tasklet_unlock(t);
continue;//跳到while继续遍历余下的tasklet
}
tasklet_unlock(t);
}
如果t->count的值不等于0,说明这个tasklet在调度之后,被disable掉了,所以会将tasklet结构体重新放回到tasklet_vec链表,并重新调度TASKLET_SOFTIRQ软中断,在之后enable这个tasklet之后重新再执行它
local_irq_disable();
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;//把当前tasklet挂入到当前CPU的tasklet_vec中,等待下一次触发时再执行。
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ);//再次置TASKLET_SOFTIRQ位
local_irq_enable();
}
}
HI_SOFTIRQ类型对应的tasklet_hi_action()函数:
static void tasklet_hi_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable();
list = __this_cpu_read(tasklet_hi_vec.head);
__this_cpu_write(tasklet_hi_vec.head, NULL);
__this_cpu_write(tasklet_hi_vec.tail, this_cpu_ptr(&tasklet_hi_vec.head));
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED,
&t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = NULL;
*__this_cpu_read(tasklet_hi_vec.tail) = t;
__this_cpu_write(tasklet_hi_vec.tail, &(t->next));
__raise_softirq_irqoff(HI_SOFTIRQ);
local_irq_enable();
}
}
二、小结
tasklet基于softirq,但是tasklet和softirq又存在一些区别。
softirq | tasklet | |
---|---|---|
分配 | softirq是静态定义的 | tasklet既可以静态定义,也可以通过tasklet_init()动态创建 |
并发性 | softirq是可重入的,同一类型的软中断可以在多个CPU上并发执行。 | tasklet是不可重入的,tasklet必须串行执行,同一个tasklet不可能同时在两个CPU上运行。tasklet通过TASKLET_STATE_SCHED和TASKLET_STATE_RUN保证串行 |
运行 | softirq运行在开中断环境下。软中断回调函数不能睡眠,因为软中断可能处于中断上下文中,睡眠导致Linux无法调度。软中断的执行时机可能在中断返回时,即退出中断上下文时。或者local_bh_enable()中。 | taskelt执行时机在softirq中 |