《Linux内核设计与实现》读书笔记(6)--- 下半部和推后执行的工作(2)

下半部和推后执行的工作

4.tasklet

    tasklet是通过软中断实现的,它由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。两者唯一区别在于前者优先于后者执行。

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

struct tasklet_struct {

    struct tasklet_struct *next;    /* 链表中的下一个tasklet */
    
    unsigned long state;              /* tasklet的状态 */
    
    atomic_t count;                      /* 引用计数器 */
    
    void (*func)(unsigned long);  /* tasklet处理函数 */
    
    unsigned long data;               /* 给tasklet处理函数的参数 */

}

    结构体中的func成员是tasklet的处理程序(像软中断中的action一样),data是它唯一的参数。state成员只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。TASKLET_STATE_SCHED表明tasklet已被调度,正准备投入运行。TASKLET_STATE_RUN表明该tasklet正在运行。count成员是tasklet的引用计数器,如果它不为0,则tasklet被禁止,不允许执行;只有当它为0时,tasklet才被激活,并且在被设置为挂起状态时,该tasklet才能够执行。

 

    已调度的tasklet存放在两个单处理器数据结构:tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)中。分别由tasklet_schedule()和tasklet_hi_schedule()进行调度。tasklet_schedule()的细节:

    1)检查tasklet的状态是否为TASKLET_STATE_SCHED。如果是,说明tasklet已经被调度过了(有可能是一个tasklet已经被调度过但还没有来得及执行,而该tasklet又被唤起了一次)函数立即返回。

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

    3)把需要调度的tasklet加到每个处理器一个的tasklet_vec或tasklet_hi_vec链表的表头上去。

    4)唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次调用do_softirq()时就会执行该tasklet。

    5)恢复中断到原状态并返回。

 

    tasklet_action()和tasklet_hi_action()是tasklet处理的核心。它们的细节:

    1)禁止中断。(没有必要首先保存其状态,因为这里的代码总是作为软中断被调用,而且中断总是被激活的)并为当前处理器检索tasklet_vec或tasklet_hi_vec链表。

    2)将当前处理器上的该链表设置为NULL,达到清空的效果。

    3)允许响应中断。没有必要再恢复它们回原状态,因为这段程序本身就是作为软中断处理程序被调用的,所以中断是应该被允许的。

    4)循环遍历获得链表上的每一个待处理的tasklet。

    5)如果是多处理器系统,通过检查TASKLET_STATE_RUN状态标志来判断这个tasklet是否正在其他处理器上运行,如果它正在运行,那么现在就不要执行,跳到下一个待处理的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。

 

5.使用tasklet

    可以静态或动态地创建tasklet。如果你准备静态地创建一个tasklet,使用下面<linux/interrupt.h>中定义的两个宏中的一个。

    DECLARE_TASKLET(name, func, data);

    DECLARE_TASKLET_DISABLED(name, func, data);

    这两个宏都能根据给定的名称静态地创建一个tasklet_struct结构。当该tasklet被调度以后,给定的函数func会被执行,它的参数由data给出。这两个宏之间的区别在于引用计数器的初始值设置不同。前面一个宏把创建tasklet的引用计数器设置为0,该tasklet处理激活状态。另一个把引用计数器设置为1,所以该tasklet处于禁止状态。

    动态创建则调用tasklet_init函数:

    tasklet_init(t, tasklet_handler, dev);

 

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

    void tasklet_handler(unsigned long data)

    因为是靠软中断实现,所以tasklet不能睡眠。tasklet运行时允许响应中断,但两个相同的tasklet决不会同时执行。

 

    通过调用tasklet_schedule()函数并传递给它相应的tasklet_struct指针,该tasklet就会被调度以便执行。

    tasklet_schedule(&my_tasklet);

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

    你可以调用tasklet_disable()函数禁止某个指定的tasklet。如果该tasklet当前正在执行,这个函数会等到它执行完毕再返回。你也可以调用tasklet_disable_nosync()函数,它也可用来禁止指定的tasklet,不过它无须在返回前等待tasklet执行完毕。这往往不太安全。调用tasklet_enable()函数可以激活一个tasklet。调用tasklet_kill()函数从挂起的队列中去掉一个tasklet。

 

6.ksoftirqd

    每个处理器都有一组辅助处理软中断(和tasklet)的内核线程,当内核中出现大量软中断的时候,这些内核线程就会辅助它们。这些线程在最低的优先级上运行(nice值是19),避免和其他重复的任务抢夺资源。所有线程的名字都叫做ksoftirad/n,区别在于n,它对应的是处理器的编号。

 

7.工作队列

    工作队列(work queue)是另一种将工作推后执行的形式。它由一个内核线程去执行——这个下半部分总是会在进程上下文执行。最重要的就是工作队列允许重新调度甚至是睡眠。

    如果推后执行的任务需要睡眠,那么就选择工作队列,反之,选择软中断或tasklet。实际上,工作队列通常可以用内核线程替换,但是由于内核开发者们非常反对创建新的内核线程,所以推荐使用工作队列。

    工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程被称作工作者线程(worker thread)。工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个默认的工作者线程来处理这些工作。

    默认的工作者线程叫做events/n,这里n是处理器的编号;每个处理器对应一个线程。

    要实际创建一些需要推后完成的工作,可能通过 DECLARE_WORK 在编译时静态地创建:DECLARE_WORK(name, void(*func)(void *), void *data),这样就会静态地创建一个名为 name,处理函数为 func,参数为 data 的 workstruct 结构体。同时,也可以在运行时通过指针创建一个工作:INIT_WORK(struct work_struct *work, void(*func)(void *), void *data);

    工作队列处理函数的原型是:void work_handler(void *data),这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中。默认情况下,允许响应中断并且不持有任何锁。如果需要,函数可以睡眠。需要注意的是,尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。通常在系统调用发生时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有此时它才会映射用户空间的内存。

    使用 schedule_work(&work) 和 schedule_delayed_work(&work, delay) 调度工作,flush_scheduled_work(void) 保证操作已经执行完毕,int cancel_delayed_work(struct work_struct *work) 取消延迟执行的工作。struct workqueue_struct *create_workqueue(const char *name) 在每个处理器上创建一个工作者线程,调用 int queue_work(struct workqueue_struct *wq, struct work_struct *work); 和 int queue_delayed_work(struct workqueue_struct *wq, struct work_struct *work, unsigned long delay); 进行调度,刷新指定的工作队列则调用 flush_workqueue(struct workqueue_struct *wq);

你可能感兴趣的:(linux)