深入分析Tasklet机制

来源:互联网 发布:java构造方法的重载 编辑:程序博客网 时间:2024/04/26 18:01
0、为什么引入tasklet机制?
linux内核为什么还要引入tasklet机制呢?主要原因是软中断的pending标志位也就32位,一般情况是不随意增加软中断处理的。而且内核也没有提供通用的增加软中断的接口。其次内,软中断处理函数要求可重入,需要考虑到竞争条件比较多,要求比较高的编程技巧。所以内核提供了tasklet这样的一种通用的机制。
其实每次写总结的文章,总是想把细节的东西说明白,所以越写越多。这样做的好处是能真正理解其中的机制。但是,内容太多的一个坏处就是难道记忆,所以,在讲清楚讲详细的同时,我还要把精髓总结出来。Tasklet的特点,也是tasklet的精髓就是:tasklet不能休眠,同一个tasklet不能在两个CPU上同时运行,但是不同tasklet可能在不同CPU上同时运行,则需要注意共享数据的保护。
1、tasklet使用
Tasklet的使用比较简单,每个Tasklet结构体有一个函数指针,指向你自己定义的函数。当我们要使用 tasklet ,首先新定义一个tasklet_struct结构,并初始化好要执行函数指针,然后将它挂接到 task_vec 链表中,并触发一个软中断就可以等着被执行了。
1.1.定义tasklet_struct结构
void my_tasklet_func(unsigned long);DECLARE_TASKLET(my_tasklet.my_tasklet_func,data);
代码DECLARE_TASKLET实现了定义名称为my_tasklet的tasklet并将其与my_tasklet_func这个函数绑定,而传入这个函数的参数为data。需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度,如下所示:
1.2.调度tasklet
tasklet_schedule(&my_tasklet);
此函数将定义后的tasklet挂接到cpu的tasklet_vec链表,具体是哪个cpu的tasklet_vec链表,是根据当前线程是运行在哪个cpu来决定的。此函数不仅会挂接tasklet ,而且会引起一个软tasklet的软中断 , 即把tasklet对应的中断向量挂起 (pend) 。
1.3.驱动模板
void xxx_do_tasklet(unsigned long);    DECLARE_TASKLET(xxx_tasklet,xxx_do_tasklet,0);   void xxx_do_tasklet(unsigned long)  {      ……  }   irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)  {        ……        tasklet_schedule(&xxx_tasklet);        ……  }   int _init xxx_init(void)  {        ……        result=request_irq(xxx_irq,xxx_interrupt,SA_INTERRUPT,”xxx”,NULL)        ……  }   void _exit xxx_exit(void)  {        ……        free_irq(xxx_irq,xxx_irq_interrupt);        ……  }
2、tasklet函数详解
2.1.tasklet_struct
tasklet对于中断处理特别有用:硬件中断必须尽快处理, 但大部分的数据管理可以延后到以后安全的时间执行。tasklet 以一个数据结构形式存在,使用前必须被初始化。初始化能够通过调用一个特定函数或者通过使用某些宏定义声明结构:
#include <linux/interrupt.h>  struct tasklet_struct  {      struct tasklet_struct *next;      unsigned long state;      atomic_t count;      void (*func)(unsigned long);      unsigned long data;  };
在这个结构体中,第一个成员代表链表中的下一个tasklet。第二个变量代表此刻tasklet的状态,一般为TASKLET_STATE_SCHED,表示此tasklet已被调度且正准备运行;此变量还可取TASKLET_STATE_RUN,表示正在运行,但只用在多处理器的情况下。count成员是一个引用计数器,只有当其值为0时候,tasklet才会被激活;否则被禁止,不能被执行。而接下来的func变量很明显是一个函数指针,它指向tasklet处理函数,这个处理函数的唯一参数为data。
2.2.tasklet操作函数
void tasklet_init(struct tasklet_struct *t,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 }        void tasklet_disable(struct tasklet_struct *t);   /*函数暂时禁止给定的tasklet被tasklet_schedule调度,直到这个tasklet再次被enable;若这个tasklet当前在运行, 这个函数忙等待直到这个tasklet退出*/        void tasklet_disable_nosync(struct tasklet_struct *t);   /*和tasklet_disable类似,但是tasklet可能仍然运行在另一个 CPU */        void tasklet_enable(struct tasklet_struct *t);   /*使能一个之前被disable的tasklet;若这个tasklet已经被调度, 它会很快运行。tasklet_enable和tasklet_disable必须匹配调用, 因为内核跟踪每个tasklet的"禁止次数"*/         void tasklet_schedule(struct tasklet_struct *t);   /*调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己*/        void tasklet_hi_schedule(struct tasklet_struct *t);   /*和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期*/        void tasklet_kill(struct tasklet_struct *t);   /*确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 这个函数等待直到它执行完毕。若 tasklet 重新调度它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,如同使用 del_timer_sync*/
3、实现分析
我们就从上面这个实例入手来分析tasklet的实现,主要分析tasklet_schedule()函数的实现。
    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,则触发之。这样,就保证了多个cpu不可能同时运行同一个tasklet,因为如果一个tasklet被调度过一次,那么它的state字段就会被设置TASKLET_STATE_SCHED标记,然后插入per-cpu变量的链表中。如果这时另外一个cpu也去调度该tasklet,那么就会在下面的if语句中被挡掉,不会运行到__tasklet_schedule(),从而不会插入到另外这个cpu的per-cpu变量的链表中,就不会被运行到。所以这里是保证了tasklet编写的函数不用是可重入的,这样就方便了编程人员。(注意,softirq机制需要编写可重入的函数)

