Tasklet
有的时候在驱动程序中需要延迟某些操作的进行,最典型的操作就是在驱动程序的中断处理函数延迟操作。比如在DMA驱动中,当数据传输完毕之后会触发中断的,通常这时候会启动一个tasklet来完成耗时的操作,也就是中断的下半部,让中断尽早的返回。
在Softirq中说过了,Tasklet的实现是基于Softirq的。也就是说Tasklet是Softirq中的一种。根据优先级不同,Linux将Tasklet分为两类如下:
代码语言:javascript复制enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
其中HI_SOFTIRQ和TASKLET_SOFTIRQ就是提供给Tasklet使用,HI_SOFTIRQ的优先级比较高,一般驱动程序很少见到使用。
Tasklet定义
linux内核使用tasklet_struct结构体来表示一个Tasklet
代码语言:javascript复制struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
next: 用于指向下一个Tasklet
state: 用于表示当前Tasklet的状态,linux内核定义了两种状态,如下:
代码语言:javascript复制enum
{
TASKLET_STATE_SCHED, /* Tasklet is scheduled for execution */
TASKLET_STATE_RUN /* Tasklet is running (SMP only) */
};
TASKLET_STATE_SCHED用于表示当前tasklet已经被提交,但是还没有执行,可能还在Tasklet列表中。
TASKLET_STATE_RUN用于表示当前tasklet正在执行,此状态只在SMP系统下有效。
count: 用于表示当前tasklet是否disable/enable。如果count=0表示tasklet是enabled,可以被调度执行,否则不可以被调度。
func: 该tasklet上的延迟回调函数,而date则是该回调函数的参数。
申明Tasklet
如果在驱动程序中需要使用tasklet,就需要先申明一个tasklet,驱动程序可以使用如下的宏同时初始化一个tasklet
代码语言:javascript复制#define DECLARE_TASKLET(name, func, data)
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
同样内核也提供了一个相似的宏,用来申明一个disable状态的tasklet
代码语言:javascript复制#define DECLARE_TASKLET_DISABLED(name, func, data)
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
除了静态申明以及初始化一个tasklet,内核也提供一个动态初始化tasklet的函数
代码语言:javascript复制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
驱动程序在申明了一个tasklet之后,就需要将其加入到系统的tasklet管理链表中来,内核定义了如下的结构用来管理tasklet
代码语言:javascript复制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);
head: 用来执行tasklet对象链接的第一个节点。
tail: tail是个双指针,用来执行tasklet_struct指针的指针,而每个tasklet_struct中有一个next指针,所以可以理解tail总是指向最后一个节点的next。
系统为HI_SOFTIRQ和TASKLET_SOFTIRQ类型定义了各自的tasklet_head管理链表,而且tasklet_vec和tasklet_hi_vec都是per-cpu变量。
也就是说系统使用tasklet_vec用来管理系统中所有softirq类型为TASKLET_SOFTIRQ的tasklet, tasklet_hi_vec同理。
提交tasklet
当在驱动程序中初始化一个tasklet之后,在需要延迟的时候就需要将该tasklet加入到系统的tasklet_vec链表中,提交到系统中。linux系统使用tasklet_schedule函数用来提交一个tasklet
代码语言:javascript复制static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
函数首先要提交的tasklet的state上的TASKLET_STATE_SCHED位是否已经置1,如果置1说明该tasklet已经被提交过,如果没有置1,则返回的是0,同时将state的TASKLET_STATE_SCHED位置1,置1之后表示这个tasklet已经被提交了。test_and_set_bit函数的作用就是返回旧的值,设置新的值。
代码语言:javascript复制void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags); ---------------A
t->next = NULL; ---------------B
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
raise_softirq_irqoff(TASKLET_SOFTIRQ); ----------------C
local_irq_restore(flags); ----------------D
}
A: 关闭本地中断,虽然tasklet_vec是per-cpu变量,但是如果不关闭中断仍然会存在并发的情况,需要关闭本地中断。
B: 此处的操作就是将t加入到tasklet_vec链表的末尾,同时需要更改tail和next指针的指向。
C: 这时候就需要触发softirq,在softirq小节有讲到。
D: 恢复中断。
Tasklet机制的初始化
在开机启动的时候,内核使用softirq_init函数来初始化softirq, 用来安装了tasklet的执行函数。
代码语言:javascript复制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);
}
可以看到为每个cpu都初始化了tasklet_vec和tasklet_hi_vec, 同时为TASKLET_SOFTIRQ和HI_SOFTIRQ安装了各自对于的处理函数。
执行Tasklet
当一个tasklet提交到系统之后,系统会在合适的时机处理该tasklet,最终调用到tasklet_action函数中。
代码语言:javascript复制static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable(); ------------------------A
list = __this_cpu_read(tasklet_vec.head); ------------------------B
__this_cpu_write(tasklet_vec.head, NULL); -----------------------C
__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head)); ------------------------D
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) { ------------------------E
if (!atomic_read(&t->count)) { -----------------------F
if (!test_and_clear_bit(TASKLET_STATE_SCHED, ------------------------H
&t->state))
BUG();
t->func(t->data); -----------------------P
tasklet_unlock(t); -----------------------G
continue;
}
tasklet_unlock(t);
}
local_irq_disable(); -----------------------Q
t->next = NULL; -----------------------X
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ); ---------------------Y
local_irq_enable();
}
}
A: 因为接下来需要操作到tasklet_vec,需要关闭本地中断,防止并发操作影响到tasklet_vec的数据。
B: 将整个tasklet_vec链表的数据保存到临时变量list中。
C: 将整个tasklet_vec的head指向NULL。
D: 同时初始化tail执行为NULL
E: 这时候就从list链表取出一个tasklet,首先通过tasklet_trylock函数判断
代码语言:javascript复制static inline int tasklet_trylock(struct tasklet_struct *t)
{
return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}
该函数就是判断state的TASKLET_START_RUN是否设置为1, 如果已经设置为1,则返回0,否则返回1。同时会设置TASKLET_START_RUN的位为1。
那这里为什么需要判断这个TASKLET_START_RUN标志呢? 根据前面的知识此标志只有在SMP系统有效。所以在SMP系统下有如下的情况:
假如有来两个处理的系统A处理器和B处理器。当前有个tasklet任务T已经提交到处理器A,并且已经调度执行了,此时TASKLET_STATE_SCHED状态已经清0。此时假设外部发生了一次中断,系统将此次中断处理权交给了B处理器,而在B的中断处理函数中调用了tasklet_schedule把tasklet T提交到B处理器,因为此taksklet T的状态TASKLET_STATE_SCHED的状态已经清0,从而可以提交成功。这样一下同一个Tasklet就有可能出现在两个处理器上执行,所以引入了TASKLET_START_RUN标志。还是前面的情况,因为在A处理器已经设置了TASKLET_STATE_RUN标志,所以在B处理器执行的时候就会失败,从而防止了统一个tasklet执行在多个处理器上。当然了这种情况只出现在SMP系统中。
F: 读取count的值,判断是否是enable的tasklet。disable的tasklet是不允许执行的。
H: 将state的TASKLET_STATE_SCHED的位清0。
P: 执行该tasklet的回调处理函数
G: 将state的TASKLET_START_RUN的位清0
Q: 因为接下来需要操作到tasklet_vec变量,为了防止中断引入并发问题,关闭中断
X: 将上述不符合条件的tasklet重新加入到tasklet_vec链表中进行管理。因为在上述的执行的过程中也有新的tasklet加入到tasklet_vec中
Y: 再次触发softirq。
其他Tasklet操作
enable一个tasklet
代码语言:javascript复制static inline void tasklet_enable(struct tasklet_struct *t)
{
smp_mb__before_atomic();
atomic_dec(&t->count);
}
将指定的tasklet对象t上的coun减去1。
disable一个tasklet
代码语言:javascript复制static inline void tasklet_unlock_wait(struct tasklet_struct *t)
{
while (test_bit(TASKLET_STATE_RUN, &(t)->state)) { barrier(); }
}
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
atomic_inc(&t->count);
smp_mb__after_atomic();
}
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
smp_mb();
}
内核提供了两个版本的disable函数,相比tasklet_disable_nosync函数tasklet_disable函数在disable的时候会调用tasklet_unlock_wait等待,主要是等待TASKLET_START_RUN的状态被清除,也就是等待tasklet被处理完毕后在disable。
kill掉一个tasklet
代码语言:javascript复制void tasklet_kill(struct tasklet_struct *t)
{
if (in_interrupt())
pr_notice("Attempt to kill tasklet from interruptn");
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);
该函数最终会通过清除TAKLET_STATE_SCHED状态位,使器不再被调用。而如果当前tasklet已经提交但是没有执行,tasklet_kill将会睡眠直到该tasklet从tasklet_vec链表中删除。如果当前tasklet已经提交并且正在运行,这时候就需要等待tasklet运行完毕。该函数一般会在驱动的卸载函数中被调用到。