softirq, tasklet, work_queue


softirq, tasklet, work_queue


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



################################################################################

中断处理的tasklet(小任务)机制和workqueue(工作队列)机制

################################################################################

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

中断服务程序一般都是在中断请求关闭的条件下执行的,以避免嵌套而使中断控制复杂化。但是,中断是一个随机事件,它随时会到来,如果关中断的时间太长,CPU就不能及时响应其他的中断请求,从而造成中断的丢失。因此,Linux内核的目标就是尽可能快的处理完中断请求,尽其所能把更多的处理向后推迟。例如,假设一个数据块已经达到了网线,当中断控制器接受到这个中断请求信号时,Linux内核只是简单地标志数据到来了,然后让处理器恢复到它以前运行的状态,其余的处理稍后再进行(如把数据移入一个缓冲区,接受数据的进程就可以在缓冲区找到数据)。因此,内核把中断处理分为两部分:上半部(tophalf)和下半部(bottomhalf),上半部(就是中断服务程序)内核立即执行,而下半部(就是一些内核函数)留着稍后处理, . Y0 C; i2 I5 i& Z. a/ t& e
首先,一个快速的“上半部”来处理硬件发出的请求,它必须在一个新的中断产生之前终止。通常,除了在设备和一些内存缓冲区(如果你的设备用到了DMA,就不止这些)之间移动或传送数据,确定硬件是否处于健全的状态之外,这一部分做的工作很少。 9 n7 {( ^& Q; _1 F0 B. E. G3 T
下半部运行时是允许中断请求的,而上半部运行时是关中断的,这是二者之间的主要区别。 3 T9 R4 E8 H+ {0 q9 i7 Y# u
但是,内核到底什时候执行下半部,以何种方式组织下半部?这就是我们要讨论的下半部实现机制,这种机制在内核的演变过程中不断得到改进,在以前的内核中,这个机制叫做bottomhalf(简称bh),在2.4以后的版本中有了新的发展和改进,改进的目标使下半部可以在多处理机上并行执行,并有助于驱动程序的开发者进行驱动程序的开发。下面主要介绍常用的小任务(Tasklet)机制及2.6内核中的工作队列机制。 ' b: A; u2 a9 K9 ]& |
. K( ?; Y8 `+ \" x- ?
小任务机制 # N5 S$ O& s& l2 x) \8 f3 x9 G
. [- m; K4 z$ B1 q2 e! S
这里的小任务是指对要推迟执行的函数进行组织的一种机制。其数据结构为tasklet_struct,每个结构代表一个独立的小任务,其定义如下: 
structtasklet_struct { 
structtasklet_struct *next; /*指向链表中的下一个结构*/ ) g8 g) E; Q; |+ R% Z5 p: {
unsignedlong state; /* 小任务的状态*/ " L- [4 s" N" S- Q( b" B$ o1 F
atomic_tcount; /* 引用计数器*/ ! W% ]- K+ A0 |* i* [) z
void(*func) (unsigned long); /* 要调用的函数*/ 
unsignedlong data; /* 传递给函数的参数*/ 
}; & w  W2 i) r5 ~- R% Y
结构中的func域就是下半部中要推迟执行的函数,data是它唯一的参数。 
State域的取值为TASKLET_STATE_SCHED或TASKLET_STATE_RUN。TASKLET_STATE_SCHED表示小任务已被调度,正准备投入运行,TASKLET_STATE_RUN表示小任务正在运行。TASKLET_STATE_RUN只有在多处理器系统上才使用,单处理器系统什么时候都清楚一个小任务是不是正在运行(它要么就是当前正在执行的代码,要么不是)。 
Count域是小任务的引用计数器。如果它不为0,则小任务被禁止,不允许执行;只有当它为零,小任务才被激活,并且在被设置为挂起时,小任务才能够执行。

1. 声明和使用小任务大多数情况下,为了控制一个寻常的硬件设备,小任务机制是实现下半部的最佳选择。小任务可以动态创建,使用方便,执行起来也比较快。 9 R7 f' \! M4 ~7 K& u" }' l
我们既可以静态地创建小任务,也可以动态地创建它。选择那种方式取决于到底是想要对小任务进行直接引用还是一个间接引用。如果准备静态地创建一个小任务(也就是对它直接引用),使用下面两个宏中的一个: . T7 g, [1 |& N3 s* x
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指针,该小任务就会被调度以便适当的时候执行: 1 ^- l. 
tasklet_schedule(&my_tasklet); /*把my_tasklet标记为挂起 */ 
在小任务被调度以后,只要有机会它就会尽可能早的运行。在它还没有得到运行机会之前,如果一个相同的小任务又被调度了,那么它仍然只会运行一次。 + u4 Y1 z: o$ C$ Y% Z. a
可以调用tasklet_disable()函数来禁止某个指定的小任务。如果该小任务当前正在执行,这个函数会等到它执行完毕再返回。调用tasklet_enable()函数可以激活一个小任务,如果希望把以DECLARE_TASKLET_DISABLED()创建的小任务激活,也得调用这个函数,如: * [6 Y; a! o$ d% Z# N
tasklet_disable(&my_tasklet); /*小任务现在被禁止,这个小任务不能运行*/ 
tasklet_enable(&my_tasklet); /* 小任务现在被激活*/ ' b. y# d% d7 _3 P
也可以调用tasklet_kill()函数从挂起的队列中去掉一个小任务。该函数的参数是一个指向某个小任务的tasklet_struct的长指针。在小任务重新调度它自身的时候,从挂起的队列中移去已调度的小任务会很有用。这个函数首先等待该小任务执行完毕,然后再将它移去。 

4.tasklet的简单用法 6 o8 s: X2 [( ~0 |/ h. h
下面是tasklet的一个简单应用,以模块的形成加载。 2 w0 @  h5 R7 a# I
% m: d- l( ?+ R% d7 R
#include <linux/module.h> 9 O$ T) y. K) j* @5 N
#include <linux/init.h> 7 S  C5 G9 _  `/ B5 p
#include <linux/fs.h> : V8 r7 \6 Q" F1 l7 U
#include <linux/kdev_t.h> ( B* f6 N7 |+ e! I3 m" B
#include <linux/cdev.h> 
#include <linux/kernel.h> , T+ G/ o& u6 o4 H$ Z) d
#include <linux/interrupt.h> 
3 B- W( Q4 I6 r; k  W
static struct t asklet_struct my_tasklet; % w. e7 b2 q0 d1 |
" A" A. J) W  v
static void tasklet_handler (unsigned long d ata) 
9 u9 W2 z) I, e: C5 n  n2 a
printk(KERN_ALERT,"tasklet_handler is running./n"); 

7 `: T2 u, ~. F# ~) A9 G. j& j
static int __init test_init(void) 

tasklet_init(&my_tasklet,tasklet_handler,0); , _* ]7 L! O4 e3 L# _0 |2 v
tasklet_schedule(&my_tasklet); 
return0; 


static void __exit test_exit(void) : r( p$ b- f& C6 M3 [
/ \9 O" D" j* L2 b$ l
tasklet_kill(&tasklet); 0 ?/ x- J5 y. e% u, Q, ~
printk(KERN_ALERT,"test_exit is running./n"); 1 N. h4 f( G! I5 |
) P& N( ~: I0 M+ G  `3 a9 k
MODULE_LICENSE("GPL"); 
, p# P8 W* M# W- x' ^
module_init(test_init); ' y3 a6 F7 [1 H7 {) r
module_exit(test_exit); 
从这个例子可以看出,所谓的小任务机制是为下半部函数的执行提供了一种执行机制,也就是说,推迟处理的事情是由tasklet_handler实现,何时执行,经由小任务机制封装后交给内核去处理。 1 J0 j9 U. P- w3 b- D8 ?) I

二、中断处理的工作队列机制 9 ?; V( p, X0 n6 W' ~% l. _
工作队列(work queue)是另外一种将工作推后执行的形式,它和前面讨论的tasklet有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许被重新调度甚至是睡眠。 
那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。 

工作、工作队列和工作者线程 
如前所述,我们把推后执行的任务叫做工作(work),描述它的数据结构为work_struct,这些工作以队列结构组织成工作队列(workqueue),其数据结构为workqueue_struct,而工作线程就是负责执行工作队列中的工作。系统默认的工作者线程为events,自己也可以创建自己的工作者线程。 8 u% 
表示工作的数据结构 / G7 F0 [% Q9 W8 A7 @
工作用<linux/workqueue.h>中定义的work_struct结构表示: - I* e8 i/ v: v' _  y3 o5 @
struct work_struct{ 
unsigned long pending; /* 这个工作正在等待处理吗?*/ 
struct list_head entry; /* 连接所有工作的链表 */ 
void (*func) (void *); /* 要执行的函数 */ - w3 S# v, e  E- m" Q
void *data; /* 传递给函数的参数 */ 
void *wq_data; /* 内部使用 */ 
struct timer_list timer; /* 延迟的工作队列所用到的定时器 */   t- u. w& d2 d  P0 G7 a
}; 
这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。

3. 创建推后的工作 
要使用工作队列,首先要做的是创建一些需要推后完成的工作。可以通过DECLARE_WORK在编译时静态地建该结构: : ?3 i8 A) E( S! u  u4 z& m( U6 y
DECLARE_WORK(name, void (*func) (void *), void *data); 7 b4 L* V% X( k3 e% d
这样就会静态地创建一个名为name,待执行函数为func,参数为data的work_struct结构。 : G( W7 l9 M" _
同样,也可以在运行时通过指针创建一个工作: ) f. J" u) y7 z: r0 O0 ^9 T+ E: k
INIT_WORK(struct work_struct *work, woid(*func) (void *), void *data); 2 _, q" C9 q2 |/ `" ]
这会动态地初始化一个由work指向的工作。 

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

5. 对工作进行调度 
现在工作已经被创建,我们可以调度它了。想要把给定工作的待处理函数提交给缺省的events工作线程,只需调用 ) n9 W; V4 D/ F9 N
schedule_work(&work);work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。 , T, t- Z! X& i8 U& D. Z- O
有时候并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,可以调度它在指定的时间执行: 
schedule_delayed_work(&work, delay); ) [6 ~; m! o/ [) J2 e
这时,&work指向的work_struct直到delay指定的时钟节拍用完以后才会执行。 

6. 工作队列的简单应用 ! P( ?/ ^- U, [( }1 u9 S7 q8 p2 K
#include <linux/module.h> 2 k- V  g& d+ a  B8 _
#include <linux/init.h> 
#include <linux/workqueue.h> - L( {, |2 k3 }2 Z7 O* k
4 b9 J* y5 z  m' N
static struct workqueue_struct *queue = NULL; / E! K+ G0 n; d/ U. q8 Q
static struct work_struct work; 4 c; G  _# ]0 q5 P* m3 U1 ~

static void work_handler(struct work_struct *data) 
' X; r+ R% K: j; Z' H9 j
printk(KERN_ALERT "work handler function./n"); 6 j6 P' h* p5 Z% }/ x6 h9 X


static int __init test_init(void) & S' D* [  ?9 a0 y/ j8 C

queue = create_singlethread_workqueue("helloworld"); /*创建一个单线程的工作队列*/ 7 }  I' P' w5 K5 Z8 h1 {' g* k
if (!queue) 
goto err; 4 V) ?4 D/ u5 {

INIT_WORK(&work, work_handler); 0 J5 z7 j. F  ^+ V* a
schedule_work(&work); % ?4 u  J: v5 E8 i

return 0; 
err: 
return -1; 5 E5 M9 Y' t: I% C# _" d# c
) b- A0 f: U4 Z7 J
4 D- c) L; {" O7 y/ g7 e
static void __exit test_exit(void) + U7 @( e* q+ C* b1 f7 V
3 a7 B- r& F' ?
destroy_workqueue(queue); ) r7 q0 X. u. U! }
9 E( p; n& F2 @# H6 t( L
MODULE_LICENSE("GPL"); $ C4 j- R+ }; f/ Z. R. ]; A  H5 [
module_init(test_init); 
module_exit(test_exit);



Tasklets

另一个有关于定时的内核设施是 tasklet。它类似内核定时器:在中断时间运行且运行同一个 CPU 上, 并接收一个 unsigned long 参数。不同的是:无法要求在一个指定的时间执行函数,只能简单地要求它在以后的一个由内核选择的时间执行。它对于中断处理特别有用:硬件中断必须尽快处理, 但大部分的数据管理可以延后到以后安全的时间执行。 实际上, 一个 tasket, 就象一个内核定时器, 在一个"软中断"的上下文中执行(以原子模式)。软件中断是在使能硬件中断时执行异步任务的一个内核机制。

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;
};
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*/

tasklet 的特点:
(1)一个 tasklet 能够被禁止并且之后被重新使能; 它不会执行,直到它被使能与被禁止相同的的次数;
(2)如同定时器, 一个 tasklet 可以注册它自己;
(3)一个 tasklet 能被调度来执行以正常的优先级或者高优先级;
(4) 如果系统不在重负载下,taslet 可能立刻运行, 但是从不会晚于下一个时钟嘀哒;
(5)一个 tasklet 可能和其他 tasklet 并发, 但是它自己是严格地串行的 ,且tasklet 从不同时运行在不同处理器上,通常在调度它的同一个 CPU 上运行。

工作队列

工作队列类似 taskets,允许内核代码请求在将来某个时间调用一个函数,不同在于:
(1)tasklet 在软件中断上下文中运行,所以 tasklet 代码必须是原子的。而工作队列函数在一个特殊内核进程上下文运行,有更多的灵活性,且能够休眠。
(2)tasklet 只能在最初被提交的处理器上运行,这只是工作队列默认工作方式。
(3)内核代码可以请求工作队列函数被延后一个给定的时间间隔。
(4)tasklet 执行的很快, 短时期, 并且在原子态, 而工作队列函数可能是长周期且不需要是原子的,两个机制有它适合的情形。

工作队列有 struct workqueue_struct 类型,在 <linux/workqueue.h> 中定义。一个工作队列必须明确的在使用前创建,宏为:

struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);

每个工作队列有一个或多个专用的进程("内核线程"), 这些进程运行提交给这个队列的函数。若使用 create_workqueue, 就得到一个工作队列它在系统的每个处理器上有一个专用的线程。在很多情况下,过多线程对系统性能有影响,如果单个线程就足够则使用 create_singlethread_workqueue 来创建工作队列。

提交一个任务给一个工作队列,在这里《LDD3》介绍的内核2.6.10和我用的新内核2.6.22.2已经有不同了,老接口已经不能用了,编译会出错。这里我只讲2.6.22.2的新接口,至于老的接口我想今后内核不会再有了。从这一点我们可以看出内核发展。

/*需要填充work_struct或delayed_work结构,可以在编译时完成, 宏如下: */

struct work_struct {
    atomic_long_t data;
#define WORK_STRUCT_PENDING 0        /* T if work item pending execution */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
    struct list_head entry;
    work_func_t func;
};

struct delayed_work {
    struct work_struct work;
    struct timer_list timer;
};

DECLARE_WORK(n, f)    
/*n 是声明的work_struct结构名称, f是要从工作队列被调用的函数*/
DECLARE_DELAYED_WORK(n, f)
/*n是声明的delayed_work结构名称, f是要从工作队列被调用的函数*/

/*若在运行时需要建立 work_struct 或 delayed_work结构, 使用下面 2 个宏定义:*/
INIT_WORK(struct work_struct *work, void (*function)(void *)); 
PREPARE_WORK(struct work_struct *work, void (*function)(void *)); 
INIT_DELAYED_WORK(struct delayed_work *work, void (*function)(void *)); 
PREPARE_DELAYED_WORK(struct delayed_work *work, void (*function)(void *)); 
/* INIT_* 做更加全面的初始化结构的工作,在第一次建立结构时使用. PREPARE_* 做几乎同样的工作, 但是它不初始化用来连接 work_struct或delayed_work 结构到工作队列的指针。如果这个结构已经被提交给一个工作队列, 且只需要修改该结构,则使用 PREPARE_* 而不是 INIT_* */

/*有 2 个函数来提交工作给一个工作队列:*/
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct delayed_work *work, unsignedlong delay);
/*每个都添加work到给定的workqueue。如果使用 queue_delay_work, 则实际的工作至少要经过指定的 jiffies 才会被执行。 这些函数若返回 1 则工作被成功加入到队列; 若为0,则意味着这个 work 已经在队列中等待,不能再次加入*/

在将来的某个时间, 这个工作函数将被传入给定的 data 值来调用。这个函数将在工作线程的上下文运行, 因此它可以睡眠 (你应当知道这个睡眠可能影响提交给同一个工作队列的其他任务) 工作函数不能访问用户空间,因为它在一个内核线程中运行, 完全没有对应的用户空间来访问。
取消一个挂起的工作队列入口项可以调用:

int cancel_delayed_work(struct delayed_work *work); 
void cancel_work_sync(struct work_struct *work)

如果这个入口在它开始执行前被取消,则返回非零。内核保证给定入口的执行不会在调用 cancel_delay_work 后被初始化. 如果 cancel_delay_work 返回 0, 但是, 这个入口可能已经运行在一个不同的处理器, 并且可能仍然在调用 cancel_delayed_work 后在运行. 要绝对确保工作函数没有在 cancel_delayed_work 返回 0 后在任何地方运行, 你必须跟随这个调用来调用:

void flush_workqueue(struct workqueue_struct *queue);

在 flush_workqueue 返回后, 没有在这个调用前提交的函数在系统中任何地方运行。
而cancel_work_sync会取消相应的work,但是如果这个work已经在运行那么cancel_work_sync会阻塞,直到work完成并取消相应的work。

当用完一个工作队列,可以去掉它,使用:

void destroy_workqueue(struct workqueue_struct *queue);

共享队列

在许多情况下, 设备驱动不需要它自己的工作队列。如果你只偶尔提交任务给队列, 简单地使用内核提供的共享的默认的队列可能更有效。若使用共享队列,就必须明白将和其他人共享它,这意味着不应当长时间独占队列(不能长时间睡眠), 并且可能要更长时间才能获得处理器。

使用的顺序:
(1) 建立 work_struct 或  delayed_work

static struct work_struct jiq_work;
static struct delayed_work jiq_work_delay;

    /* this line is in jiq_init() */
INIT_WORK(&jiq_work, jiq_print_wq);
INIT_DELAYED_WORK(&jiq_work_delay, jiq_print_wq);

(2)提交工作

int schedule_work(&jiq_work);/*对于work_struct结构*/
int schedule_delayed_work(&jiq_work_delay, delay);/*对于delayed_work结构*/

/*返回值的定义和 queue_work 一样*/

若需取消一个已提交给工作队列入口项, 可以使用 cancel_delayed_work和cancel_work_sync, 但刷新共享队列需要一个特殊的函数:

void flush_scheduled_work(void);

因为不知道谁可能使用这个队列,因此不可能知道 flush_schduled_work 返回需要多长时间。




你可能感兴趣的:(数据结构,工作,timer,struct,任务,linux内核)