转载自https://zhuanlan.zhihu.com/p/91106844, 作者兰新宇
1. workqueue
Linux 中的 workqueue 机制是中断底半部的一种实现,同时因为其支持用一个线程处理多个小任务,有利于降低资源开销,因此也成为一种通用的任务异步处理的手段。
进入 workqueue 队列处理的任务(work item)在代码中由 "work_struct " 结构体表示:
struct work_struct {
struct list_head entry;
work_func_t func;
atomic_long_t data;
};
其中,"entry" 表示其所挂载的队列节点,"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" 的。
为了充分利用 locality,通常选择将处理 hardirq 的 CPU 作为该 hardirq 对应的 workqueue 底半部的执行CPU,在早期 Linux 的实现中,每个 CPU 对应一个 workqueue 队列,并且每个 CPU 上只有一个 worker 线程来处理这个 workqueue 队列,也就是说 workqueue 队列和 worker 线程都是 per-CPU 的,且一一对应。
让我们看看这种设计存在什么问题。
假设现在一个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执行完毕将总共需要50ms的时间。
假设现在一个CPU上有2个worker线程,分别为worker 1和worker 2,那么整个执行时间将缩短到35ms:
如果一个CPU上有3个worker线程,执行时间将进一步缩短到25ms:
2. cmwq
这种在一个CPU上运行多个worker线程的做法,就是 v2.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" 结构体表示:
struct worker_pool {
int cpu; /* the associated cpu */
int id; /* pool ID */
struct list_head worklist; /* list of pending works */
struct list_head idle_list; /* list of idle workers */
DECLARE_HASHTABLE(busy_hash, 6); /* hash of busy workers */
...
}
如果一个worker正在处理work item,那么它就是busy的状态,将挂载在busy workers组成的6阶的hash表上。既然是hash表,那么就需要key,充当这个key的是正在被处理的work item的内存地址。
如果一个worker没有处理work item,那么它就是idle的状态,将挂载在idle workers组成的链表上。
前面说过,有未处理的work item,内核就会启动一个新的worker线程,以提高效率。有创建就有消亡,当现在空闲的worker线程过多的时候,就需要销毁一部分worker线程,以节省CPU资源。就像一家公司,在项目紧张,人员不足的时候需要招人,在项目不足,人员过剩的时候可能就会裁员。
这种动态的资源伸缩,就有点“云”的味道了,即根据负载的变化,动态地扩容和缩容,目标是用尽可能少的资源,来完成尽可能多的事。至于保留多少空闲线程可以取得较理想的平衡,则涉及到一个颇为复杂的算法,在此就不展开了。
worker 线程在代码中由"worker" 结构体表示:
struct worker {
struct worker_pool *pool; /* the associated pool */
union {
struct list_head entry; /* while idle */
struct hlist_node hentry; /* while busy */
};
struct work_struct *current_work; /* work being processed */
work_func_t current_func; /* current_work's fn */
struct task_struct *task; /* worker task */
struct pool_workqueue *current_pwq; /* current_work's pwq */
...
}
其中,"pool"是这个worker线程所在的worker pool,根据worker线程所处的状态,它要么在idle worker组成的空闲链表中,要么在busy worker组成的hash表中。
"current_work"和"current_func"分别是worker线程正在处理的work item和其对应的入口函数。既然worker线程是一个内核线程,那么不管它是idle,还是busy的,都会对应一个task_struct(由"task"表示)。
"current_pwq"指向被服务的work item对应的workqueue队列,关于workqueue队列的介绍,以及它与worker pool之间的交互,将在下文讲解。
评论 (0)