Tasklet与Workqueue

一、中断处理的tasklet(小任务)机制

中断服务程序一般都是在中断请求关闭的条件下执行的,以避免嵌套而使中断控制复杂化。但是,中断是一个随机事件,它随时会到来,如果关中断的时间太长,CPU就不能及时响应其他的中断请求,从而造成中断的丢失。因此,Linux内核的目标就是尽可能快的处理完中断请求,尽其所能把更多的处理向后推迟。例如,假设一个数据块已经达到了网线,当中断控制器接受到这个中断请求信号时,Linux内核只是简单地标志数据到来了,然后让处理器恢复到它以前运行的状态,其余的处理稍后再进行(如把数据移入一个缓冲区,接受数据的进程就可以在缓冲区找到数据)。因此,内核把中断处理分为两部分:上半部(tophalf)和下半部(bottomhalf),上半部(就是中断服务程序)内核立即执行,而下半部(就是一些内核函数)留着稍后处理,

首先,一个快速的“上半部”来处理硬件发出的请求,它必须在一个新的中断产生之前终止。通常,除了在设备和一些内存缓冲区(如果你的设备用到了DMA,就不止这些)之间移动或传送数据,确定硬件是否处于健全的状态之外,这一部分做的工作很少。

下半部运行时是允许中断请求的,而上半部运行时是关中断的,这是二者之间的主要区别。

但是,内核到底什时候执行下半部,以何种方式组织下半部?这就是我们要讨论的下半部实现机制,这种机制在内核的演变过程中不断得到改进,在以前的内核中,这个机制叫做bottomhalf(简称bh),在2.4以后的版本中有了新的发展和改进,改进的目标使下半部可以在多处理机上并行执行,并有助于驱动程序的开发者进行驱动程序的开发。下面主要介绍常用的小任务(Tasklet)机制及2.6内核中的工作队列机制。


小任务机制

这里的小任务是指对要推迟执行的函数进行组织的一种机制。其数据结构为tasklet_struct,每个结构代表一个独立的小任务,其定义如下:

  1. <span style="font-weight: normal;">struct tasklet_struct {  
  2. struct tasklet_struct *next; /*指向链表中的下一个结构*/  
  3. unsignedlong state; /* 小任务的状态*/  
  4. atomic_tcount; /* 引用计数器*/  
  5. void(*func) (unsigned long); /* 要调用的函数*/  
  6. unsignedlong data; /* 传递给函数的参数*/  
  7. };</span>  


