linux内核工作队列的第三篇,上一篇keventd/events的演变讲到linux2.6时代的workqueue的雏形,linux-2.6早期的工作队列以一个workqueue+多个worker_thread的方式呈现:
这解决了众多work项到来导致单个work_thread压力过大问题,但这个设计又带来了新问题。
linux2.6 events存在两个严重的问题:
(1)每使用create_workqueue接口创建一个新的工作队列会一并创建N个后台线程(N=系统cpu的个数)。这导致在一些大型机上,内核刚启动就把PID给耗尽了,而就算手动将PID数量调大,解决了启动,但生成了大量后台线程消耗了很多内存资源。
(2)虽然创建了很多的后台进程,工作项的并发性又如何?其实并没有解决work并发执行。
举个例子:
假设进程A向系统投递了两个工作项,workA和workB,其中workB是带睡眠的,随后进程B也向系统工作队列投递了workC和workD,由于workB是阻塞的,那么workC和workD必须要等workB执行完成。导致workC的执行被阻塞,而更严重的是:如果后面又有新的work排队进来,都被会阻塞。
CMWQ时代:
直到linux-2.6.35,内核大神Tejun Heo对工作队列代码进行了重构。Tejun Heo实现了基于并发管理的workqueue,也就是Concurrency Managed Workqueue,CMWQ时代终于到来了,这也正是本篇要讲的重点。
即使是目前5.x内核,同样也是基于CMWQ实现的,掌握了CMWQ即掌握了workqueue的精髓。
那么cmwq是如何降低系统资源,并解决并发问题的呢?
CMWQ考虑了以下几个目标:
(1)兼容原有的workqueue API接口。
(2)实现每个cpu的统一线程池,线程池被所有wq共享,提供灵活的并发程度,不浪费大量的资源。
(3)自动管理线程池的伸缩,具备高并发处理能力,而且用户不需要关心细节。
老衲为了搞清workqueue的所有实现细节,基本上把整个CMWQ的代码都撸了一遍。这里将工作队列的核心设计总结出来,知道了设计思想,在去看代码也就不难了。
设计思想:
仍然保留work_struct,用于对work项的抽象。
设计一个新的work_pool线程池,为每个cpu上创建线程池,线程被称为工人“worker”,也就是work_thread,他们的任务是一个接一个的处理任务。没有任务处理时,work进入空闲状态。
work_pool分为两类,一个处理正常任务,一个处理高优先级任务。线程池中的worker数量是动态变化的。
驱动或内核模块,可以创建自己的工作队列,工作队列配备了flag标记。不同的flag标记,会产生不同属性的worker_pool,也有可能影响work项的排队机制,这些标记包括CPU局部性、并发限制、优先级等待。
排队一个work项时,根据wq的flags及work_pool的属性,来决定work具体在哪个CPU、哪个worker中运行。
并发处理是关键的设计点,CMWQ试图保持最小的资源消耗,但要保证足够的并发性。CMWQ使用最小的资源来发挥它的全部能力。
work_pool中有工人唤醒或者睡眠时,会与调度器交互,并统计空闲worker数量。
work_pool中如果有一个或多个工人正在CPU上运行着,这时有新的任务(non-block)到来,work_pool会保持work_thread持续工作状态,尽量不唤醒新的worker,但是如果最后一个工人睡眠时,它会立即安排一个新的工人起来服务,这允许使用最小数量的工人而不损失带宽。
如果一个带阻塞的work到来,work_pool会立即创建新的工人,以处理后续的work项,实现并发。
CMWQ也提供了特定的flag,可以解除并发级别的限制。
当worker处理idle一段时间后,work_pool会将多余的idle worker销毁,节约资源。
worker的动态创建,在内存紧张时可能会创建失败,则导致work项无法运行,用户可以指定特殊的标记(WQ_MEM_RECLAIM)创建救援线程,表明队列要用于内存回收路径。(也就是说:如果一个work是用于内存回收,我们创建一个救援线程是值得的,因为你给它一点内存,它可能会还给你更多的内存)
bound worker:
bound worker是与CPU绑定的worker,CMWQ会为每个CPU创建了两个bound work_pool,一个正常优先级的work_pool,一个高优先级的worker_pool,分别用于处理正常事物和高优先级事物。work_pool中的工人数量会动态变更(根据work项的情况),每个工人有一个固定格式名字,包含了3个属性,从工人的名字中可识别出worker绑定的CPU号、worker id、优先级。
unbound worker:
系统中除了每CPU上的bound worker,还存在一类unbound worker,顾名思义,这类worker不与任何CPU绑定,可以运行到任意CPU上。系统起来后会自动创建一个unbound工作队列,与它对应的work_pool是unbound类型的。
在一个SMP系统上,系统起来后,所有的worker呈现如下:
在一台8核的电脑上,输入ps -ax | grep worker,可以看到如下输出:
再来说说CMWQ的并发设计,CMWQ的核心就是worker的动态管理,从而实现work项的并发,那么是如何实现动态管理的呢?CMWQ内部实现了如下状态机:
worker状态转换:
(1)没有工作时,worker都处于idle状态。
(2)有work加入,空闲worker进入running状态。
(3)如果work带有阻塞/睡眠,worker进入sleep状态。后面有新work加入,work_pool会创建新的worker实现并发。
(4)work解除睡眠后,worker则继续进入running状态,处理完成后进入idle状态。
(5)当一个idle worker超过300s秒没有被唤醒,则会被销毁。
注意:work_pool中至少要保持1个worker,用于动态管理。
创建工作队列的接口:
struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, ...)
需要注意,这个接口仅仅是创建工作队列,并不会创建worker,在CMWQ时代,线程池与wq已经解耦了。work_pool作为系统的公共资源初始化时会创建好,被所有wq共享。
工作队列flags:
WQ_UNBOUND:指明wq为unbound队列。
WQ_MEM_RECLAIM:创建wq时会创建一个救援线程,nice=-20,常驻后台。
WQ_FREEZABLE:队列可以被冻结,用于CONFIG_FREEZER配置。
WQ_HIGHPRI:高优先级队列。nice=-20
WQ_CPU_INTENSIVE:队列定义为CPU消耗型。
WQ_SYSFS:在sysfs下创建wq设备节点。
WQ_POWERER_EFFICIENT:队列节能特性,表现为UNBOUND。
max_active:代表该队列最大并发处理work项的数目。默认=256。
内核启动后会自动生成一些默认的workqueue,供内核其他模块使用。当然用户也可以自己使用alloc_workqueue接口创建自己的队列。
工作队列的使用比较简单,网上资料很多,这里就不介绍了。另外想深挖代码的可以结合设计思想, 并把struct work_pool、struct pool_workqueue、struct workqueue、struct worker这几个核心数据结构搞清楚。然后看实现流程就比较顺了,并发管理的核心函数是manage_workers。
FAQ
Q:CMWQ实现了并发,那如何让work能按顺序执行?
A:使用alloc_ordered_workqueue接口创建队列。内核对wq置__WQ_ORDERED标记,并设置max_active=1。实现了顺序执行。
Q:已经被queued的work项是否可以cancel?
A:可以,通常delayed work会涉及cancel操作,调用cancel_work_sync来取消任务。(前提是work没有被执行哦)
Q:schedule_work一个工作项,work会在哪个CPU,哪个worker中运行呢?
A:schedule_work调度后,会在当前cpu上执行,如果没有正在运行的worker,那么会唤醒work_pool中第一个idle的worker执行。
Q:tasklet和workqueue都可以用于中断低半步,区别?
A:tasklet是一种软中断,要求任务不能有睡眠,且优先级大于workqueue。而workqueue是在进程上下文执行,允许睡眠。用于不紧急任务的推迟执行。
Q:内核中还有irq_work,和workqueue是一个东西吗?
A:irq_work同样是在中断上下文执行,实现原理和wq相似,但irq_work被queue后,会使用ipi中断进行触发,优先级更高。
Debugging
Q:一个kworker占用大量CPU资源?
两种可能:
(1)大量work项被排队到wq中,kworker需要反复运行任务回调函数。
(2)某些work项耗费了大量的CPU周期。
如果一个任务是CPU消耗型的,请为wq设置WQ_CPU_INTENSIVE标记。
借助ftrace定位:
echo workqueue:workqueue_queue_work > /sys/kernel/debug/tracing/set_event
查看进程栈:
cat /proc/pid/stack
魔法键sysrq-t也可以看到一些busy状态的workqueue,不过导出的信息比较多。
CMWQ出现后,一直活到了现在,就像CFS调度器一样,经典永垂不朽,其核心设计一直保留到现在。不过CMWQ的代码还是蛮复杂的,不过当你真正理解了代码所有实现,还是很有成就感的,柳暗花明。
以往文章: