在文章《softirq原理和源码分析》中对中断的下半部机制softirq进行了简单分析,在Linux内核中比较有名的中断下半部机制还有tasklet和workqueue等,本文重点围绕tasklet的原理和源码进行了详细的分析。
一 tasklet基本概念
tasklet是建立在softirq基础上的一种中断的下半部机制,在本质上与softirq基本相同,但却有简单的编程接口和宽松的锁规则。
tasklet是通过两种softirq来实现的,即HI_SOFTIRQ和TASKLET_SOFTIRQ,因此,tasklet也被分为两种:高优先级的tasklet(基于HI_SOFTIRQ)和普通优先级的tasklet(基于TASKLET_SOFTIRQ)。在Linux内核中,tasklet通过结构体struct tasklet_struct来描述:
- struct tasklet_struct
- {
- struct tasklet_struct *next; /*构成tasklet的链表*/
- unsigned long state; /*tasklet的状态*/
- atomic_t count; /*使能计数*/
- void (*func)(unsigned long); /*tasklet处理函数*/
- unsigned long data; /*处理函数的参数*/
- };
每个成员的含义参见注释。这里详细介绍下state成员,它表示tasklet的状态,当前Linux内核中支持两种状态:
(1)TASKLET_STATE_SCHED:表示tasklet已经准备好了,只要被选择到就可以运行了,类似于进程的就绪态。
(2)TASKLET_STATE_RUN:表示tasklet正在执行,类似于进程的RUN态,需要注意的是,TASKLET_STATE_RUN态仅仅在Linux支持SMP的情况下才有定义,因为在UP上处理器能够明确知道当前执行的tasklet是哪个。
另外,只有count=0的时候,tasklet才能被执行,因为对某个tasklet,内核或驱动开发者希望能够显示的使能/禁止其执行,所以才在tasklet结构体中增加了此成员。可以使得count成员变化的接口如下:
- static inline void tasklet_disable_nosync(struct tasklet_struct *t)
- {
- atomic_inc(&t->count);
- smp_mb__after_atomic_inc();
- }
- static inline void tasklet_disable(struct tasklet_struct *t)
- {
- tasklet_disable_nosync(t);
- tasklet_unlock_wait(t);
- smp_mb();
- }
- static inline void tasklet_enable(struct tasklet_struct *t)
- {
- smp_mb__before_atomic_dec();
- atomic_dec(&t->count);
- }
- static inline void tasklet_hi_enable(struct tasklet_struct *t)
- {
- smp_mb__before_atomic_dec();
- atomic_dec(&t->count);
- }
从这些接口的名称也可以看出count成员的主要功能,而不能简单的将count理解为引用计数,容易操作误解,所以理解为使能计数更贴切些!
二、tasklet声明和定义
1、静态和动态创建tasklet的两种方式
(1)静态创建方式
- #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的count不同,前面已经讲过count表示tasklet的使能计数,只有当count=0,表示tasklet创建后处于使能状态;count=1,表示tasklet创建后处于禁止状态。
(2)动态创建
- struct tasklet_struct t;
- void (*func)(unsigned long);
- tasklet_init(t,func,NULL);
2. tasklet处理函数
与softirq一样,tasklet不能睡眠和阻塞,因此在设计tasklet处理函数时必须格外小心,不能使用信号或其他引起阻塞的函数。另外,同一个tasklet不可能同时在两个processor上运行,如果两个不同的tasklet之间有数据共享,需要注意采用合适的锁机制。
三、tasklet的执行过程
Linux内核中采用两个每CPU变量来存储属于当前CPU的tasklet:
(1)tasklet_vec:普通的tasklet,即基于TASKLET_SOFTIRQ实现的tasklet
(2)tasklet_hi_vec:高优先级的tasklet,即基于HI_SOFTIRQ实现的tasklet
在Linux内核中通过显示的调用tasklet_schedule()和tasklet_hi_schedule()函数来实现tasklet的调度,然后将在之后的某个时间,该tasklet将会被执行。需要注意一点:tasklet一定是在调度它的CPU上执行。
- static inline void tasklet_schedule(struct tasklet_struct *t)
- {
- if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
- __tasklet_schedule(t);
- }
- extern void __tasklet_hi_schedule(struct tasklet_struct *t);
- static inline void tasklet_hi_schedule(struct tasklet_struct *t)
- {
- if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
- __tasklet_hi_schedule(t);
- }
从代码可以看到,首先检查当前tasklet是否处于TASKLET_STATE_SCHED状态,即test_and_set_bit
(TASKLET_STATE_SCHED
, &t
->state),个人感觉只有深入分析了这个函数才能真正了解其实现过程。
- static inline int test_and_set_bit(int nr, volatile unsigned long *addr)
- {
- int oldbit;
- asm volatile(LOCK_PREFIX "bts %2,%1\n\t"
- "sbb %0,%0" : "=r" (oldbit), ADDR : "Ir" (nr) : "memory");
- return oldbit;
- }
这里首先介绍两个比较少用的汇编指令:(内联汇编的知识就不多讲了,不理解的同学可以专门去查阅这方面的知识)
(1)bts %2,%1
bts汇编指令的原型为:bts dest,src 表示将目的操作数dest的第src位赋值给CF,然后再职位dest的第src位。在这里2%表示是输入操作数nr,值为0。1%表示的是输出操作数ADDR,即&t->state。那么,这句指令的意思即是将t->state的第0位先赋值给CF,然后将t->state的第0位置1。如果之前t->state = 0,那么此时CF=0,如果此前CF指向的第0位为1,那么CF=1。
(2)sbb %0,%0
指令格式:sbb 操作对象1,操作对象2 功能:操作对象1=操作对象1-操作对象2-CF
这里0%表示是输出操作数oldbit,即oldbit = oldbit - oldbit - CF,因此返回值只于CF有关,如果为0,返回值就为0,CF不为0,返回值就不为0。
根据bts的讲解,当CF=t->state的第nr位的值,即如果原来这个tasklet被设置为TASKLET_STATE_SCHED状态,那么CF=1,那么test_and_set_bit(TASKLET_STATE_SCHED, &t->state)的返回值就非0,即不能调度执行,如果原来这个tasklet没有被设置为TASKLET_STATE_SCHED状态,那么CF=0,那么那么test_and_set_bit(TASKLET_STATE_SCHED, &t->state)的返回值就为0,这个tasklet就可以调度执行。(不知道有没有讲清楚???)
下面来分析函数__tasklet_schedule()
- 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);
- }
先通过local_irq_save(flags)保存并禁止当前cpu上的中断,然后把该tasklet添加当per-CPU变量tasklet_vec链表的末尾,然后激活tasklet对应的softirq,最后恢复并使能当前cpu上的中断。这里来分析以下函数raise_softirq_irqoff
(TASKLET_SOFTIRQ):
- /*
- * This function must run with irqs
- */
- inline void raise_softirq_irqoff(unsigned int nr)
- {
- __raise_softirq_irqoff(nr);
- /*
- * If we're in an interrupt or softirq, we're done
- * (this also catches softirq-disabled code). We will
- * actually run the softirq once we return from
- * the irq or softirq.
- *
- * Otherwise we wake up ksoftirqd to make sure we
- * schedule the softirq soon.
- */
- if (!in_interrupt())
- wakeup_softirqd();
- }
- #define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
首先在__raise_softirq_irqoff(nr)中激活这个softirq,然后,激活软中断处理内核线程。其中in_interrupt在上一篇文章中已经详细分析过了。(in_interrupt判断当有硬件中断嵌套,其他软中断以及不可屏蔽中断的情况下,返回非0值)
再往下分析就是softirq相关的知识了。
四、用于实现tasklet的softirq的处理函数
- 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;
- per_cpu(tasklet_hi_vec, cpu).tail =
- &per_cpu(tasklet_hi_vec, cpu).head;
- 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);
- open_softirq(HI_SOFTIRQ, tasklet_hi_action);
- }
从上面代码可以看出,Linux内核在进行softirq的初始化的时候,就实现指定了用于实现tasklet的响应的softirq的处理函数为:tasklet_action和tasklet_hi_action。
- static void tasklet_action(struct softirq_action *a)
- {
- struct tasklet_struct *list;
- local_irq_disable();
- list = __get_cpu_var(tasklet_vec).head;
- __get_cpu_var(tasklet_vec).head = NULL;
- __get_cpu_var(tasklet_vec).tail = &__get_cpu_var(tasklet_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;
- *__get_cpu_var(tasklet_vec).tail = t;
- __get_cpu_var(tasklet_vec).tail = &(t->next);
- __raise_softirq_irqoff(TASKLET_SOFTIRQ);
- local_irq_enable();
- }
- }
这个函数清晰的展示了Linux内核中tasklet的处理过程,即在while循环中,执行了tasklet的处理函数t->func。具体代码就不去分析了,估计大家都能看明白!
阅读(436) | 评论(0) | 转发(0) |