结构中的func域就是下半部中要推迟执行的函数,data是它唯一的参数。
State域的取值为TASKLET_STATE_SCHED或TASKLET_STATE_RUN。TASKLET_STATE_SCHED表示小任务已被调度,正准备投入运行,TASKLET_STATE_RUN表示小任务正在运行。TASKLET_STATE_RUN只有在多处理器系统上才使用,单处理器系统什么时候都清楚一个小任务是不是正在运行(它要么就是当前正在执行的代码,要么不是)。
Count域是小任务的引用计数器。如果它不为0,则小任务被禁止,不允许执行;只有当它为零,小任务才被激活,并且在被设置为挂起时,小任务才能够执行。
1. 声明和使用小任务大多数情况下,为了控制一个寻常的硬件设备,小任务机制是实现下半部的最佳选择。小任务可以动态创建,使用方便,执行起来也比较快。
我们既可以静态地创建小任务,也可以动态地创建它。选择那种方式取决于到底是想要对小任务进行直接引用还是一个间接引用。如果准备静态地创建一个小任务(也就是对它直接引用),使用下面两个宏中的一个:
DECLARE_TASKLET(name,func, data)
DECLARE_TASKLET_DISABLED(name,func, data)
这两个宏都能根据给定的名字静态地创建一个tasklet_struct结构。当该小任务被调度以后,给定的函数func会被执行,它的参数由data给出。这两个宏之间的区别在于引用计数器的初始值设置不同。第一个宏把创建的小任务的引用计数器设置为0,因此,该小任务处于激活状态。另一个把引用计数器设置为1,所以该小任务处于禁止状态。例如:
DECLARE_TASKLET(my_tasklet,my_tasklet_handler, dev);
这行代码其实等价于
structtasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0),
tasklet_handler,dev};
这样就创建了一个名为my_tasklet的小任务,其处理程序为tasklet_handler,并且已被激活。当处理程序被调用的时候,dev就会被传递给它。
2. 编写自己的小任务处理程序小任务处理程序必须符合如下的函数类型:
voidtasklet_handler(unsigned long data)
由于小任务不能睡眠,因此不能在小任务中使用信号量或者其它产生阻塞的函数。但是小任务运行时可以响应中断。
3. 调度自己的小任务通过调用tasklet_schedule()函数并传递给它相应的tasklt_struct指针,该小任务就会被调度以便适当的时候执行:
tasklet_schedule(&my_tasklet); /*把my_tasklet标记为挂起 */
在小任务被调度以后,只要有机会它就会尽可能早的运行。在它还没有得到运行机会之前,如果一个相同的小任务又被调度了,那么它仍然只会运行一次。
可以调用tasklet_disable()函数来禁止某个指定的小任务。如果该小任务当前正在执行,这个函数会等到它执行完毕再返回。调用tasklet_enable()函数可以激活一个小任务,如果希望把以DECLARE_TASKLET_DISABLED()创建的小任务激活,也得调用这个函数,如:
tasklet_disable(&my_tasklet); /*小任务现在被禁止,这个小任务不能运行*/
tasklet_enable(&my_tasklet); /* 小任务现在被激活*/
也可以调用tasklet_kill()函数从挂起的队列中去掉一个小任务。该函数的参数是一个指向某个小任务的tasklet_struct的长指针。在小任务重新调度它自身的时候,从挂起的队列中移去已调度的小任务会很有用。这个函数首先等待该小任务执行完毕,然后再将它移去。
4.tasklet的简单用法
下面是tasklet的一个简单应用,以模块的形成加载。

  1. <span style="font-weight: normal;">#include <linux/module.h>  
  2. #include<linux/init.h>  
  3. #include<linux/fs.h>  
  4. #include<linux/kdev_t.h>  
  5. #include <linux/cdev.h>  
  6. #include <linux/kernel.h>  
  7. #include<linux/interrupt.h>  
  8.   
  9. static struct t asklet_struct my_tasklet;  
  10.   
  11. static void tasklet_handler (unsigned longd ata)  
  12. {  
  13. printk(KERN_ALERT,"tasklet_handler is running./n");  
  14. }  
  15.   
  16. staticint __init test_init(void)  
  17. {  
  18. tasklet_init(&my_tasklet,tasklet_handler,0);  
  19. tasklet_schedule(&my_tasklet);  
  20. return0;  
  21. }  
  22.   
  23. static void __exit test_exit(void)  
  24. {  
  25. tasklet_kill(&tasklet);  
  26. printk(KERN_ALERT,"test_exit is running./n");  
  27. }  
  28. MODULE_LICENSE("GPL");  
  29.   
  30. module_init(test_init);  
  31. module_exit(test_exit);</span>  


从这个例子可以看出,所谓的小任务机制是为下半部函数的执行提供了一种执行机制,也就是说,推迟处理的事情是由tasklet_handler实现,何时执行,经由小任务机制封装后交给内核去处理。

二、中断处理的工作队列机制

工作队列(work queue)是另外一种将工作推后执行的形式,它和前面讨论的tasklet有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许被重新调度甚至是睡眠。

那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。


  1. 工作、工作队列和工作者线程

如前所述,我们把推后执行的任务叫做工作(work),描述它的数据结构为work_struct,这些工作以队列结构组织成工作队列(workqueue),其数据结构为workqueue_struct,而工作线程就是负责执行工作队列中的工作。系统默认的工作者线程为events,自己也可以创建自己的工作者线程。

  1. 表示工作的数据结构

工作用<linux/workqueue.h>中定义的work_struct结构表示:

  1. <span style="font-weight: normal;">struct work_struct{  
  2. unsigned long pending; /* 这个工作正在等待处理吗?*/  
  3. struct list_head entry; /* 连接所有工作的链表 */  
  4. void (*func) (void *); /* 要执行的函数 */  
  5. void *data; /* 传递给函数的参数 */  
  6. void *wq_data; /* 内部使用 */  
  7. struct timer_list timer; /* 延迟的工作队列所用到的定时器 */  
  8. };</span>  


这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。

3. 创建推后的工作

要使用工作队列,首先要做的是创建一些需要推后完成的工作。可以通过DECLARE_WORK在编译时静态地建该结构:

DECLARE_WORK(name, void (*func) (void *), void *data);

这样就会静态地创建一个名为name,待执行函数为func,参数为data的work_struct结构。

同样,也可以在运行时通过指针创建一个工作:

INIT_WORK(struct work_struct *work, woid(*func) (void *), void *data);

这会动态地初始化一个由work指向的工作。

4. 工作队列中待执行的函数

工作队列待执行的函数原型是:

void work_handler(void *data)

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

5. 对工作进行调度

现在工作已经被创建,我们可以调度它了。想要把给定工作的待处理函数提交给缺省的events工作线程,只需调用

schedule_work(&work);

work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。

有时候并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,可以调度它在指定的时间执行:

schedule_delayed_work(&work, delay);

这时,&work指向的work_struct直到delay指定的时钟节拍用完以后才会执行。

