第六章 延迟操作
tasklet需要在中断上下文中执行, 所以有些延尽操作无法用它来完成, 为此内核又提供了一个期于进程的延
迟操作实现机制, 工作队列workqueue.
本文欢迎转载
本文出处:http://blog.csdn.net/dyron
6.1 tasklet
tasklet是内核定义的几种softirq之一, 中断处理例程常常利用tasklet来完成一些延后的处理, 根据优先级
的不同, 内核将tasklet分成两种, 对softirq中对应TASKLET_SOFTIRQ和HI_SOFTIRQ,后者优先于前者执行.
6.1.1 tasklet机制初始化
系统初始化期间通过调用softirq_init为TASKLET_SOFTIRQ和HI_SOFTIRQ安装了执行函数:
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
int i;
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
for (i = 0; i < NR_SOFTIRQS; i++)
INIT_LIST_HEAD(&per_cpu(softirq_work_list[i], cpu));
}
register_hotcpu_notifier(&remote_softirq_cpu_notifier);
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
函数中的for_each_possible_cpu循环用初始化管理tasklet链表的变量tasklet_vec和tasklet_hi_vec,open_
softirq用来给TASKLET_SOFTIRQ和HI_SOFTIRQ安装对应的函数:
softirq_vec[TASKLET_SOFTIRQ].action = tasklet_action;
softirq_vec[HI_SOFTIRQ].action = tasklet_hi_action;
softirq_vec是一个struct softirq_action类型的数组, 数组的每一项对应一个软中断的函数指针.
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
软中断处理函数的原型为:
struct softirq_action
{
void (*action)(struct softirq_action *);
}
在中断处理的SOFTIRQ部分, 如果发现本地CPU的__softirq_pending上TASKLET_SOFTIRQ/HI_SOFTIRQ位被置1,
就将调用tasklet_action或者tasklet_hi_action.
6.1.2 提交一个tasklet
struct tasklet_struct用来表示一个tasklet对象
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
struct tasklet_struct *next: 用来将系统中的tasklet对象构建成链表.
unsinged long state: 记录每个tasklet在系统中的状态, 其值是枚举型变量TASKLET_STATE_SCHED和TASKLE
T_STATE_RUN两者之一.
TASKLET_STATE_SCHED表示当前tasklet已经被提交.
TASKLET_STATE_RUN只用在对称多处理器系统smp中, 表示当前tasklet正在执行.
atomic_t count: 用来实现tasklet的disable和enable操作, count.counter=0表示当前的tasklet是enable的
, 可以被调度, 否则便是个disabled的tasklet, 不可以被执行.
void (*func)(unsigned long):tasklet上的执行函数, 当tasklet在SOFTIRQ被调度执行时, 该函数指针指向
的函被调用, 用来完成驱动程序中实际的延迟操作任务.
unsigned long data: func指向的函数被调用时, data将作为参数传给func函数. 驱动利用data向tasklet上
的执行函数传递特定的参数.
驱动程序用DECLARE_TASKLET宏声明并初始化一个静态的tasklet对象:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = {NULL, 0, ATOMIC_INIT(0), func, data}
DECLARE_TASKLET_DISABLED用来声明一个处于disabled状态的对象:
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = {NULL, 0, ATOMIC_INIT(1), func, data}
动作构建一个tasklet对象:
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->data = data;
}
声明了tasklet对象后, 需要调用tasklet_schedule向系统提交这个tasklet. 这个动作实际上就是将一个
tasklet对象加入到tasklet_vec管理的链表中. HI_SOFTIRQ提交函数为tasklet_hi_schedule, 除了用来管理
tasklet对象链表的变量为tasklet_hi_vec外, 其它方面完全一样.
static inline void tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
if (!test_and_set_bi(TASKLET_STATE_SCHED, &t->state)) {
local_irq_save(flags);
t->next = NULL;
*__get_cpu_var(tasklet_vec).tail = 5;
__get_cpu_var(tasklet_vec).tail = &(t->next);
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
}
tasklet_vec是个per-cpu型变量, 用来将系统中所有抬爱过tasklet_schedule函数提交的tasklet对象构建成
链表, 如要是多处理器系统, 那么每个处理器都将用各自的tasklet_vec管理提交到其上的tasklet.
struct tasklet_head
{
struct tasklet_struct *head; //总是指向tasklet对象链表的第一个节点
struct tasklet_struct **tail; //tasklet对象指针的指针. taill总是保存tasklet链表最后一个节点
//所在tasklet对象中next成员的地址.
}
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
tasklet_vec的初始化最早发生在linux系统初始化阶段调用的softirq_init函数中.
tasklet_schedule首先检查要提交的tasklet的state上的TASKLET_STATE_SCHED位有没有置1, 对一个尚未提交
过的tasklet对象来说, 其值应该是0, 所以test_and_set_bit会返回0, 同时把tasklet上的state上的TASKLET
_STATE_SCHED位置1表明这个tasklet已被提交. 此后该tasklet对象的TASKLET_STATE_SCHED位一直为1, 直到
被调度运行, 因此一个tasklet对象被成功提交进系统但尚未调度执行时, 处于TASKLET_STATE_SCHED状态.此
时即便是多处理器系统中, 运行在其他处理器上的tasklet_schedule函数也无法再次提交一个处于这种状态的
tasklet对象, 因此一个tasklet对象在同一时间只可能在一个处理器上运行, 不会在多cpu上有多个实例.
如果可以提交, 接下来的工作就是把它加入到当前处理器tasklet_vec管理的链表中, 然后通过raise_softirq
_irqoff(TASKLET_SOFTIRQ)调用告诉SOFTIRQ部分当前处理器有个TASKLET_SOFTIRQ等待处理. raise_softirq
_irqoff用一个整型变量的位来表示该位上是否有待决定的softirq, 1表示有, 0表示没有.
??????此时如果在执行tasklet时, 又来中断了又掉用tasklet会失败, 是否会丢掉数据.
6.1.3 tasklet_action
内核分别为TASKLET_SFOTIRQ和HI_SOFTIRQ分别安装了执行函数tasklet_action/tasklet_hi_action.
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable();
list = __this_cpu_read(tasklet_vec.head);
__this_cpu_write(tasklet_vec.head, NULL);
__this_cpu_write(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;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}
函数的主体是个while循环, 进入循环之前, 需要得到tasklet链表的头指针, 这需要访问per-cpu变量tasket
_vec, 该变量用来管理tasklet链表, tasklet_vec.head指向tasklet链表的第一个节点, 此时注意在访问tas
ket_vec之前, 关闭了处理器中断, 这是因为虽然tasklet_vec在每个处理器中都有副本, 但在单一cpu上, 依
然存在SOFTIRQ在执行时被外设中断, 在它的中断处理中使用到了tasklet的功能,如调用tasklet_schedule来
提交一个tasklet对象, 这样会导致两个执行路径有操作tasklet_vec的可能性, 所以用关中断和开中断来保护
tasklet_vec不会在可能的并发访问中被破坏, 代码将tasklet_vec管理的链表第一个节点放在本地变量中,然
后将tasklet_vec设置成其最初的状态.
tasklet_action是一个softirq执行函数, 在多处理器上可能同时在不同的处理器上运行, 虽然处于TASKLET_
STATE_SCHED状态的tasklet对象不能被多次提交, 但当一个tasklet被调度运行时, TASKLET_STATE_SCHED状态
位会被清掉, 这样就可能导致该tasklet对象在别的处理器上被重新提交.
while循环中tasklet_action通过一个本地变量list来对tasklet链表遍历,对于遍历过程中的每一个tasklet节
点, 如果不满足执行条件, 将通过tasklet_vec.tail指针将其重新加入tasklet_vec链表. 通过一个本地变量
list, 使得我们在调用tasklet上的执行函数时, 无须考虑list 链表的互斥访问问题, 因此在tasklet上执行
函数在运行期间, 中断是打开的.
tasklet_trylock在单处理器中直接返回1, 在多处理器中定义如下:
static inline int tasklet_trylock(struct tasklet_struct *t)
{
return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}
函数将tasklet中state的TASKLET_STATE_RUN位置1, 同时返回TASKLET_STATE_RUN原来的值, 在smp系统中一个
运行中的tasklet有可能被重新提供到另一处理器的tasklet_vec链表中, 为了防止该tasklet同时在不同的处
理器上运行, 内核在smp系统中为tasklet对象增加了一个额外的状态位TASKLET_STATE_RUN, 这个状态位只对
smp系统有效, 单处理器不需要这个状态, 内核用tasklet对象的TASKLET_STATE_RUN位来标记对应的tasklet当
前是否正在运行, 如果没有,tasklet_trylock(t)将返回真, 同时tasklet_trylock会将state中的TASKLET_STA
TE_RUN位置1, 这样其它CPU再运行tasklet_action时, 将不会处理该tasklet直到其运行完毕清除掉TASKLET_
STATE_RUN, 内核通过这种方式实现了tasklet的串行化.
接下来通过atomic_read对tasklet的count成员进行测试, 这个成员主要用来实现enable或disable一个taskle
t, 如果某个对象的count为0, 说明它处于enable, 对于一个enabled的tasklet, 需要再测试其state的TASKL
ET_STATE_SCHED位有没有被置1, 提交的tasklet函数会设置该位, 如果没有被设置, 说明正在试图调度一个没
有被提交的tasklet.
tasklet运行在中断上下文环境中, 因此在中断上下文的种种限制同样适用于tasklet的延迟函数.
6.1.4 tasklet的其他操作
. tasklet_disable/tasklet_disable_nosync
这两个函数用来disable一个tasklet. disable就是将要操作tasklet对象t上的count加1就可以了. 相对于
tasklet_disable_nosync是个同步版本, 它在调用tasklet_disable_nosync函数之后, 会再调用tasklet_un
lock_wait函数实现所谓同步功能. 此功能仅限于SMP系统, 如果要disable的tasklet正在运行, 那么taskl
et_unlcok_wait要一直忙等到t的TASKLET_STATE_RUN状态被清除, 就是说等到t运行完才会完回.
一个处于disabled状态的tasklet可以被提交到tasklet_vec, 但不会被调度执行.
. tasklet_enable
就是将指定的tasklet对象t上的count减1, 一个tasklet要被执行, count应该为0, 如果想enable一个先前被
disable的tasklet, 使之能被调度执行, tasklet_enable和tasklet_disable的调用次数要匹配.
. tasklet_kill
函数通过清除一个tasklet对象的TASKLET_STATE_SCHED状态位, 使SOFTIRQ不再能调度运行它. 如果tasklet
对象正在运行, 那么tasklet_kill将忙等待直到tasklet运行结束, 这样可以确保tasklet_kill返回后系统中
不再有运行中的该tasklet对象. 如果一个tasklet对象被提交到了系统但没有被调度执行, 那么tasklet_kill
会睡眠直到该tasklet被执行完从tasklet_vec链表中移除, 这个函数是可能被阻塞的.
在驱动模块要被移除时, 你可能会删除tasklet对象所在的空间, 但这不会影响到tasklet_vec已有的链表元素
构成, 所以可能有一个情况, 驱动已经移出系统, SOFTIRQ还是调度运行了你的驱动程序提交的tasklet对象,
这很危险, 因为你的模块已经从系统移除后,也许被调度运行的tasklet函数也许会使用到模块中的资源, 内核
模块调用tasklet_kill可以确保不会发生这种情况.
6.2 工作队列work queue
设计思想与现实中的工厂加工非常相象: 基础设施就是一条加工厂的生产流水线和在流水线上工作的工人, 平
时没事的时候,流水线上的工作就休息. 如果某一客户想要加工一件工作,只需要把要加班的工件打个包,扔到
流水线上, 然后客户可以继续做自己的事. 流水线上的工作发现有活做, 就结束休息, 开始加工工作. 在linu
x中, 流水线变成了worklist, 工人变成了worker_thread, 打成包的工件就是struct work_struct对象, 把包
仍到流水不线的工作就变成了queue_work函数的调用等等.
内核本身提供一套默认的工作队列, 但驱动也可以创建属于自己的工作队列.
6.2.1 数据结构
struct work_struct {
atomic_long_t data;//驱动可以利用data来将驱动使用的某些指针传递给延迟函数.
struct list_head entry;//双向链表, 用来将提交的工作节点形成链表.
work_func_t func;//工作节点的延迟函数.
}
驱动要通过工作队列实现延迟操作, 需要生成一个struct work_struct对象, 称之为工作结点, 然后通过que
ue_work函数将其提交给工作队列.
typedef void (*work_func_t)(struct work_struct *work);
struct cpu_workqueue_struct {
spinlock_t lock;
struct list_head worklist;
wait_queue_head_t more_work;
struct work_struct *current_work;
struct workqueue_struct *wq;
struct task_struct *thread;
}__cacheline_aligned;
struct cpu_workqueue_struct对象是个per-cou型的变量, 通过alloc_percpu动态创建, 每个cpu都有一份,
本书称cpu_workqueue_struct为cpu工作队列管理结构.
spinlock_t lock: 对象的自旋锁, 用于提供互斥保护.
struct list_head worklist: 双向链表, 将提交的工作节点形成链表.
wait_queue_head_t more_work: 等待队列头节点, 如果没有工作节点需要处理, 就要进入等待队列.
struct work_struct *current_work: 记录当前正在处理的工作结节.
struct workqueue_struct *wq: 指向系统工作队列管理结构.
struct task_struct *thread: 指向工人线程所在的进程空间结构. workqueue_struct.
struct workqueue_struct {
struct cpu_workqueue_struct *cpu_wq;
struct list_head list;
const char *name;
int singlethread;
int freezeable;
int rt;
}
相对于上面的cpu工作队列管理结构, struct workqueue_struct为工作队列管理结构, 内核会为每个工作队列
生成一个工作队列管理结构对象.
struct cpu_workqueue_struct *cpu_wq
指向cpu工作队列管理结构的per-cpu指针. 根据该指针, 系统中的每个cpu都可以通过per_cpu_ptr来获得属于
自己的CPU工作队列管理结构的对象.
struct list_head list:
双向链表对向, 将工作队列管理结构加入到一个全局变量workqueues中, 只对非singlethread队列有效.
const char *name: 工作队列的名称.
int singlethread: 标识创建的工作队列中工人线程的数量.
int freezeable: 表示进程可否处理冻结状态.
int rt: 用来调整worker_thread线程所在进程的调度策略.
6.2.2 create_singlethread_workqueue和create_workqueue
#define create_workqueue(name) \
alloc_workqueue((name), WQ_MEM_RECLAIM, 1)
#define create_freezable_workqueue(name) \
alloc_workqueue((name), WQ_FREEZABLE | WQ_UNBOUND | WQ_MEM_RECLAIM, 1)
#define create_singlethread_workqueue(name) \
alloc_workqueue((name), WQ_UNBOUND | WQ_MEM_RECLAIM, 1)
最终调用的函数是__create_wrokqueue_key, create_workqueue和create_singlethread_workqueue的区别在
于调用__create_workqueue_key时的第二个参数.
__create_workqueue_key一开始调用kzalloc生成一个工作队列管理结构的对象wq并初始化, 并利用alloc_p
ercpu生成一个per-cpu类型的cpu工作队列管理结构对象;
wq->cpu_wq = alloc_percpu(struct cpu_workqueue_struct);
接下来根据参数singlethread的值对单纯种和多线程队列分别进行处理。 对于create_singlethread_workqu
eue函数生成的工作队列是单线程的, singlethread=1,需要做的是:
1.调用init_cpu_workqueue函数, 其中获得系统中第一个cpu对应的cpu工作队列管理结构的指针cwq, 同时初
始化cwq中的等待队列和双向链表行装成员变量。
2. 调用create_workqueue_thread生成工人线程(work_thread). create_workqueue_thread函数实际上的操作
是生成一个新的进程, 将该进程的task_struct中保存有进行执行现场寄存器的pc值指向worker_thread函数
, 这样该进程被调度运行时将执行worker_thread函数, 传给函数的参数是系统中第一个cpu上的cwq指针。
新进程的task_struct结构体指针p将保存在cpu工作队列管理结构的thread成员中: cwq->thread=p.
3. 调用struct_workqueue_thread函数, 后者再通过wake_up_process函数将新进程投入到系统的运行队列中
:wake_up_process(p).如此之后新进程就具备了被调度的条件。
如果singlethread不为1,__create_wrokqueue_key将对系统中的每个cpu调用singlethread中的三大步骤,这
样每个cpu都将拥有自己的cpu工作队列管理结构和工作在其上的工人线程。这种情况下, 工作队列管理结构
对象wq还将把自己加入到workqueues管理的链表中:
list_add(&wq->list, &workqueues);
workqueues是一个全局型的双向链表对象, 用来链接系统中所有非singlethread的工作队列:
内核部分通过craete_singlethread_workqueue/create_workqueue创建工作队列及其上的工人纯种worker_th
read, 后者的任务是操作worklist链表上的工作节点 ,如果worklist上没有工作节点, 那么worker_thread
所在的进程将进入睡眠并驻留在more_work维护的等待队列中。 验动程序将要延迟的操作打包进struct work
_struct类型的工作节占中, 然后通过queue_work向worklist上提交该工作节点, 最后吃醋worker_thread.
6.2.3 工人线程worker_thread
worker_thread用来处理驱动提交到工作队列中的工作节点, 如果工作队列中没有节点需要处理, 它将睡眠
在cwq->more_work的等待队列中。 worker_thread运行在一个独立的新进程空间中。
worker_thread的主体是一for(;;)循环, 首先用kthread_should_stop检测有没有被调用kthread_stop, 如果
有的话, kthread对象的should_stop成员将被置1, 此时worker_thread将通过break跳出循环, 纯种函数所
在的进程将会终结。 如果需不需要stop且cwq->worklist上没有工作节点等待处理, 工作线程将调用schedul
e以TASK_INTERRUPTIBLE状态睡眠在等待队列cwq->more_work中, 直到驱动向cwq->worklist上提交一个新的
节点并唤醒worker_thread, 它醒来后调用run_workqueue来处cwq->worklist上的工作节点:
static void run_workqueue(struct cpu_workqueue_struct *cwq)
{
spin_lock_irq(&cwq->lock);
while(!list_empty(&cwq->worklist)) {
struct work_struct *work = list_entry(cwq->worklist.next, struct work_sturct , entry);
work_func_t f = work->func;
cwq->current_work = work;
list_del_init(cwq->worklist.next);
spin_unlock_irq(&cwq->lock);
work_clear_pending(work);
f(work);
spin_lock_irq(&cwq->lock);
cwq->current_work = NULL;
}
spin_unlock_irq(&cwq->lock);
}
在while中循环遍历cwq->worklist链表, 对于其中的每个节点work, 先将其从cwq->worklist链表删除,然后
调用工作节点上的延迟函数f(work), 传递给函数的参数是延迟函数所在工作节点的指针work. 一个工作节点
被处理完之后, 将不会再出现在工作队列的 cwq->worklist链表中。
work_clear_pending用来清除work->data的WORK_STRUCT_PENDING位, 内核把work->data的低2位用于记录wor
k的状态信息, 当驱动调用queue_work向工作队列提交节点work时, queue_work会把work->data的WORK_STRU
CT_PENDING位置1, 为了防止驱动将一个没有被处理的工作节点再次向cwq->worklist提交。
???????如是驱动多次提交尚未被处理的工作节点会返回什么呢? 失败吗?
6.2.4 destroy_workqueue
destroy_workqueue执行与create_singlethread_workqueue/create_workqueue相返的任务, 当驱动不再需要
使用工作队列时, 需要调用destroy_workqueue来做工作队列的清理工作。
除了将wq从workqueue中移走及释放工作队列管理结构等所占的内存外, 主要工作是调用cleanup_workqueue_
thread来完全终结worker_thread, 因为destroy_workqueue被调用时,worker_thread可能正在处理worklist
中余下的工作节点。
cleanup_workqueue_thread主要作用是通过调用kthread_stop函数让worker_thread所在进程终止, 因为一旦
进行执行worher_thread结束, 进程就将调用do_exit而终结, 所以kthread_stop让worker_thread结束的原
理就是设置should_stop=1.
终目worker_thread所在进程的前提条件是要确保所有提交到cwq->worklist中的工作节点都处理完毕, 这是
由flush_cpu_workqueue函数完成。
flush_cpu_workqueue确保cwq->worklist上所有工作节点都已处理完毕的设计思想是利用完成接口completion
: 如果cwq->worklist不为空或者cwq->current_work不为空, 说明cwq_worklist上还有工作节点或者正在处
理一个工作节点, 则向cwq->worklist上提交一个新工作节点, 当这个终止节点上的延迟函数被执行时,它
将调用complete通知flush_cpu_workqueue, 后者提交中止节点后将睡眠等待在wait_for_completion上, 直
到提交的中止节点上的延迟函数执行结束, 如此就可保证中止节点前的工作节点都会被处理完毕。
在提交了中止节点后, 其他部分依然可以向cwq->worklist提交新的工作节点, 但内核无法保证这些节点上
的延迟函数有机会执行。
flush_cpu_workqueue的操作范围只限于单cpu. 对于多cpu应使用flush_workqueue函数:
如果驱动想等待在某单个提交的工作节点上直到该节点处理完毕函数才返回, 就可以使用flush_work函数。
6.2.5 提交工作节点queue_work
???????相对于多cpu, 单个相同的工作结点, 可以同时工作在多个cpu上吗, 形成重入状态
int queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work)
{
int ret = 0;
if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
__queue_work(cpu, wq, work);
ret = 1;
}
return ret;
}
函数首先检测work->data的WORK_STRUCT_PENDING位有没有被置1,置1的话意味着此前该work已被提交还没有
处理, 内核禁止驱动在一个工作节点还没有处理完就再次提交该节点。
......此处告诉驱动,在构造工作节点对象work时, 应该确保work->data低2位为0.
如果work->data的WORk_STRUCT_PENDING位是0,就把它置为1, 然后调用__queue_work来提交节点
static void __queue_work(struct cpu_workqueue_struct *cwq, struct work_struct *work);
第1个参数是cpu工作队列管理结构, 第2个是待提交的节点指针。 queue_work_on在调用__queue_work时传递
的第1个参数是wq_per_cpu(wq,cpu).
wq_per_cpu很简单,如果是singlethread类型的工作队列,工作节点提交到第一个cpu的cwq上, 否则哪个cpu
调用queue_work,工作节点就提交到哪个cpu的cwq上。
static void insert_work(struct cpu_workqueue_struct *cwq,
struct work_struct *work, struct list_head *head,
unsigned int extra_flags)
{
struct global_cwq *gcwq = cwq->gcwq;
/* we own @work, set data and link */
set_work_cwq(work, cwq, extra_flags);
/*
* Ensure that we get the right work->data if we see the
* result of list_add() below, see try_to_grab_pending().
*/
smp_wmb();
list_add_tail(&work->entry, head);
/*
* Ensure either worker_sched_deactivated() sees the above
* list_add_tail() or we see zero nr_running to avoid workers
* lying around lazily while there are works to be processed.
*/
smp_mb();
if (__need_more_worker(gcwq))
wake_up_worker(gcwq);
}
insert_work完成的节点提交, 函数的工作是将工作节点加到cwq->worklist链表尾部, 然后调用wake_up唤
醒在等待队列cwq->more_work上睡眠的worker_thread, 如果worker_thread正在运行, wake_up什么也不做.
PREPARE_WORK / INIT_WORK用来初始化work_struct对象:
#define PREPARE_WORK(_work, _func) \
do { \
(_work)->func = (_func); \
} while (0)
#define PREPARE_DELAYED_WORK(_work, _func) \
PREPARE_WORK(&(_work)->work, (_func))
INIT_WORK初始化struct work_struct中的每个成员,PREPARE_WORK只是重设置func指针。
DECLARE——WORK用来静态定义struct work_struct并初始化。
#define DECLARE_WORK(n, f) \
struct work_struct n = __WORK_INITIALIZER(n, f)
queue_delayed_work用延时提交等待队列, 参数delay表示延迟的时间。 这种提交的工作在queue_delayd_
work_on中完成。函数利用定时哭timer来实现延迟提交, timer->expires=jiffies+delay, 当delay时间到期
后, timer->function = delayed_work_timer_fn将被调用, delayed_work_timer_fn会把要提交的节点提交
到工作队列中。 所以驱动使用时要先生成一个struct delayed_work对象。
struct delayed-work {
struct work_struct work;
struct timer_list timer;
}
6.2.6 内核创建的工作队列
linux在初始化阶段init_workqueues函数中通过create_workqueue创建了一个名为events的工作队列:
static struct workqueue_struct *keventd_wq__read_mostly;
void __init init_workqueues(void)
{
...
keventd_wq = create_workqueue("events");
...
}
就算驱动没有自己创建工作队列, 也可利用内核创建的工作队列来袜现延迟操作, 提交工作节点时只需要调
用queue_work(keventd_wq, work)即可, 内核提供另一个函数包装了queue_work(kevent-wq, work)
int schedule_work(struct work_struct *work)
{
return queue_work(keventd_wq, work);
}
对应queue_delayed_work而言, 延迟提交函数变成了schedule_dealyed_work.
使用内核提供的工作队列好处是无处自己创建工作队列就可以提交节点, 不好的地方就是我们正与系统中的
其它模块共享一个工作队列及该队列上的worker_thread, 调度时间可能会过长。
6.3 本章小结
本文欢迎转载
本文出处:http://blog.csdn.net/dyron
tasklet的实现苦于softirq, 内核在初始化期间就初始化了HI_SOFTIRQ和TASKLET_SOFTIRQ两个softirq所对
应的action函数: tasklet_hi_action和tasklet_action.
驱动在可以使用tasklet机制前, 需要定义一个tasklet对象, 将要延迟的函数封装到对象中, 然后调用ta
sklet_schedule函数向系统提交该对象。 中断处理的SOFTIRQ发现有等待的softirq需要处理时, 就去处理被
提交的tasklet对象, 这时打包进tasklet对象的实际延迟函数将被调用。 当一个tasklet对象在SOFTIRQ部分
处理完后, 除非再次提交, 否则不再会被执行。 tasklst实现的延迟操作运行在中断上下文中, 所以不该
有睡眠。
tasklet是严格串行化的, 在任一时刻,同一tasklet只能有一个实例在运行, 多处理哭系统也是如此。另一
个重要特性是, 哪个CPU提交的tasklet,只能在该CPU上运行。
相对于tasklet, 工作队列运行在独立的进程环境下。 系统中可能有两种形式的工作队列:singlethread和非
singlethread. 对于多处理器, 前者只在第一个cpu上产生工作队列和工人线程, 后者则为系统的每个处理
器都产生一个工作队列和工人纯种。 可以使用内核创建的一个非singlethread工作队列, 也可创建自己的工
作队列。
为了实现延迟操作, 驱动需要生成一个类型为struct work_struct 的工作对象, 将要延迟执行的函数打包
到该对象中, 然后通过queue_work向工作队列提交该节点。 同tasklet一样, 当work对象被处理完后除非再
次被提交, 否则将不再有执行的机会。 与tasklet不同的是, 苦于工作队列的延迟操作是运行在进程的上下
文中, 允许睡眠。