下半部和推后执行的工作--tasklet

(一):tasklet

tasklet是利用软中断实现的一种下半部机制,他和进程没有任何关系,他在本质上和软中断是相似的,行为表现也很相近.但是他的接口很简单,锁保护也要求较低.

1:tasklet的实现

tasklet有两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ.这两者之间唯一的实际区别在于,HI_SOFTIRQ类型的软中断先于TASKLET_SOFTIRQ类型的软中断执行.

1):tasklet结构体

tasklet由tasklet_struct结构表示.每个结构体单独代表一个tasklet,他在linux/interrupt.h中定义:

/* Tasklets --- multithreaded analogue of BHs.
   BHS的多线程模拟
   Main feature differing them of generic softirqs: tasklet
   is running only on one CPU simultaneously.
   他们和软中断不同的主要特征为:tasklet只能同时运行在一个cpu上.
   Main feature differing them of BHs: different tasklets
   may be run simultaneously on different CPUs.
   他们和BHS不同的主要特征为:不同的tasklet可能同时运行在不同的CPU上
   Properties:
   特性:
   * If tasklet_schedule() is called, then tasklet is guaranteed
     to be executed on some cpu at least once after this.
     如果tasklet_schedule()函数被调用,tasklet一定会运行在
     一些CPU上,至少一次
   * If the tasklet is already scheduled, but its excecution is still not
     started, it will be executed only once.
     如果tasklet已经被调度了,但是他的运行依然没有开始,他将仅仅被执行一次.
   * If this tasklet is already running on another CPU (or schedule is called
     from tasklet itself), it is rescheduled for later.
     如果该tasklet已经运行在另外一个CPU上了(或这是对tasklet本身执行调度了),
     他将会在后面被调度.
   * Tasklet is strictly serialized wrt itself, but not
     wrt another tasklets. If client needs some intertask synchronization,
     he makes it with spinlocks.
     和锁相关
 */
struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long); unsigned long data; }; 

结构体中func成员是tasklet的处理程序.data是他唯一的参数.state成员只能在0,TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值.TASKLET_STATE_SCHED 表明tasklet已被调度,正准备投入运行,TASKLET_STATE_RUN表明该tasklet正在运行.TASKLET_STATE_RUN只有在多处理器上才会作为一种优化来使用,单处理器系统任何时候都清楚单个tasklet是不是正在运行.

count成员是tasklet的引用计数器.如果他不为0,则tasklet被禁止,不允许执行;只有当他为0的时候,tasklet才被激活,并且设置为挂起状态,该tasklet才能够执行.

2):调度tasklet

已调度的tasklet(等同于被触发的软中断)存放在两个单处理器数据结构:tasklet_vec(普通tasklet)和task_hi_vec(高优先级的tasklet).这两个数据结构都是由tasklet_struct构成的链表.链表中每一个tasklet_struct代表一个不同的tasklet.

tasklet由tasklet_schedule()和tasklet_hi_schedule()函数进行调度.他们接受一个指向tasklet_struct结构的指针作为参数.两个函数非常相似(区别在于一个使用TASKLET_SOFTIRQ 而另外一个使用HI_SOFTIRQ). 现在我们看一下tasklet_schedule()函数.

static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_schedule(t);
}

1:首先检查tasklet的状态是否为TASKLET_STATE_SCHED.如果是,说明tasklet已经被调度过了,函数立即返回
2:调用__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);
}

3:保存中断状态,然后禁止本地中断.我们在执行tasklet代码的时候,这么做能够保证当tasklet_schedule()处理这些tasklet的时候,处理器上的数据不会被弄乱.

4:把需要调度的tasklet加到每个处理器的tasklet_vec链表或task_hi_vec链表的表头上去.

5:唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样下一次调用do_softirq()的时候就会执行该tasklet.

6:恢复中断到原状态并返回

下面我们来看一下相应的软中断处理程序,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();
    }
}

1:禁止中断,并未当前处理器检索tasklet_vec或者是tasklet_hi_vec.
2:将当前处理器上的该链表设置为NULL,达到清空效果.
3:允许响应中断.没有必要再恢复他们到原来状态,因为这段代码本身就是作为软中断处理程序调用的,所以中断是应该被允许的
4:循环遍历获得连表上的每一个待处理的tasklet
5:如果是多处理器系统,通过检查TASKLET_STATE_RUN来判断这个tasklet是否在其他处理器上运行,如果他正在运行,那么现在就不要执行,跳到下一个待处理的tasklet去.
6:如果当前这个tasklet没有执行,将其状态设置为TASKLET_STATE_RUN,这样别的处理器就不会再去执行他了.
7:检查count是否为0,确保tasklet没有被禁止.如果tasklet被禁止了,则跳到下一个挂起的tasklet中去.
8:我们已经清楚的知道这个tasklet没有在其他地方执行,并且被我们设置成执行状态,这样他在其他部分就不会被执行,并且引用计数为0,现在可以执行tasklet的处理程序了.
9:tasklet执行完毕,清除tasklet的state域的TASKLET_STATE_RUN状态标志.
10:重复执行下一个tasklet,直至没有剩余的等待处理的tasklet.

2:使用tasklet