6. 工作队列的简单应用

  1. <span style="font-weight: normal;">#include<linux/module.h>  
  2. #include<linux/init.h>  
  3. #include<linux/workqueue.h>  
  4.   
  5. staticstruct workqueue_struct *queue =NULL;  
  6. staticstruct work_struct work;  
  7.   
  8. staticvoid work_handler(struct work_struct*data)  
  9. {  
  10. printk(KERN_ALERT"work handler function./n");  
  11. }  
  12.   
  13. staticint __init test_init(void)  
  14. {  
  15. queue= create_singlethread_workqueue("helloworld"); /*创建一个单线程的工作队列*/  
  16. if(!queue)  
  17. goto err;  
  18.   
  19. INIT_WORK(&work, work_handler);  
  20. schedule_work(&work);/*schedule_work是添加到系统的events workqueue, 要添加到自己的workqueue, 应该使用queue_work, 故此处有误*/  
  21.   
  22. return 0;  
  23. err:  
  24. return-1;  
  25. }  
  26.   
  27. staticvoid __exit test_exit(void)  
  28. {  
  29. destroy_workqueue(queue);  
  30. }  
  31. MODULE_LICENSE("GPL");  
  32. module_init(test_init);  
  33. module_exit(test_exit);</span>  



tasklet与workqueue的区别和不同应用环境总结  


tasklet

Workqueue

处于atomic context,不能sleep

不处于atomic context,可以sleep

处于中断上下文,OS不可以进行进程调度

处于进程上下文,OS可以进行进程调度

运行调度它们的同一个CPU上

默认同一个CPU上

不能指定确定时间进行调度

不能指定确定时间进行调度或者指定至少延时一个确定时间后调度

只能交给ksoftirqd/0

可以提交给events/0,也可以提交给自定义的workqueue

Tasklet函数带参数

Work函数不带参数

Tasklet与workqueue的不同应用环境总结如下:

(1) 必须立即进行紧急处理的极少量任务放入在中断的顶半部中,此时屏蔽了与自己同类型的中断,由于任务量少,所以可以迅速不受打扰地处理完紧急任务。

(2) 需要较少时间的中等数量的急迫任务放在tasklet中。此时不会屏蔽任何中断(包括与自己的顶半部同类型的中断),所以不影响顶半部对紧急事务的处理;同时又不会进行用户进程调度,从而保证了自己急迫任务得以迅速完成。

(3) 需要较多时间且并不急迫(允许被操作系统剥夺运行权)的大量任务放在workqueue中。此时操作系统会尽量快速处理完这个任务,但如果任务量太大,期间操作系统也会有机会调度别的用户进程运行,从而保证不会因为这个任务需要运行时间将其它用户进程无法进行。

(4) 可能引起睡眠的任务放在workqueue中。因为在workqueue中睡眠是安全的。



softirq和tasklet都属于软中断,tasklet是softirq的特殊实现;

workqueue是普通的工作队列。

1、softirq

软中断支持SMP,同一个softirq可以在不同的CPU上同时运行,softirq必须是可重入的。软中断是在编译期间静态分配的,它不像tasklet那样能被动态的注册或去除。kernel/softirq.c中定义了一个包含32个softirq_action结构体的数组。每个被注册的软中断都占据该数组的一项。因此最多可能有32个软中断。2.6版本的内核中定义了六个软中断:HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、 SCSI_SOFTIRQ、TASKLET_SOFTIRQ
一般情况下,在硬件中断处理程序后都会试图调用do_softirq()函数,每个CPU都是通过执行这个函数来执行软中断服务的。由于软中断不能进入硬中断部分,且同一个CPU上软中断的执行是串行的,即不允许嵌套,因此,do_softirq()函数一开始就检查当前CPU是否已经正出在中断服务中,如果是则 do_softirq()函数立即返回。这是由do_softirq()函数中的 if (in_interrupt()) return; 保证的。

2、tasklet

引入tasklet,最主要的是考虑支持SMP,提高SMP多个cpu的利用率;不同的tasklet可以在不同的cpu上运行。tasklet可以理解为softirq的派生,所以它的调度时机和软中断一样。对于内核中需要延迟执行的多数任务都可以用tasklet来完成,由于同类tasklet本身已经进行了同步保护,所以使用tasklet比软中断要简单的多,而且效率也不错。 tasklet把任务延迟到安全时间执行的一种方式,在中断期间运行,即使被调度多次,tasklet也只运行一次,不过tasklet可以在SMP系统上和其他不同的tasklet并行运行。在SMP系统上,tasklet还被确保在第一个调度它的CPU上运行,因为这样可以提供更好的高速缓存行为,从而提高性能。 


    与一般的软中断不同,某一段tasklet代码在某个时刻只能在一个CPU上运行,但不同的tasklet代码在同一时刻可以在多个CPU上并发地执行。 Kernel/softirq.c中用tasklet_trylock()宏试图对当前要执行的tasklet(由指针t所指向)进行加锁,如果加锁成功(当前没有任何其他CPU正在执行这个tasklet),则用原子读函数atomic_read()进一步判断count成员的值。如果count为0,说明这个tasklet是允许执行的。如果tasklet_trylock()宏加锁不成功,或者因为当前tasklet的count值非0而不允许执行时,我们必须将这个tasklet重新放回到当前CPU的tasklet队列中,以留待这个CPU下次服务软中断向量TASKLET_SOFTIRQ时再执行。为此进行这样几步操作:

