文章为本人学习笔记和总结,如有错误,请多多指教;
引言:
linux实现中断底半部的机制主要有tasklet、工作队列、软中断和线程化;
本文主要介绍下工作队列---workqueue
1、工作队列(workqueue)简介
Linux中的Workqueue机制就是为了简化内核线程的创建。通过调用workqueue的接口就能创建内核线程。并且可以根据当前系统CPU的个数创建线程的数量,使得线程处理的事务能够并行化。workqueue是内核中实现简单而有效的机制,他显然简化了内核daemon的创建,方便了用户的编程.
工作队列(workqueue)是另外一种将工作推后执行的形式.工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。最重要的就是工作队列允许被重新调度甚至是睡眠;
由于每一个工作队列都有一个或者多个专用的进程(内核线程),这些进程运行提交到该队列的函数;
从表面看工作队列(workqueue)类似于tasklet,它们都允许内核代码请求某个函数在将来的时间被调用。但两者有很大差别:
- tasklet在软中断上下文中运行,因此所有的tasklet代码必须是原子的;而工作队列函数是在特殊的内核进程上下文中运行,因此具有更好的灵活性,如工作队列可以调用和休眠
- tasklet始终运行在被初始提交的同一CPU上,但这只是工作队列的默认方式
- 内核代码可以请求延时给定的时间间隔后执行工作队列函数
两者关键区别:tasklet会在很短时间段内执行,并且是原子模式的,而工作队列函数具有更长延时并且不必原子化;
2、工作队列(workqueue)使用
- 工作(work)数据结构:
struct work_struct {
atomic_long_t data; /*工作处理函数func的参数*/
#define WORK_STRUCT_PENDING 0 /* T if work item pending execution */
#define WORK_STRUCT_STATIC 1 /* static initializer (debugobjects) */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
struct list_head entry; /*连接工作的指针*/
work_func_t func; /*工作处理函数*/
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
//工作队列执行函数的原型:
void (*work_func_t)(struct work_struct *work);
//该函数会由一个工作者线程执行,因此其在进程上下文中,可以睡眠也可以中断。但只能在内核中运行,无法访问用户空间。
- 工作将以队列形式组织成工作队列(workqueue),其数据结构如下:
struct workqueue_struct {
struct cpu_workqueue_struct *cpu_wq;
struct list_head list;
const char *name; /*workqueue name*/
int singlethread; /*是不是单线程 - 单线程我们首选第一个CPU -0表示采用默认的工作者线程event*/
int freezeable; /* Freeze threads during suspend */
int rt;
};
- 多CPU多线程工作队列cpu_workqueue_struct
truct cpu_workqueue_struct {
spinlock_t lock;/*因为工作者线程需要频繁的处理连接到其上的工作,所以需要枷锁保护*/
struct list_head worklist;
wait_queue_head_t more_work;
struct work_struct *current_work; /*当前的work*/
struct workqueue_struct *wq; /*所属的workqueue*/
struct task_struct *thread; /*任务的上下文*/
} ____cacheline_aligned;
综上所述:工作有struct work_struct的类型,工作队列有struct workqueue_struct的类型,该结构定义在<linux/workqueue.h>中。在使用前,我们必须显示的创建一个工作队列,介绍如下:
- 创建工作队列workqueue
在使用工作队列之前,首先我们需要创建工作队列,可以使用下面两个函数之一:
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
两个函数的区别:
使用create_workqueue()函数,内核则会在系统中的每一个CPU上为该工作队列创建专用的线程;这种情况下,可能会对系统性能造成一定影响。
而如果单个线程能够满足要求,则建议使用create_singlethread_workqueue()函数创建工作队列。
- 提交工作work
1)创建好工作队列后,那么如何提交工作到一个工作队列,首先需要填充work_struct结构体,可以有如下方法:
DECLARE_WORK(name, void (*func)(void *), void *data)
INIT_WORK(struct work_struct *work, void (*func)(void *), void *data)
PREPARE_WORK(struct work_struct *work, void (*func)(void *), void *data)
区别如下:
DECLARE_WORK()是一个宏定义,并且该宏定义是在编译时完成的,即静态创建方法,其中name为结构名称,func是工作队列要调度的函数,data是传递给该函数的参数;
如果要在运行时构造work_struct结构,应该使用后两个宏。INIT_WORK完成更加彻底的初始化工作,因此在首次使用该结构时,建议使用这个宏。而PREPARE_WORK完成几乎相同的工作,但是它不会初始化链接work_struct结构的工作队列的指针;因此如果工作work_struct已经被提交到工作队列中,而只是需要修改该结构,则应该使用PREPARE_WORK。
2)然后要将工作提交到工作队列,则使用下面两个函数之一:
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work,
unsigned long delat);
它们都会将work添加到指定的workqueue。但是如果使用queue_delayed_work,则实际的工作至少会在经过指定的jiffies(由delay指定)之后才会被执行;如果工作被成功添加到队列,则上述函数返回值为1,。返回值为非零时,则意味着给定的work_struct结构已经等待在该队列中,从而不能添加两次。
3、工作队列调度schedule
使用如下函数调度工作队列中的工作,使用如下函数之一:
int schedule_work(struct work_struct *work);
int schedule_delayed_work(struct work_struct *work, unsigned long delay);
两者区别:如果要马上调度运行work,则使用schedule_work()函数,即一旦其所在的处理器上的工作者线程被唤醒,它就会被执行;有时候并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行,则使用schedule_delayed_work()函数。
工作队列是没有优先级的,基本按照FIFO的方式进行处理;
4、工作队列取消
如果要取消某个挂起的工作队列入口项,可调用:
int cancel_delayed_work(struct work_struct *work);
void flush_workqueue(struct workqueue_struct *queue);
在结束对工作队列的使用后,可调用下面函数释放相关资源:
void destroy_workqueue(struct workqueue_struct *queue);