大多数情况下,为了控制一个寻常的硬件设备,tasklet机制都是实现自己的下半部的最佳选择.tasklet可以动态创建,使用方便,执行起来也还算快.

1):声明自己的tasklet

你既可以静态的创建tasklet,也可以动态的创建他.选择哪种方式取决于你到底是有一个对tasklet的直接引用还是间接引用.如果你准备静态的创建一个tasklet(也就是有一个他的直接引用),使用下面linux/interrupt.h中定义的两个宏的一个:

#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_struct结构.当该tasklet被调度以后,给定的函数fun会被执行,他的参数由data给出.这两个宏之间的区别在于引用计数器的初始值不同.前面一个宏把创建的tasklet的引用计数器设置为0,该tasklet处于激活状态.另一个把引用计数器设置为1,所以该tasklet处于禁止状态,下面是一个例子:

DECLARE_TASKLET(my_tasklet,my_tasklet_hadnler,dev);
//等价于
struct tasklet_struct my_tasklet = { NULL, 0 , ATOMIC_INIT(0), my_tasklet_handler,dev};

这样就创建了一个名为my_tasklet.处理程序为tasklet_handler并且是已被激活的tasklet.当处理程序被调用的时候,dev就被传递给他.

还可以通过将一个间接引用(一个指针)赋给一个动态创建的tasklet_struct结构的方式来初始化一个tasklet_init():

tasklet_init(t,tasklet_handler,dev); /* 动态而不是静态创建 */

2):编写你自己的tasklet处理程序

tasklet处理程序必须符合规定的函数类型:

void tasklet_handler(unsigned long data);

因为是靠软中断实现,所以tasklet不能睡眠.这意味着你不能在tasklet中使用信号量或者其他什么阻塞式的函数.由于tasklet运行的时候允许响应中断,所以你必须做好预防工作(如屏蔽中断然后获取一个锁),如果你的tasklet和中断处理程序之间共享了某些数据的话.两个相同的tasklet绝不会同时执行,这点和软中断不同-尽管两个不同的tasklet可以在两个处理器上同时执行.如果你的tasklet和其他的tasklet或者是 软中断共享了数据,你必须进行适当的锁保护.

3):调度你自己的tasklet

通过调用task_schedule()函数并传递给他相应的task_struct的指针,该tasklet就会被调度以便执行:

tasklet_schdule(&my_tasklet);  /* 把my_tasklet标记为挂起 */

在tasklet被调度以后,只要有机会他就会尽可能早的运行.在他还没有得到运行机会之前,如果有一个相同的tasklet又被调度了,那么他仍然只会运行一次.而如果这时他已经开始运行了,比如说在另外一个处理器上,那么这个新的tasklet会被重新调度并再次运作.作为一种优化措施,一个tasklet总在调度他的处理器上执行--这是更好的利用处理器的高速缓存.

你可以调用tasklet_disable()函数来禁止某个指定的tasklet.如果该tasklet当前正在执行,这个函数会等到他执行完毕后再返回.也可以使用tasklet_disable_nosync()函数,他也用来禁止指定的tasklet,不过他无须在返回前等待tasklet执行完毕.调用tasklet_enable()函数可以激活一个tasklet,如果希望激活DECLARE_TASKLET_DISABLE()创建的tasklet,你也得调用这个函数.如:

tasklet_disable(&my_tasklet);  /* tasklet现在被禁止 */
/* 我们现在毫无疑问的知道tasklet不能运行 */
tasklet_enable(&my_tasklet); /* tasklet现在激活 */

你也可以使用tasklet_kill()函数从挂起的队列中去掉一个tasklet.这个函数的参数是一个指向某个tasklet的tasklet_struct的长指针.在处理一个经常调度他自身的tasklet的时候,从挂起的队列中移去已调度的tasklet会很有用,这个函数首先等待该tasklet执行完毕,然后再将他移除.当然,没有什么可以阻止其他地方的代码重新调度该tasklet,由于该函数可能会引起休眠,所以禁止在中断上下文中使用它.

4:ksoftirqd

当有大量软中断出现的时候,立即处理软中断或者是不立即处理软中断都会导致一些问题,那么就需要有一种折中的方法,那就是,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载,这些线程在最低的优先级上运行,这样能够避免他们跟重要的任务抢夺资源.所以这个方案能够保证在软中断负担很重的时候,用户程序不会因为得不到处理时间而处于饥饿状态,相应的,也能够保证”过量”的软中断终究会得到处理.

每个处理器都有一个这样的线程,所有线程的名字都叫做ksoftirqd/n,区别在于n.他对应的是处理器的编号.在一个双CPU的机器上就有两个这样的线程,分别叫做ksoftirqd/0和ksoftirqd/1;为了保证只要有空闲的处理器,他们就会软中断,所有给每个处理器都分配一个这样的线程.一旦该线程被初始化,他就会执行类似下面的死循环.

for (;;) { 
if (!softirq_pending(cpu)) 
schedule(); 
set_current_state(TASK_RUNNING); 
while (softirq_pending(cpu)) { 
do_softirq(); 
if (need_resched()) 
schedule(); 
} 
set_current_state(TASK_INTERRUPTIBLE); 
} 

你可能感兴趣的:(tasklet,软中断,下半部执行)