工作队列work queue解析

2.6内核的工作队列做了很大的修改,新的工作队列机制被称为Concurrency Managed Work Queue(并发管理工作队列,简称cmwq)。其主旨是增强多核时的处理性能。

cmwq 的实现遵循了以下几个原则:

  • 与原有的工作队列接口保持兼容,cmwq 只是更改了创建工作队列的接口,很容易移植到新的接口(__create_workqueue -> alloc_workqueue)。
  • 工作队列共享 per-CPU 的线程池,提供灵活的并发级别而不再浪费大量的资源。
  • 自动平衡工作者线程池和并发级别,这样工作队列的用户不再需要关注如此多的细节。
create_workqueue (create_singlethread_workqueue) -> alloc_workqueue -> __alloc_workqueue_key
struct workqueue_struct*alloc_workqueue(char *name, unsigned int flags, int max_active);

其中:

name为工作队列的名字,而不像 2.6.36 之前实际是为工作队列服务的内核线程的名字。

flag 指明工作队列的属性,可以设定的标记如下:

  • WQ_NON_REENTRANT:默认情况下,工作队列只是确保在同一 CPU 上不可重入,即工作项不能在同一 CPU 上被多个工作者线程并发执行,但容许在多个 CPU 上并发执行。但该标志标明在多个 CPU 上也是不可重入的,工作项将在一个不可重入工作队列中排队,并确保至多在一个系统范围内的工作者线程被执行。
  • WQ_UNBOUND:工作项被放入一个由特定 gcwq 服务的未限定工作队列,该客户工作者线程没有被限定到特定的 CPU,这样,未限定工作者队列就像简单的执行上下文一般,没有并发管理。未限定的 gcwq 试图尽可能快的执行工作项。
  • WQ_FREEZEABLE:可冻结 wq 参与系统的暂停操作。该工作队列的工作项将被暂停,除非被唤醒,否者没有新的工作项被执行。
  • WQ_MEM_RECLAIM:所有的工作队列可能在内存回收路径上被使用。使用该标志则保证至少有一个执行上下文而不管在任何内存压力之下。
  • WQ_HIGHPRI:高优先级的工作项将被排练在队列头上,并且执行时不考虑并发级别;换句话说,只要资源可用,高优先级的工作项将尽可能快的执行。高优先工作项之间依据提交的顺序被执行。
  • WQ_CPU_INTENSIVE:CPU 密集的工作项对并发级别并无贡献,换句话说,可运行的 CPU 密集型工作项将不阻止其它工作项。这对于限定得工作项非常有用,因为它期望更多的 CPU 时钟周期,所以将它们的执行调度交给系统调度器。

max_active决定了一个 wq 在 per-CPU 上能执行的最大工作项。比如 max_active 设置为 16 表示一个工作队列上最多 16 个工作项能同时在 per-CPU 上同时执行。当前实行中,对所有限定工作队列,max_active 的最大值是 512,而设定为 0 时表示是 256;而对于未限定工作队列,该最大值为:MAX[512,4 * num_possible_cpus() ],除非有特别的理由需要限流或者其它原因,一般设定为 0 就可以了。

在之前的代码中,一些用户依赖于 ST 中的严格执行顺序,这种行为在 cmwq 中可以将 max_active 设为 1,flag 设置为 WQ_UNBOUND 来获得相同的行为

cmwq 本质上是提供了一个公共的内核线程池的实现,其接口基本上和以前保持了兼容,只是更改了创建工作队列的函数的后端,它实际上是将工作队列和内核线程的一一绑定关系改为由内核来管理内核线程的创建,因此在 cmwq 中创建工作队列并不意味着一定会创建内核线程。



内核实现工作队列的基本框架如下图所示:
1. gcwq封装了线程池,worker封装了工作者线程,work就是工作者线程真正在处理的事务。
2. 针对每一个CPU,内核用一个数据结构global_cwq来表示cmwq,即gcwq,还有一个未和CPU绑定的gcwq,所以总共gcwq的数量为CPU数目+1.
3. gcwq中有三个重要的list,分别是: 1) 管理空闲worker的idlelist; 2)管理正在工作的worker的busy_hash; 3) 以及本CPU上等待处理的work列表worklist
4. worker才是Linux调度器进行调度的基本单位,worker有一个成员task_struct,调度器利用此结构来调度worker_thread,决定他何时休眠,合适唤醒

工作队列的工作原理
workqueue有两种形式:
1. ST (Single Thread):只在创建它的CPU上有cwq与之对应
2. MT (Multi Thread):在每个active CPU上均有cwq与之对应,对应到每个CPU上,有一个cpu_workqueue_struct与之对应,即cwq

wq与cwq的对应关系是在__alloc_workqueue_key中实现的:
1. 通过alloc_cwqs创建一组per-CPU变量
2. 用一个for_each_cwq_cpu循环遍历所有CPU,并为每个CPU的cwq结构初始化,指定cwq->gcwq和cwq->wq

work的处理流程如下图所示:


work在各个list之间的转换的关系如下图所示:


work如何处理: process_scheduled_work()
1. 在busy_hash table中找,是否有thread正在处理当前事务。如果有,则将本事务转移到那个worker thread的scheduled列表上,以保证同一个work不在同一CPU的不同thread上并发。
2. 配置worker->current_work = work, worker->current_cwq = cwq, 记录此时在处理的是哪个workqueue上的哪个work。由于此时gcwq->lock是锁上的,所以和其他本地thread同步没有问题
3. 记录CPU number到work->data,从scheduled列表中删除该work
4. 处理gcwq有GCWQ_HIGHPRI_PENDING标志的情况,加入下一个work仍然是高优先级,则去叫醒idle的worker来处理,如果没有则继续。保证在有高优先级pending的时候,尽量多的worker参与处理。
5. 解锁gcwq->lock
6. 为work清除pending标志,开始真正的处理该work,调用work->func
7. 锁住gcwq->lock
8. 做一些完成时的更新:1) 从busy_hash中删除该worker; 2) 将current_work和current_cwq赋为NULL

queue_work如果不指定cpu id的话,会默认调用get_cpu()获得当前thread所在的CPU号(也就是queue_work所在的CPU,或者说提交work所在的CPU),并将work加到CPU对应的gcwq的worklist上,以待进一步处理。

工作者线程如何调度
所有的线程通过gcwq->lock做同步,当在拷贝列表,以及前期处理时(修改标志位,manage worker),必须要获得锁,但真正处理时(可能是最耗时的操作),锁是释放状态的。当线程处理完以后,线程标志位设为TASK_INTERRUPTIBLE,并调用schedule(),以交出控制权,等待调度器将其唤醒,唤醒后,立即进入woke_up分支,并首先获得gcwq->lock锁。