内核机制—workqueue-2-资料
一、workqueue.rst
注: 翻译自 msm-5.4/Documentation/core-api/workqueue.rst
====================================
并发管理工作队列(cmwq)
====================================
:Date: 2010 年 9 月
:Author: Tejun Heo <tj@kernel.org>
:Author: Florian Mickler <florian@mickler.org>
1. 简介
在很多场景下,都需要一个异步进程执行上下文,而 workqueue(wq)API 是这类场景最常用的机制。
当需要这种异步执行上下文时,会把一个描述“要执行哪个函数”的 work item 放入一个队列。一个独立线程充当该异步执行上下文。这个队列叫 workqueue,这个线程叫 worker。
只要 workqueue 里还有 work item,worker 就会逐个执行对应函数。当队列中没有剩余 work item 时,worker 会进入空闲状态。新的 work item 入队后,worker 会再次开始执行。
2. 为什么需要 cmwq
在最初的 wq 实现中,多线程(MT)wq 是“每 CPU 一个 worker 线程”,单线程(ST)wq 是“全系统一个 worker 线程”。单个 MT wq 需要维持与 CPU 数量相同规模的 worker。多年下来,内核里 MT wq 用户越来越多,而 CPU 核心数持续上升,有些系统仅启动时就会耗尽默认的 32k PID 空间。
虽然 MT wq 浪费了大量资源,但提供的并发水平并不理想。这个限制对 ST 与 MT 都存在,只是 MT 稍轻。每个 wq 维护各自独立的 worker 池。MT wq 每 CPU 只能提供一个执行上下文,ST wq 整个系统只有一个。work item 必须争抢这些非常有限的执行上下文,导致各种问题,包括围绕单执行上下文更容易死锁。
“并发水平”与“资源占用”之间的张力,也迫使使用者做出不必要的折中。例如 libata 为了轮询 PIO 选择 ST wq,并接受“两个轮询 PIO 无法并行推进”这一额外限制。由于 MT wq 的并发改进也有限,需要更高并发的用户(如
async、fscache)不得不自建线程池。
并发管理工作队列(cmwq)是对 wq 的重实现,目标聚焦于:
* 兼容原有 workqueue API。
* 使用按 CPU 统一的 worker 池(由所有 wq 共享),按需提供灵活并发,避免大量资源浪费。
* 自动调节 worker 池和并发级别,让 API 使用者无需关心这些细节。
3. 设计
为简化函数的异步执行,引入了一个新抽象:work item。
work item 是一个简单结构体,保存要异步执行函数的指针。驱动或子系统如果想异步执行某个函数,需要先设置一个指向该函数的 work item,再把它排入某个 workqueue。
名为 worker thread 的专用线程会从队列中逐个取出并执行函数。若没有排队工作,worker thread 就会空闲。这些 worker thread 由所谓 worker-pool 进行管理。
cmwq 设计区分了两层:
一层是面向用户的 workqueue(驱动/子系统往这里投递 work item),另一层是后端机制(管理 worker-pool 并处理排队 work item)。
对于每个可能 CPU,都有两个 worker-pool:普通 work 用一个,高优先级 work 用一个;另外还有一些 worker-pool 用于 unbound workqueue。后者的后端池数量是动态的。
驱动和子系统可以按需通过 workqueue API 创建并投递 work item。它们可通过 workqueue 标志位影响 work item 执行方式,例如 CPU 局部性、并发上限、优先级等。详细说明见下文 "alloc_workqueue()"。
当 work item 被排入某个 workqueue 时,系统会根据队列参数和 workqueue 属性决定目标 worker-pool,并把该 work item 追加到该 worker-pool 的共享 worklist 中。例如,若未特别覆写,bound workqueue 上的 work item 会排到“当前发起 CPU 对应”的普通或高优先级 worker-pool 的 worklist 上。
对任何 worker-pool 实现来说,并发级别管理(有多少执行上下文处于活跃)都是关键问题。cmwq 的目标是把并发保持在“最小但足够”的水平:“最小”用于节省资源,“足够”则保证系统满负荷利用。
每个绑定到实际 CPU 的 worker-pool 都会通过与调度器联动来实现并发管理。只要活跃 worker 休眠或唤醒,worker-pool 就会收到通知,并跟踪当前可运行 worker 数量。通常认为 work item 不应长期独占 CPU。因此,只维持“刚好不阻塞处理流程”的并发最优:只要 CPU 上有一个或更多可运行 worker,worker-pool 就不会启动新的 work;而当最后一个运行中的 worker 进入休眠,会立刻调度新的 worker,避免
在仍有待处理 work item 时 CPU 空转。这样能以最少 worker 数量保持执行带宽。
保留空闲 worker 的成本基本只是 kthread 的内存占用,因此 cmwq 会在一段时间后才回收空闲 worker。
对于 unbound workqueue,后端池数量是动态的。unbound workqueue 可通过 "apply_workqueue_attrs()" 指定自定义属性,workqueue 会自动创建符合属性的后端 worker-pool。并发级别调节责任由使用者承担。此外还有一个标志可让 bound wq 忽略并发管理。详见 API 章节。
前进性(forward progress)保证依赖于:当需要更多执行上下文时可以创建 worker,而这又通过 rescue worker 机制得到保证。所有可能出现在内存回收路径中的 work item,必须排入在内存压力下保留 rescue worker 的 wq。否则 worker-pool 可能因等待执行上下文释放而死锁。
4. 应用编程接口(API)
"alloc_workqueue()" 用于分配 wq。原先的 "create_*workqueue()" 已弃用并计划移除。"alloc_workqueue()"
接收三个参数:"@name"、"@flags" 和 "@max_active"。
"@name" 是 wq 名称;若存在 rescuer 线程,也会用作其线程名。wq 不再直接管理执行资源,而是承担前进性保证、flush 以及 work item 属性域的作用。
"@flags" 与 "@max_active" 控制 work item 如何被分配执行资源、调度和执行。
4.1 flags 参数
(1) "WQ_UNBOUND"
排入 unbound wq 的 work item 由特殊 worker-pool 处理,这些 worker 不绑定到特定 CPU。这样 wq 更像一个简单执行上下文提供者,不进行并发管理。unbound worker-pool 会尽快启动 work item 执行。unbound wq 牺牲了局部性,但适用于以下场景:
* 预期并发需求波动很大。若使用 bound wq,当提交者在不同 CPU 间跳转,可能在各 CPU 上创建大量大多闲置的 worker。
* 长时间运行且 CPU 密集型负载,交给系统调度器管理更好。
(2) "WQ_FREEZABLE"
freezable wq 参与系统 suspend 的 freeze 阶段。该 wq 上的 work item 会被排空,在 thaw 之前不会启动新的 work item。
(3) "WQ_MEM_RECLAIM"
所有可能用于内存回收路径的 wq **必须** 设置该标志。无论内存压力多大,该 wq 都保证至少有一个执行上下文。
(4) "WQ_HIGHPRI"
highpri wq 的 work item 会排入目标 CPU 的 highpri worker-pool。highpri worker-pool 使用更高 nice 级别的 worker 线程服务。
注意:普通 worker-pool 与 highpri worker-pool 彼此不交互。两者分别维护各自 worker 池,并在各自池内实施并发管理。
(5) "WQ_CPU_INTENSIVE"
CPU 密集型 wq 的 work item 不计入并发级别。换言之,可运行的 CPU 密集型 work item 不会阻止同一 worker-pool 内其他 work item 启动执行。对于预期会大量消耗 CPU 周期的 bound work item,这很有用,因为其执行可由系统调度器调节。
虽然 CPU 密集型 work item 不计入并发级别,但其“启动执行”仍受并发管理约束;可运行的非 CPU 密集型 work item 仍可延迟 CPU 密集型 work item 的启动。
该标志对 unbound wq 无意义。
请注意,"WQ_NON_REENTRANT" 标志已不存在,因为现在所有 workqueue 都非重入:任一 work item 在任意时刻全系统最多只会由一个 worker 执行。
4.2 max_active
"@max_active" 决定每 CPU 可分配给某个 wq 的最大执行上下文数量。例如 "@max_active" 为 16 时,该 wq 在每 CPU 上最多同时执行 16 个 work item。
当前实现中,bound wq 的 "@max_active" 上限为 512;若指定 0,默认值为 256。对于 unbound wq,上限为 512 与 "4 * num_possible_cpus()" 二者中的较大值。选择这些值是为了在不成为常规瓶颈的前提下,对失控场景提供保护。
wq 的活跃 work item 数量通常由用户行为调节,更具体地说取决于用户同时排队多少 work item。除非有明确节流需求,推荐指定 0。
有些用户依赖 ST wq 的严格执行顺序。过去可通过 "WQ_UNBOUND" + "@max_active" 为 1 来实现。此配置下,work item 总是排到 unbound worker-pool,且任一时刻仅一个活跃 work item,因此可获得与 ST wq 相同的顺序属性。
在当前实现中,上述配置仅能保证“单个 NUMA 节点内”的 ST 行为。若要获得“全系统范围”的 ST 行为,应使用 "alloc_ordered_queue()"。
5. 执行场景示例
下面的示例尝试说明 cmwq 在不同配置下的行为。
work item w0、w1、w2 被排到同一 CPU 上的 bound wq q0。w0 先占用 CPU 5ms,再休眠 10ms,再占用 CPU 5ms 后结束。w1 和 w2 各占用 CPU 5ms,再休眠 10ms。
忽略其他任务、work 与处理开销,并假设简单 FIFO 调度,原始 wq 下可能出现如下(高度简化)事件序列: ::
------------------------------------------------- TIME IN MSECS EVENT ------------------------------------------------- 0 w0 starts and burns CPU 5 w0 sleeps 15 w0 wakes up and burns CPU 20 w0 finishes 20 w1 starts and burns CPU 25 w1 sleeps 35 w1 wakes up and finishes 35 w2 starts and burns CPU 40 w2 sleeps 50 w2 wakes up and finishes -------------------------------------------------
I: 单线程上 pending 的所有 work 串行处理的情况,即使前面的 work func 休眠了(没有执行结束),占用着执行上下文,其它 pending 的 work 也无法被执行。
在 cmwq 且 "@max_active" >= 3 时: ::
------------------------------------------------- TIME IN MSECS EVENT ------------------------------------------------- 0 w0 starts and burns CPU 5 w0 sleeps 5 w1 starts and burns CPU 10 w1 sleeps 10 w2 starts and burns CPU 15 w2 sleeps 15 w0 wakes up and burns CPU 20 w0 finishes 20 w1 wakes up and finishes 25 w2 wakes up and finishes -------------------------------------------------
I: 也就是说 work func 休眠后,让出CPU了,就可以有其它 work 放到CPU上运行了,可以有三个 work 同时运行。
若 "@max_active" == 2: ::
------------------------------------------------- TIME IN MSECS EVENT ------------------------------------------------- 0 w0 starts and burns CPU 5 w0 sleeps 5 w1 starts and burns CPU 10 w1 sleeps 15 w0 wakes up and burns CPU 20 w0 finishes 20 w1 wakes up and finishes 20 w2 starts and burns CPU 25 w2 sleeps 35 w2 wakes up and finishes -------------------------------------------------
I: max_active=2 即表示同一 workqueue 上最多只能有 2 个 work func 并行运行,即使两个 work func 执行中途休眠了,其它 pengding 状态的 work 的 func 也不能运行。
现在假设 w1 与 w2 被排入另一个设置了 "WQ_CPU_INTENSIVE" 的 q1: ::
------------------------------------------------- TIME IN MSECS EVENT ------------------------------------------------- 0 w0 starts and burns CPU 5 w0 sleeps 5 w1 and w2 start and burn CPU 10 w1 sleeps 15 w2 sleeps 15 w0 wakes up and burns CPU 20 w0 finishes 20 w1 wakes up and finishes 25 w2 wakes up and finishes -------------------------------------------------
I: rescue 应该是对应同一个 work,可以 queue 到其它 workqueue 上运行。
6. 使用建议
* 若某个 wq 可能处理“用于内存回收路径”的 work item,务必使用 "WQ_MEM_RECLAIM"。每个设置该标志的 wq 都有保留执行上下文。若多个用于内存回收的 work item 之间存在依赖,应分别排入不同的 wq,并且每个都设置 "WQ_MEM_RECLAIM"。
* 除非必须严格顺序执行,否则不需要使用 ST wq。
* 除非有明确需求,推荐把 @max_active 设为 0。在大多数场景下,并发水平通常远低于默认上限。
* wq 充当前进性保证("WQ_MEM_RECLAIM")、flush 和 work item 属性的作用域。若某些 work item 不涉及内存回收,不需要作为一组被 flush,也不需要特殊属性,可直接使用系统 wq。使用专用 wq 与系统 wq 在执行特性上 没有差异。
* 除非预期 work item 会消耗大量 CPU 周期,bound wq 通常更有利,因为 wq 操作与 work item 执行的局部性更好。
7. 调试
由于 work 函数由通用 worker 线程执行,定位异常 workqueue 用户时,需要一些技巧。
worker 线程在进程列表中显示为: ::
root 5671 0.0 0.0 0 0 ? S 12:07 0:00 [kworker/0:1] root 5672 0.0 0.0 0 0 ? S 12:07 0:00 [kworker/1:2] root 5673 0.0 0.0 0 0 ? S 12:12 0:00 [kworker/0:0] root 5674 0.0 0.0 0 0 ? S 12:13 0:00 [kworker/1:0]
如果 kworker 异常忙(CPU 使用过高),通常有两类问题:
(1) 某处在高频率连续调度 work
(2) 单个 work item 消耗了大量 CPU 周期
第一类问题可用 tracing 跟踪: ::
$ echo workqueue:workqueue_queue_work > /sys/kernel/debug/tracing/set_event $ cat /sys/kernel/debug/tracing/trace_pipe > out.txt (wait a few secs) ^C
若存在 work queue 的忙等入队,输出会被其主导,可通过 work item 函数定位问题来源。
第二类问题通常可直接查看异常 worker 线程栈: ::
$ cat /proc/THE_OFFENDING_KWORKER/stack
通常可以在栈中直接看到对应 work item 的函数。
8. 内核内联文档参考
.. kernel-doc:: include/linux/workqueue.h
posted on 2026-05-21 20:50 Hello-World3 阅读(3) 评论(0) 收藏 举报
浙公网安备 33010602011771号