(1)先关 CPU中断,以保证下面操作的原子性。

(2)把这个tasklet重新放回到当前CPU的tasklet队列的首部;

(3)调用 __cpu_raise_softirq()函数在当前CPU上再触发一次软中断请求TASKLET_SOFTIRQ;

(4)开中断。
软中断和tasklet都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和tasklet不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。也正是由于它们运行在中断上下文中,所以它们在同一个CPU上的执行是串行的,这样就不利于实时多媒体任务的优先处理。

3、workqueue

什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet

 

 

*******************************************************************************************************

 

 当前的2.6版内核中,有三种可能的选择:softirq、tasklet和work queue。 tasklet基于softirq实现,所以两者很相近。work queue与它们完全不同,它靠内核线程实现。

1、softirq

       软中断支持SMP,同一个softirq可以在不同的CPU上同时运行,softirq必须是可重入。软中断是在编译期间静态分配的,它不像tasklet那样能被动态的注册或去除。kernel/softirq.c中定义了一个包含32个 softirq_action结构体的数组。每个被注册的软中断都占据该数组的一项。因此最多可能有32个软中断。2.6版本的内核中定义了六个软中断:HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、SCSI_SOFTIRQ、 TASKLET_SOFTIRQ。

       软中断的特性:
       1).一个软中断不会抢占另外一个软中断。
       2).唯一可以抢占软中断的是中断处理程序。
       3).其他软中断(包括相同类型的)可以在其他的处理其上同时执行。
       4).一个注册的软中断必须在被标记后才能执行。
       5).软中断不可以自己休眠(即调用可阻塞的函数或sleep等)。
       6).索引号小的软中断在索引号大的软中断之前执行。

2、tasklet

      引入tasklet,最主要的是考虑支持SMP,提高SMP多个cpu的利用率;两个相同的tasklet决不会同时执行。tasklet可以理解为softirq的派生,所以它的调度时机和软中断一样。对于内核中需要延迟执行的多数任务都可以用tasklet来完成,由于同类tasklet本身已经进行了同步保护,所以使用tasklet比软中断要简单的多,而且效率也不错。tasklet把任务延迟到安全时间执行的一种方式,在中断期间运行,即使被调度多次,tasklet也只运行一次,不过tasklet可以在SMP系统上和其他不同的tasklet并行运行。在SMP系统上,tasklet还被确保在第一个调度它的CPU上运行,因为这样可以提供更好的高速缓存行为,从而提高性能。

       tasklet的特性:.不允许两个两个相同类型的tasklet同时执行,即使在不同的处理器上。

3、work queue

       如果推后执行的任务需要睡眠,那么就选择工作队列。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。work queue造成的开销最大,因为它要涉及到内核线程甚至是上下文切换。这并不是说work queue的低效,但每秒钟有数千次中断,就像网络子系统时常经历的那样,那么采用其他的机制可能更合适一些。 尽管如此,针对大部分情况工作队列都能提供足够的支持。

      工作队列特性:
      1).工作队列会在进程上下文中执行!
      2).可以阻塞。(前两种机制是不可以阻塞的)
      3).可以被重新调度。(前两种只可以被中断处理程序打断)
      4).使用工作队列的两种形式:
             1>缺省工作者线程(works threads)
             2>自建的工作者线程
      5).在工作队列和内核其他部分之间使用锁机制就像在其他的进程上下文一样。
      6).默认允许响应中断。
      7).默认不持有任何锁。

4、softirq和tasklet共同点

       软中断和tasklet都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和tasklet不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。也正是由于它们运行在中断上下文中,所以它们在同一个CPU上的执行是串行的,这样就不利于实时多媒体任务的优先处理。

5、总结

表 1. 对下半部的比较

下半部 上下文 顺序执行保障
软中断 中断 没有
Tasklet 中断 同类型不能同时执行
工作队列 进程 没有(和进程上下文一样被调度)
       简单地说,一般的驱动程序的编写者需要做两个选择。 首先,你是不是需要一个可调度的实体来执行需要推后完成的工作――从根本上来说,有休眠的需要吗?要是有,工作队列就是你的惟一选择。 否则最好用tasklet。要是必须专注于性能的提高,那么就考虑softirq。
1
0
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值