![d2153604b8bb1d6c3e511a0d00e979c8.png](https://i-blog.csdnimg.cn/blog_migrate/0dd44f72d626e2f5e2f201e3821a4ff9.png)
Linux中的workqueue机制是中断底半部的一种实现,同时也是一种通用的任务异步处理的手段。进入workqueue队列处理的任务(work item)在代码中由"work_struct "结构体表示(定义在include/linux/workqueue.h):
struct
其中,"entry"表示其所挂载的workqueue队列的节点,"func"就是要执行的任务的入口函数。而"data"表示的意义就比较丰富了。最后的4个bits是作为"flags"标志位使用的,中间的4个bits是用于flush功能的"color"。flush功能简单地说就是:等待workqueue队列上的任务都处理完,并清空workqueue队列(由于笔者也没有深入研究过这一块的具体实现原理,在本文的叙述中就不涉及这一部分内容了)。
剩下的bits在不同的场景下有不同的含义(相当于C语言里的"union"),它可以指向work item所在的workqueue队列的地址,由于低8位被挪作他用,因此要求workqueue队列的地址是按照256字节对齐的。它还可以表示处理work item的worker线程所在的pool的ID(关于pool将在本文的后半部分介绍)。
这种在一个C语言变量里塞入不同的类型的数据的方法在Linux的代码实现中还是不难见到的,在目前的workqueue机制中,"flags"和"color"所需的bits都较少,单独使用整形变量去表示确实会增加一定的内存消耗。但这种牺牲可读性的做法也被一些内核开发者认为是比较"ugly"的。
![3fc79da69ca4eab8d233eb42a0caa600.png](https://i-blog.csdnimg.cn/blog_migrate/1e0a311548bb20d11b7a70fe40737f05.png)
为了充分利用locality,通常选择将处理hardirq的CPU作为该hardirq对应的workqueue底半部的执行CPU,在早期Linux的实现中,每个CPU对应一个workqueue队列,并且每个CPU上只有一个worker线程来处理这个workqueue队列,也就是说workqueue队列和worker线程都是per-CPU的,且一一对应。
![3281149895e4c09301ba9f9ce8c6457a.png](https://i-blog.csdnimg.cn/blog_migrate/d0fed4a1aef49d20bec861cdac536275.png)
让我们看看这种设计存在什么问题。假设现在一个work item(设为w0)被添加到了workqueue队列上。w0需要运行5ms后休眠10ms,接着再运行5ms。在w0开始运行5ms和10ms后,另外两个work items(设为w1和w2)也分别加入了workqueue队列,w1和w2都是需要运行5ms,然后再休眠10ms(该示例来自内核Documentation/core-api/workqueue.rst文档)。
因为只有1个worker线程,所以即便在执行某个work item的时候休眠,其他的work item也得不到执行,因此将这3个work item执行完毕将总共需要55ms的时间。
![7c5355e11d9934a225a9d0aa8120ee8a.png](https://i-blog.csdnimg.cn/blog_migrate/082bcc99515b45093c8d1666a1fe7ff9.png)
假设现在一个CPU上有2个worker线程,分别为worker 1和worker 2,那么整个执行时间将缩短到35ms:
![58fef762156e6f63d615eb245f1930ea.png](https://i-blog.csdnimg.cn/blog_migrate/3462dfa7710fae6c94c2b436dbf1e915.png)
如果一个CPU上有3个worker线程,执行时间将进一步缩短到25ms:
![6c7f8ef97dd57ba7af9b9410b1580dd1.png](https://i-blog.csdnimg.cn/blog_migrate/bf9bb9d2b652eadff66840deb198d21e.png)
cmwq
这种在一个CPU上运行多个worker线程的做法,就是2.6.36版本引入的,也是现在Linux内核所采用的concurrency managed workqueue,简称cmwq。一个CPU上是不可能“同时”运行多个线程的,所以这里的名称是concurrency(并发),而不是parallelism(并行)。
显然,设置合适的worker线程数目是很关键的,多了浪费资源,少了又不能充分利用CPU。大体的原则就是:如果现在一个CPU上的所有worker线程都进入了睡眠状态,但workqueue队列上还有未处理的work item,那么就再启动一个worker线程。
一个CPU上的所有worker线程共同构成了一个worker pool(此概念由内核v3.8引入),我们可能比较熟悉memory pool,当需要内存时,就从空余的memory pool中去获取,同样地,当workqueue上有work item待处理时,我们就从worker pool里挑选一个空闲的worker线程来服务这个work item。
worker pool在代码中由"worker_pool "结构体表示(定义在kernel/workqueue.c):
struct
如果一个worker正在处理work item,那么它就是busy的状态,将挂载在busy workers组成的6阶的hash表上。既然是hash表,那么就需要key,充当这个key的是正在被处理的work item的内存地址。
如果一个worker没有处理work item,那么它就是idle的状态,将挂载在idle workers组成的链表上。因为空闲的worker线程数目较少,用链表管理就可以了,而busy的worker线程可能较多,所以用hash表来组织,以加快查找的速度。
前面说过,有未处理的work item,内核就会启动一个新的worker线程,以提高效率。有创建就有消亡,当现在空闲的worker线程过多的时候,就需要销毁一部分worker线程,以节省CPU资源。就像一家公司,在项目紧张,人员不足的时候需要招人,在项目不足,人员过剩的时候可能就会裁员。至于保留多少空闲线程可以取得较理想的平衡,则涉及到一个颇为复杂的算法,在此就不展开了。
![4ada6555f1758b4381c4e2a214bbca83.png](https://i-blog.csdnimg.cn/blog_migrate/cd6e7434480689c87efb9bc8385d6dfd.png)
worker线程在代码中由"worker "结构体表示(定义在kernel/workqueue_internal.h):
struct
其中,"pool"是这个worker线程所在的worker pool,根据worker线程所处的状态,它要么在idle worker组成的空闲链表中,要么在busy worker组成的hash表中。
"current_work"和"current_func"分别是worker线程正在处理的work item和其对应的入口函数。既然worker线程是一个内核线程,那么不管它是idle,还是busy的,都会对应一个task_struct(由"task"表示)。
![c5ff57a1b073c70b04d1320cb81ae269.png](https://i-blog.csdnimg.cn/blog_migrate/50ea642d5813c2ad507a5aa96f04e501.png)
"current_pwq"指向被服务的work item所在的workqueue队列,关于workqueue队列的介绍,以及它与worker pool之间的交互,将在下文讲解。
参考:
Interrupts -7- (Workqueue 1)
Interrupts -8- (Workqueue 2)
原创文章,转载请注明出处。