4. 工作队列
工作队列(work queue)可以把工作推后,交由一个内核线程去执行,这个下半部总是会在进程上下文中执行。最重要的是工作队列允许重新调度甚至是睡眠。
如果你需要一个可以重新调度的实体来执行你的下半部处理,你应该使用工作队列。如果推后执行的任务需要睡眠,那么就选择工作队列。
4.1. 工作队列的实现
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程被称为工作者线程(worker thread)。工作队列可以让你的驱动创建一个专门的工作者线程来处理需要推后的工作。也可以使用子系统提供的一个默认的工作者线程来处理。
默认的工作者线程叫做events/n,n是处理器的编号。每个处理器对应一个线程。
4.1.1. 表示线程的数据结构
工作者线程用workqueue_struct结构表示:
struct workqueue_struct{
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
const char *name;
struct list_head list;
};
该结构是由一个cpu_workqueue_struct结构组成的数组,定义在kernel/workqueue.c中,数组的每一项对应系统中的一个处理器。cpu_workqueue_struct结构是kernel/workqueue.c中的核心数据结构:
struct cpu_workqueue_struct{
spinlock_t lock;
long remove_sequence; /*最近一个被加上的(下一个要运行的)*/
long insert_sequence; /*下一个要加上的*/
struct list_head worklist; /*工作列表*/
wait_queue_head_t more_work;
wait_queue_head_t work_done;
struct workqueue_struct *wq; /*有关联的workqueue_struct */
task_t *thread;
int run_depth; /*run_workqueue循环深度*/
};
注意:每个工作者线程类型关联一个自己的workqueue_struct。
4.1.2. 表示工作的数据结构
所有的工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread函数。在它初始化以后,这个函数执行一个死循环并开始休眠。当有操作被插入到队列里时,线程就会被唤醒,以便执行这些操作。当没有剩余的操作时,它又会休眠。
工作用<linux/workqueue.h>中定义的work_struct结构体表示。
struct work_struct{
unsigned long pending; /*这个工作正在等待处理吗?*/
struct list_head entry; /*连接所有工作的链表*/
void (*func)(void*); /*处理函数*/
void *data; /*传递处理函数的参数*/
void *wq_data; /*内部使用*/
struct timer_list timer; /*延迟的工作队列所用到的定时器*/
};
当工作者线程被唤醒,它会执行它的链表上的所有工作。工作被执行完毕,他就将相应的work_struct对象从链表上移去。
worker_thread函数的核心如下:
for(;;){
set_task_state(current, TASK_INTERRUPTIBLE);
add_wait_queue(&cwq->worklist);
if(list_empty(&cwq->worklist))
schedule();
else
set_task_state(current, TASK_RUNNING);
remove_wait_queue(&cwq->worklist &wait);
if(!list_empty(&cwq->worklist))
run_workqueue(cwq);
}
该函数在死循环中完成以下功能:
1) 线程将自己设置为休眠状态(TASK_INTERRUPTIBLE)并把自己加入到等待队列上
2) 如果工作链表是空的,线程调用schedule函数进入休眠
3) 如果链表中有对象,线程不会睡眠,它将自己设置为TASK_RUNNING,脱离等待队列。
4) 如果链表非空,调用run_workqueue函数执行被推后的工作。
4.1.3. run_workqueue函数
run_workqueue函数的核心实现如下:
while(!list_enpty(&cwq->worklist)){
struct work_struct *work;
void (*f)(void *);
void *data;
work=list_entry(cwq->worklist.next, struct work_struct, entry );
f = work->func;
data = work->data;
list_del_init(cwq->worklist.next);
crear_bit(0, &work->pending);
f(data);
};
该函数循环遍历链表上每个待处理的工作,执行链表每个节点上work_struct中的func函数:
1) 当链表不为空时,选取下一个节点对象
2) 获取希望执行的函数func及其参数
3) 把该节点从链表上解下来,将带处理标志位pending清0
4) 调用函数
5) 重复执行
4.2. 工作队列的使用
4.2.1. 创建推后的工作
首先要创建一些需要推后完成的工作。编译时创建:
DECLARE_WORK(name, void (*func)(void*), void *data);
运行时创建:
INIT_WORK(struct work_struct *work, void (*func)(void*), void *data);
该函数动态初始化一个由work指向的工作。
4.2.2. 工作队列处理函数
工作队列处理函数原型:
void work_handler(void *data);
这函数会由一个工作者线程执行,因此函数会运行在进程上下文中。默认下,允许响应汇总的,并且不持有任何锁。如果需要,可以睡眠。注意:尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。
4.2.3. 对工作进行调度
工作已经创建后,就能调度它。想要把给定的工作处理函数提交给默认的events工作现场,只需要调用:
schedule_work(&work);
如果需要工作在指定的时间执行:
schedule_delayed_work(&work, delay);
4.2.4. 刷新操作
排入队列的工作会在工作者线程下一次被唤醒的时候执行。内核准备了一个用于刷新指定工作队列的函数:
void flush_scheduled_work(void);
该函数会一直等待,直到队列中所有对象都被执行以后才返回。在等待所有待处理的工作执行时,该函数会进入睡眠,所以只能在进程上下文中使用,但不取消任何延迟执行的工作。就是说,任何通过schedule_delayed_work调度的工作,如果其延迟时间未结束,它并不会因为调用flush_scheduled_work而被刷新掉。 取消延迟执行的工作应该调用:
int cancel_delayed_work(struct work_struct *work);
该函数可以取消任何与work_struct相关的挂起工作。
4.2.5. 创建自己的工作队列
创建一个新的任务队列:
struct workqueue_struct *create_workqueu(const char *name);
name参数用于该内核线程的命名。例如,默认的events队列创建调用的是:
struct workqueue_struct *keventd_wq;
keventd_wq = create_workqueu(“events”);
该函数会创建所有的工作者线程(系统中每个处理器都有一个)并且做好所有开始处理工作之前的准备工作。
创建之后,可以调用如下函数创建工作:
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);
最后可以调用下面函数刷新指定的工作队列:
void flush_ workqueue(struct workqueue_struct*wq);
4.3. 老的任务机制
任务队列接口的使用者在2.5开发版本分为两个部分。其中一部分转向了使用tasklet。还有一部分已经演化成了工作队列接口。
任务队列机制通过定义一组队列来实现其功能。每个队列都有自己的名字,比如调度队列、立即队列和定时器队列。缺点是接口是一团乱麻,这些队列基本上都是随意创建的抽象概念,散落在内核各处。
5. 下半部机制的选择
在当前的2.6内核内核中,有三种可能的选择:软中断、tasklet和工作队列。tasklet是基于软中断实现,而工作队列机制靠内核线程实现。
软中断提供的执行序列化的保障最少,适用于对时间要求严格和执行频率很高的应用。驱动程序开发者应该尽可能选择tasklet而不是软中断,因为两个同种类型的tasklet不能同时执行。如果需要把任务推后到进程上下文中完成,就只能选择工作队列。
6. 在下半部之间加锁
使用tasklet的一个好处在于它自己负责执行的序列化保障:两个相同类型的tasklet不允许同时执行,即使在不同的处理器也不行。tasklet之间的同步(就是两个不同类型的tasklet共享数据时)需要正确的锁。
因为软中断根本不保障执行序列化,所以所有的共享数据都需要合适的锁。
如果进程上下文和一个下半部共享数据,需要禁止下半部的处理并得到的使用权。
如果中断上下文和一个下半部共享数据,需要禁止中断并得到锁的使用权。
任何在工作队列中被共享的数据也需要使用锁机制。
6.1. 禁止下半部
为了保证共享数据的安全,更常见的做法是先得到一个锁然后再禁止下半部的处理。如果需要禁止所有的下半部处理(就是本地所有的软中断和所有的tasklet),可以调用local_bh_disable函数。允许下半部进行处理,可以调用local_bh_enable函数。
这些函数有可能被嵌套使用。函数通过preempt_count为每个进程维护一个计数器。当计数器为0时,下半部才能够被处理。
但是,这些函数并不能禁止工作队列的执行。因为工作队列是在进程上下文中运行,不会涉及异步执行问题,所以没有必要禁止它们执行。由于软中断和tasklet是异步发生的,所以内核代码必须禁止它们。