我们来看__tasklet_schedule()的实现,就是把tasklet_struct结构体挂到tasklet_vec链表或者挂接到tasklet_hi_vec链表上,并调度软中断TASKLET_SOFTIRQ或者HI_SOFTIRQ:

    void fastcall __tasklet_schedule(struct tasklet_struct *t)      {          unsigned long flags;                        local_irq_save(flags);          /*把需要添加进系统的自己编写的struct tasklet_struc加入到per-cpu变量tasklet_vec的本地副本的链表的表头中*/          t->next = __get_cpu_var(tasklet_vec).list;           __get_cpu_var(tasklet_vec).list = t;          raise_softirq_irqoff(TASKLET_SOFTIRQ); /*触发softirq的TASKLET_SOFTIRQ*/            local_irq_restore(flags);     }
这段代码也非常简单,只是把自己要注册到系统中的tasklet_struct挂入到per-cpu变量tasklet_vec的list中而已,这里是挂到链表首部。因为需要修改per-cpu变量tasklet_vec的list的值,为了防止中断处理程序也去修改这个值,为了保持数据的一致性,所以关闭中断。然后通过raise_softirq_irqoff()设置低优先级的tasklet对应的softirq标记,以便cpu在运行softirq的时候运行到tasklet,因为tasklet是凌驾在softirq机制之上的。

这里就完成了我们自己的my_tasklet的注册和触发对应的softirq,那我们现在就应该分析tasklet的运行了。我们前面提到,tasklet是凌驾在softirq机制之上的。Linux有六种softirq,优先级最高的是HI_SOFTIRQ,优先级最低的是TASKLET_SOFTIRQ,一般情况下我们是利用TASKLET_SOFTIRQ来实现tasklet的功能。在open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL)中定义了处理tasklet的处理函数tasklet_action。所以我们要分析这个函数的实现:
static void tasklet_action(struct softirq_action *a)  {      struct tasklet_struct *list;      /*把per-cpu变量tasklet_vec的本地副本上的list设置为NULL,由于这里要修改per-cpu变量,为了防止中断处理程序或者内核抢占造成该数据的不一致性,      所以这里禁止中断再修改数据,然后再开启中断.(注意,关闭本地中断的副作用就是禁止内核抢占,因为内核抢占只有两个时间点:       1.中断返回到内核态;2.手动使能内核抢占。明显不会在临界区内手动使能内核抢占,所以关闭本地中断的副作用就是禁止内核抢占)*/      local_irq_disable();      list = __get_cpu_var(tasklet_vec).list;      __get_cpu_var(tasklet_vec).list = NULL;       local_irq_enable();        /*遍历tasklet链表,让链表上挂入的函数全部执行完成*/      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); /*真正运行user注册的tasklet函数的地方*/                  tasklet_unlock(t);                  continue;              }              tasklet_unlock(t);          }            /*         * 这里相当于把tasklet的list指针从链表中后移了(可以自行画图分析),所以刚才运行过的tasklet回调函数以后不会再次运行,         * 除非用于再次通过tasklet_schedule()注册之         */          local_irq_disable();          t->next = __get_cpu_var(tasklet_vec).list;          __get_cpu_var(tasklet_vec).list = t;          __raise_softirq_irqoff(TASKLET_SOFTIRQ);  /*再一次触发tasklet对应的softirq,使下次系统运行softirq时能运行到tasklet*/          local_irq_enable();      }  }


0 0