Linux 内核 Workqueue 完全指南:cmwq 并发管理机制、alloc_workqueue API 与亲和性调优
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
工作队列(workqueue)是 Linux 内核中最常用的异步执行机制:驱动与子系统把要异步执行的函数封装成"工作项"(work item)投入队列,由内核线程"工人"(worker)代为执行。本指南以 Documentation/core-api/workqueue.rst 为核心,结合 include/linux/workqueue.h 与 kernel/workqueue.c 的源码实现,完整讲解并发管理工作队列(cmwq)的设计动机、alloc_workqueue()的 flags 与max_active语义、亲和性作用域(affinity scope)的性能权衡,以及使用wq_dump.py/wq_monitor.py进行配置检查、监控与调试的实战方法。读完本文,你将能够为驱动或子系统正确选择工作队列属性、避免内存回收路径死锁,并根据机器拓扑对 CPU 密集型工作队列做精细化调优。
引言:什么是工作队列
内核中大量场景需要一个"异步进程执行上下文":某些函数不应该在发起者自己的执行流里同步运行,而应该放到后台去处理。工作队列(workqueue,简称 wq)API 就是为此提供的最常用机制,其基本模型非常朴素:
- 当需要一个异步执行上下文时,把描述"要执行哪个函数"的工作项(work item)放到一个队列上;
- 一个独立的线程充当异步执行上下文,这个队列称为工作队列,这个线程称为工人(worker);
- 只要队列里还有工作项,worker 就按顺序一个一个地执行它们关联的函数;队列空了 worker 转入空闲;新的工作项被投入队列后,worker 再次开始执行。
工作项是一个简单的结构体,保存指向待异步执行函数的指针(即 include/linux/workqueue.h 中的struct work_struct,通常通过INIT_WORK()初始化、queue_work()投入队列)。驱动或子系统希望某个函数异步执行时,只需设置好指向该函数的工作项并把它挂到某个工作队列上即可。工作项既可以在线程上下文执行,也可以在 BH(softirq)上下文执行——后者由WQ_BH标志开启。
为什么需要 cmwq:并发管理工作队列
原始实现的困境
在 cmwq(Concurrency Managed Workqueue,并发管理工作队列)出现之前,工作队列的实现存在两个突出的问题:
资源浪费:一个多线程(MT)工作队列为每个 CPU 各保留一个 worker 线程,单线程(ST)工作队列则全系统只有一个 worker。随着内核中 MT 工作队列用户不断增加、CPU 核心数持续攀升,某些系统仅在启动阶段就会耗尽默认的 32k PID 空间——每个 MT wq 都需要"每 CPU 一个线程",多 CPU 大系统上开销极其可观。
并发度不足:每个工作队列维护自己独立的 worker 池,MT wq 每 CPU 只能提供一个执行上下文,ST wq 整个系统只有一个。工作项必须竞争这些极为有限的执行上下文,带来了各种问题,包括围绕单一执行上下文的死锁倾向。例如 libata 在轮询 PIO 时选择使用 ST wq,被迫接受"两个轮询 PIO 不能同时进行"的不必要限制;而需要更高并发度的 async、fscache 等用户,不得不自己实现线程池。
cmwq 的三大目标
cmwq 是工作队列的重写版本,聚焦于以下目标:
- 保持与原有工作队列 API 的兼容性(
queue_work()等接口不变); - 使用所有工作队列共享的每 CPU 统一 worker 池,按需提供灵活的并发度,避免浪费大量资源;
- 自动调节 worker 池规模与并发水平,让 API 使用者不必关心这些细节。
从源码结构看,这一设计在 kernel/workqueue.c 中体现为两个层次的抽象:面向用户的工作队列struct workqueue_struct,以及后端统一管理的struct worker_pool(第 195 行附近)与struct pool_workqueue(第 271 行附近)。
设计架构:工作项、工人与 worker 池
工作项(work item)
为简化函数的异步执行,cmwq 引入工作项这一抽象:一个持有"将被异步执行的函数指针"的简单结构体。每当驱动或子系统需要异步执行某函数,就设置一个指向该函数的工作项,并将其投入工作队列。
线程池与 BH 池
对于线程化工作队列,名为[k]worker的专用线程从队列中依次取出函数执行;没有工作时 worker 线程进入空闲状态。这些 worker 线程由worker-pool(worker 池)统一管理。cmwq 的设计把两类东西明确分开:
- 用户可见的工作队列:子系统与驱动向它投递工作项;
- 后端机制:管理 worker 池、处理已入队工作项。
对于每个可能的 CPU,系统维护两个 worker 池:一个服务普通工作项,另一个服务高优先级(highpri)工作项;此外还有若干服务于unbound(无绑定)工作队列的额外 worker 池,这类后备池的数量是动态的。
BH 工作队列复用同一套框架,但因为 BH(softirq)同一时刻只能有一个并发执行上下文,无需担心并发管理:每个每 CPU BH worker 池只包含一个"伪 worker",代表 BH 执行上下文。因此可以认为 BH 工作队列是 softirq 的一个便捷接口(include/linux/workqueue.h 中WQ_BH = 1 << 0的注释即"execute in bottom half (softirq) context")。
工作项的投递路径
当工作项被投入某个工作队列时,内核根据投递参数与工作队列属性确定目标 worker 池,并把它追加到该池的共享工作列表(worklist)上。例如,除非被显式覆盖,一个 bound(绑定)工作队列的工作项会被投递到"发起者当前所在 CPU"关联的普通或 highpri worker 池工作列表上。
并发管理:让并发度"最小且足够"
对任何线程池实现而言,管理并发水平(同时活跃的执行上下文数量)都是核心问题。cmwq 的目标是把并发度维持在"最小且足够":最小以节省资源,足够以让系统满负荷运转。
每个绑定到真实 CPU 的 worker 池通过挂接调度器实现并发管理:每当活跃 worker 被唤醒或睡眠,worker 池都会收到通知,并持续跟踪当前可运行的 worker 数量。一般而言,工作项不会被期望占用 CPU 太久,因此只要维持足够的并发度防止工作处理停滞就是最优的。具体规则是:
- 只要 CPU 上还有一个或多个可运行的 worker,worker 池不启动新工作的执行;
- 当最后一个正在运行的 worker 进入睡眠时,立即调度一个新 worker,让 CPU 在有挂起工作项时不至于闲置。
这保证了用最少数量的 worker 就能不损失执行带宽。空闲 worker 保留的成本仅是 kthread 占用的内存,因此 cmwq 会让空闲 worker 存活一段时间再销毁,避免频繁创建/销毁线程的开销。
对于 unbound 工作队列,后备池数量是动态的:可通过apply_workqueue_attrs()为 unbound 工作队列指定自定义属性,工作队列会自动创建匹配属性的后备 worker 池;此时调节并发度的责任落在使用者身上。此外还有一个标志可以把 bound wq 标记为"忽略并发管理"(即下文WQ_CPU_INTENSIVE)。
前向进展保证与 rescue worker
cmwq 的前向进展(forward progress)保证依赖"需要更多执行上下文时可以创建新 worker",而这又通过rescue worker(救援工人)机制保障:所有可能被内存回收(memory reclaim)路径使用的工作项,必须投递到带有保留救援 worker 的工作队列上(即设置WQ_MEM_RECLAIM标志)。否则在内存压力下,worker 池可能因等待执行上下文释放而死锁。
API:alloc_workqueue()
alloc_workqueue()用于分配一个工作队列,它是当前唯一推荐的创建接口,原来的create_*workqueue()系列函数已被弃用并计划移除。函数原型为:
struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, ...);三个核心参数:
@fmt(名称):工作队列的名字,同时如果有 rescue 线程,也会用作救援线程的名字;@flags:控制工作项如何被分配执行资源、调度与执行;@max_active:限制并发执行的工作项数量。
从源码看,alloc_workqueue()最终通过alloc_workqueue_noprof()→alloc_workqueue_va()→__alloc_workqueue()完成创建(kernel/workqueue.c 第 6026-6054 行),并在__alloc_workqueue()中为 wq 分配workqueue_attrs。在 cmwq 中,wq 本身不再管理执行资源,而是作为前向进展保证、flush(冲刷)与工作项属性的作用域(domain)。
flags 详解
下表汇总了 include/linux/workqueue.h(第 372-421 行)与文档中定义的全部用户可见标志:
| 标志 | 位 | 含义 |
|---|---|---|
WQ_BH | 1<<0 | BH 工作队列,可视为 softirq 的便捷接口。总是 per-CPU,所有 BH 工作项在投递 CPU 的 softirq 上下文中按投递顺序执行。所有 BH 工作队列必须使用 0max_active,且WQ_HIGHPRI是唯一允许附加的标志。BH 工作项不能睡眠;延迟投递、flush、取消等其他特性均支持 |
WQ_UNBOUND | 1<<1 | unbound wq 的工作项由"不绑定任何特定 CPU"的特殊 worker 池服务,使 wq 表现为一个没有并发管理的简单执行上下文提供者。unbound worker 池会尽可能快地启动工作项执行。牺牲局部性,但适合以下场景:① 并发度需求剧烈波动,用 bound wq 可能在不同 CPU 间产生大量闲置 worker;② 长时 CPU 密集型负载,更适合交给系统调度器管理 |
WQ_FREEZABLE | 1<<2 | 可冻结工作队列参与系统挂起(suspend)的冻结阶段:工作项被排空,且在新的解冻(thaw)之前不会有新工作项启动执行 |
WQ_MEM_RECLAIM | 1<<3 | 所有可能用于内存回收路径的 wq 必须设置此标志。设置了该标志的 wq 保证无论内存压力多大,至少有一个执行上下文(rescue worker)可用 |
WQ_HIGHPRI | 1<<4 | highpri wq 的工作项被投递到目标 CPU 的 highpri worker 池,该池由提升过 nice 值(nice 级别更低)的 worker 线程服务。普通与 highpri 池互不交互,各自维护独立 worker 池并独立实施并发管理 |
WQ_CPU_INTENSIVE | 1<<5 | CPU 密集型 wq 的工作项不计入并发度:可运行的 CPU 密集型工作项不会阻止同一 worker 池中其他工作项启动执行。适合"预期会长时间占用 CPU 的 bound 工作项",让其执行交由系统调度器调节。注意:虽然不计入并发度,其启动仍受并发管理约束,可运行的非 CPU 密集型工作项可能延迟 CPU 密集型工作项的执行。该标志对 unbound wq 无意义 |
WQ_SYSFS | 1<<6 | 使工作队列在 sysfs 中可见(见下文章节) |
WQ_PERCPU | 1<<8 | 投递到 per-cpu wq 的工作项绑定到特定 CPU。当 CPU 局部性重要时这是正确选择,是WQ_UNBOUND的互补标志 |
其中WQ_BH、WQ_PERCPU、WQ_UNBOUND、WQ_FREEZABLE、WQ_MEM_RECLAIM、WQ_HIGHPRI、WQ_CPU_INTENSIVE、WQ_SYSFS均在 include/linux/workqueue.h 第 372-421 行有对应位定义与注释;另外内部标志__WQ_ORDERED = 1 << 17用于标记有序工作队列。
max_active:并发执行上限
@max_active决定一个 wq 的工作项每 CPU 可获得的最大执行上下文数。例如@max_active为 16 时,该 wq 每 CPU 最多同时有 16 个工作项在执行。注意:即使对 unbound 工作队列,这也是一个 per-CPU 属性。
- 上限 2048:源码中
WQ_MAX_ACTIVE = 2048(include/linux/workqueue.h 第 419 行),__alloc_workqueue()中会用clamp_val(max_active, 1, WQ_MAX_ACTIVE)把传入值钳制到[1, 2048]区间(kernel/workqueue.c 第 5773-5777 行); - 默认值 1024:当指定 0 时使用默认值,源码中定义为
WQ_DFL_ACTIVE = WQ_MAX_ACTIVE / 2(即 1024)。这些取值足够高,通常不会成为瓶颈,同时又能防止失控(runaway)场景。
一个 wq 的活跃工作项数量通常由 wq 的使用者自己调节——更确切地说,由使用者同时投递多少个工作项决定。除非有明确的节流需求,文档推荐直接指定 0 使用默认值。
严格顺序执行的正确姿势:某些用户依赖严格的执行顺序,要求任意时刻只有一个工作项在途且按投递顺序处理。过去用@max_active=1加WQ_UNBOUND实现这一行为,但现在不再成立,应改用alloc_ordered_workqueue()。从源码看,alloc_ordered_workqueue()正是展开为alloc_workqueue(fmt, WQ_UNBOUND | __WQ_ORDERED | (flags), 1, ...)——即强制 unbound、内部有序标志并把max_active固定为 1(include/linux/workqueue.h 第 585-598 行)。内核对 ordered wq 有专门的 attrs 处理路径(apply_workqueue_attrs_locked(wq, ordered_wq_attrs[highpri]),kernel/workqueue.c 第 5731 行)。
示例执行场景:cmwq 的行为演示
文档用一组精心构造的时序实验说明 cmwq 在不同配置下的行为差异。场景设定:工作项 w0、w1、w2 被投递到同一 CPU 上的 bound wq q0;w0 烧 CPU 5ms、睡眠 10ms、再烧 CPU 5ms 后结束;w1、w2 各烧 CPU 5ms、睡眠 10ms。忽略其他任务与处理开销,假设简单 FIFO 调度:
原始 wq 的行为(串行执行,总耗时 50ms):
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 finishescmwq 且@max_active>= 3(并发管理让睡眠的 w0 让出执行上下文,总耗时 25ms):
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 finishescmwq 且@max_active== 2(并发度被限制为 2,w2 需等 w0 完成才能启动):
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 finishesw1、w2 被投递到设置了WQ_CPU_INTENSIVE的另一个 wq q1(CPU 密集型工作项不计入并发度,因此 w1、w2 可以同时启动执行):
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对比可清晰看出:cmwq 的并发管理在"工作项睡眠"这种执行上下文让渡场景中能显著压缩总耗时,而max_active与WQ_CPU_INTENSIVE则是调节并发度的两个关键旋钮。
使用指南(Guidelines)
- 不要忘记
WQ_MEM_RECLAIM:只要 wq 可能处理用于内存回收期间的工作项就必须设置。每个带WQ_MEM_RECLAIM的 wq 都保留一个专属执行上下文。如果多个用于内存回收的工作项之间存在依赖关系,它们应被投递到各自独立且都带WQ_MEM_RECLAIM的 wq 中,避免互相等待。 - 除非严格要求顺序,无需使用 ST(单线程)wq。
@max_active推荐用 0:除非有特定需求,绝大多数用例的并发水平远低于默认上限 1024。- 优先复用系统工作队列:wq 是前向进展保证(
WQ_MEM_RECLAIM)、flush 与工作项属性的作用域。不涉及内存回收、不需要作为一组工作项整体 flush、也不需要特殊属性的工作项,可以直接使用系统 wq——专用 wq 与系统 wq 在执行特性上没有区别。但注意:如果某生产者在某些情况下可能产生超过@max_active的在途工作项(务必对生产者做压力测试),它可能占满系统 wq 并导致死锁,此时应使用自己的专用工作队列。 - 优先使用 bound wq:除非工作项预期消耗大量 CPU 周期,使用 bound wq 通常更有利,因为 wq 操作与工作项执行的局部性更好。
从源码可见,内核为通用场景预先创建了一组系统工作队列(kernel/workqueue.c 第 8224-8240 行):events(system_wq,per-cpu)、events_highpri、events_long、events_unbound(system_unbound_wq/system_dfl_wq)、events_freezable、events_power_efficient、events_freezable_pwr_efficient、events_bh、events_bh_highpri以及events_dfl_long等,驱动可以直接使用这些现成的执行域。
亲和性作用域(Affinity Scopes)
unbound 工作队列会根据其亲和性作用域对 CPU 分组,以改善缓存局部性。例如使用默认作用域cache_shard时,CPU 会被分成若干 sub-LLC(末级缓存之下)分片;某个 CPU 上投递的工作项会被分配给同一分片内某个 CPU 上的 worker。worker 启动后是否允许移出作用域,取决于该作用域的affinity_strict设置。
workqueue 支持以下亲和性作用域:
| 作用域 | 语义 |
|---|---|
default | 使用模块参数workqueue.default_affinity_scope指定的作用域,该参数始终被设置为下列之一 |
cpu | CPU 不分组。在某 CPU 投递的工作项由同一 CPU 上的 worker 处理,使 unbound wq 表现得像"无并发管理的 per-cpu wq" |
smt | 按 SMT 边界分组,通常每个物理核的逻辑线程被分到一组 |
cache | 按缓存边界分组,具体使用哪级缓存由架构代码决定(多数情况用 L3) |
cache_shard | CPU 被分为最多wq_cache_shard_size个核的 sub-LLC 分片(默认 8,可用workqueue.cache_shard_size启动参数调节),分片总是在核(SMT 组)边界上切分。这是默认亲和性作用域 |
numa | 按 NUMA 边界分组 |
system | 所有 CPU 在同一组,workqueue 不努力在靠近投递 CPU 的地方处理工作项 |
源码佐证:cache_shard_size在 kernel/workqueue.c 第 446-447 行定义为unsigned int wq_cache_shard_size = 8,并注册为模块参数cache_shard_size(权限 0444);分片数量的计算在wq_calc_node_cpumask附近的布局逻辑中,按DIV_ROUND_CLOSEST(nr_cores, wq_cache_shard_size)估算(第 8467 行),且参数被强制要求大于 0,否则回退为 1(第 8559-8561 行)。default_affinity_scope则是通过module_param_cb(default_affinity_scope, ...)注册的可写模块参数(第 7321 行)。
默认亲和性作用域可通过模块参数workqueue.default_affinity_scope修改,单个工作队列的作用域可通过apply_workqueue_attrs()修改。
sysfs 接口
如果设置了WQ_SYSFS,工作队列会在/sys/devices/virtual/workqueue/WQ_NAME/目录下提供如下亲和性相关接口文件:
affinity_scope:读取显示当前亲和性作用域,写入可修改。当当前作用域是default时,读取还会以括号显示实际生效的作用域,例如default (cache);affinity_strict:默认 0,表示亲和性作用域非严格。工作项开始执行时,workqueue 会尽力保证 worker 在其作用域内(称为repatriation,遣返);一旦启动,调度器可以自由地把 worker 移动到系统中任何 CPU,从而在享受作用域局部性的同时,必要时仍能利用其他 CPU。若设为 1,则保证该作用域的所有 worker 始终留在作用域内——这在跨越亲和性作用域有额外影响(如功耗、工作负载隔离)时有用;严格 NUMA 作用域还可以用来复现旧内核的 workqueue 行为。
亲和性作用域与性能:dm-crypt 实测
理想情况下,unbound wq 无需调优就能对绝大多数用例最优。但当前内核中,局部性与利用率之间存在显著权衡,当工作队列被重度使用时需要显式配置。
- 局部性越高,相同 CPU 周期完成的工作越多(效率更高);
- 但局部性过高,如果投递者没有把工作项充分分散到各作用域,可能导致整体系统利用率下降。
文档以下面这套 dm-crypt 实测(12 核 24 线程、分布在四个 L3 缓存上,AMD Ryzen 9 3900x;关闭 CPU boost 保证一致性;/dev/dm-0是 NVME SSD 上的 dm-crypt 设备)清楚展示了这一权衡。测试关注kcryptd工作队列在不同亲和性作用域下的表现,带宽单位为 MiBps、CPU 利用率单位为百分比,每组测 5 次取均值:
场景 1:投递者充足、工作分散全机(fio --numjobs=24 --iodepth=64,--verify=sha512使每次生成并回读内容,让投递者与kcryptd之间的执行局部性变得重要):
$ fio --filename=/dev/dm-0 --direct=1 --rw=randrw --bs=32k --ioengine=libaio \ --iodepth=64 --runtime=60 --numjobs=24 --time_based --group_reporting \ --name=iops-test-job --verify=sha512| Affinity | Bandwidth (MiBps) | CPU util (%) |
|---|---|---|
| system | 1159.40 ±1.34 | 99.31 ±0.02 |
| cache | 1166.40 ±0.89 | 99.34 ±0.01 |
| cache (strict) | 1166.00 ±0.71 | 99.35 ±0.01 |
投递者充足且遍布全系统时,cache(严格或不严格)都没有劣势:三种配置都能打满整机,但 cache 亲和配置凭借更好的局部性还领先约 0.6%。
场景 2:投递者较少、但工作量足以饱和(仅将--numjobs改为 8):
$ fio --filename=/dev/dm-0 --direct=1 --rw=randrw --bs=32k \ --ioengine=libaio --iodepth=64 --runtime=60 --numjobs=8 \ --time_based --group_reporting --name=iops-test-job --verify=sha512| Affinity | Bandwidth (MiBps) | CPU util (%) |
|---|---|---|
| system | 1155.40 ±0.89 | 97.41 ±0.05 |
| cache | 1154.40 ±1.14 | 96.15 ±0.09 |
| cache (strict) | 1112.00 ±4.64 | 93.26 ±0.35 |
工作量仍足以压满系统,system与cache都接近饱和,cache消耗更少 CPU 却因效率更高达到与system相同的带宽;8 个投递者在四个 L3 作用域间移动时,cache (strict)仍能基本打满,但失去工作守恒(work-conservation)的代价开始显现——带宽损失约 3.7%。
场景 3:投递者更少、工作量不足以饱和(--numjobs=4):
$ fio --filename=/dev/dm-0 --direct=1 --rw=randrw --bs=32k \ --ioengine=libaio --iodepth=64 --runtime=60 --numjobs=4 \ --time_based --group_reporting --name=iops-test-job --verify=sha512| Affinity | Bandwidth (MiBps) | CPU util (%) |
|---|---|---|
| system | 993.60 ±1.82 | 75.49 ±0.06 |
| cache | 973.40 ±1.52 | 74.90 ±0.07 |
| cache (strict) | 828.20 ±4.49 | 66.84 ±0.29 |
此时局部性与利用率之间的权衡变得非常清晰:cache相比system带宽损失约 2%,而cache (strict)损失高达约 20%。
结论与建议
cache作用域相对system的效率优势虽然一致且可察觉,但幅度较小;其影响取决于各作用域之间的距离,在拓扑更复杂的处理器上可能更显著。- 虽然某些场景下损失工作守恒有代价,但远好于
cache (strict),而且"最大化工作队列利用率"本来就不是常见需求,因此cache是 unbound 池的默认亲和性作用域(对应源码中cache_shard为默认 scope 的设计)。 - 可能消耗大量 CPU 的 workqueue 使用方,建议用
apply_workqueue_attrs()和/或开启WQ_SYSFS进行显式配置。 - 严格
cpu亲和性作用域的 unbound wq 行为等同于WQ_CPU_INTENSIVE的 per-cpu wq,但前者没有真正的优势,且 unbound wq 提供了多得多的灵活性。 - 亲和性作用域在 Linux v6.5 引入;要模拟旧内核行为,使用严格
numa亲和性作用域。 - 非严格亲和性作用域中工作守恒的损失很可能源于调度器;理论上内核在大多数情况下本可以既做对事又保持工作守恒,因此未来调度器的改进可能让这些调优参数变得不再必要。
检查配置:wq_dump.py
使用 tools/workqueue/wq_dump.py 可以检查 unbound CPU 亲和性配置、worker 池,以及工作队列到池的映射关系:
$ tools/workqueue/wq_dump.py Affinity Scopes =============== wq_unbound_cpumask=0000000f CPU nr_pods 4 pod_cpus [0]=00000001 [1]=00000002 [2]=00000004 [3]=00000008 pod_node [0]=0 [0]=0 [1]=1 [2]=1 cpu_pod [0]=0 [1]=1 [2]=2 [3]=3 SMT nr_pods 4 pod_cpus [0]=00000001 [1]=00000002 [2]=00000004 [3]=00000008 pod_node [0]=0 [0]=0 [1]=1 [2]=1 cpu_pod [0]=0 [1]=1 [2]=2 [3]=3 CACHE (default) nr_pods 2 pod_cpus [0]=00000003 [1]=0000000c pod_node [0]=0 [1]=1 cpu_pod [0]=0 [1]=0 [2]=1 [3]=1 NUMA nr_pods 2 pod_cpus [0]=00000003 [1]=0000000c pod_node [0]=0 [1]=1 cpu_pod [0]=0 [1]=0 [2]=1 [3]=1 SYSTEM nr_pods 1 pod_cpus [0]=0000000f pod_node [0]=-1 cpu_pod [0]=0 [1]=0 [2]=0 [3]=0 Worker Pools ============ pool[00] ref= 1 nice= 0 idle/workers= 4/ 4 cpu= 0 pool[01] ref= 1 nice=-20 idle/workers= 2/ 2 cpu= 0 pool[02] ref= 1 nice= 0 idle/workers= 4/ 4 cpu= 1 pool[03] ref= 1 nice=-20 idle/workers= 2/ 2 cpu= 1 pool[04] ref= 1 nice= 0 idle/workers= 4/ 4 cpu= 2 pool[05] ref= 1 nice=-20 idle/workers= 2/ 2 cpu= 2 pool[06] ref= 1 nice= 0 idle/workers= 3/ 3 cpu= 3 pool[07] ref= 1 nice=-20 idle/workers= 2/ 2 cpu= 3 pool[08] ref=42 nice= 0 idle/workers= 6/ 6 cpus=0000000f pool[09] ref=28 nice= 0 idle/workers= 3/ 3 cpus=00000003 pool[10] ref=28 nice= 0 idle/workers= 17/ 17 cpus=0000000c pool[11] ref= 1 nice=-20 idle/workers= 1/ 1 cpus=0000000f pool[12] ref= 2 nice=-20 idle/workers= 1/ 1 cpus=00000003 pool[13] ref= 2 nice=-20 idle/workers= 1/ 1 cpus=0000000c Workqueue CPU -> pool ===================== [ workqueue \ CPU 0 1 2 3 dfl] events percpu 0 2 4 6 events_highpri percpu 1 3 5 7 events_long percpu 0 2 4 6 events_unbound unbound 9 9 10 10 8 events_freezable percpu 0 2 4 6 events_power_efficient percpu 0 2 4 6 events_freezable_pwr_ef percpu 0 2 4 6 rcu_gp percpu 0 2 4 6 rcu_par_gp percpu 0 2 4 6 slub_flushwq percpu 0 2 4 6 netns ordered 8 8 8 8 8 ...从这份输出可以直观读出:CPU/SMT作用域各 4 个 pod、CACHE/NUMA各 2 个 pod、SYSTEM1 个 pod 的分组关系;worker 池一节中cpu=表示绑定池(含 nice=0 普通池与 nice=-20 高优先级池),cpus=表示 unbound 池的 cpumask;最后的映射表则展示了events系列系统 wq 如何按 CPU 或按默认池(dfl列)落到具体 pool 上,其中netns这类 ordered wq 被映射到同一个默认 unbound 池(pool 8)。更详细的说明参见该命令的--help输出。
监控:wq_monitor.py
使用 tools/workqueue/wq_monitor.py 可以监控工作队列的运行状况,它周期性地输出每个工作队列的统计信息:
$ tools/workqueue/wq_monitor.py events total infl CPUtime CPUhog CMW/RPR mayday rescued events 18545 0 6.1 0 5 - - events_highpri 8 0 0.0 0 0 - - events_long 3 0 0.0 0 0 - - events_unbound 38306 0 0.1 - 7 - - events_freezable 0 0 0.0 0 0 - - events_power_efficient 29598 0 0.2 0 0 - - events_freezable_pwr_ef 10 0 0.0 0 0 - - sock_diag_events 0 0 0.0 0 0 - - total infl CPUtime CPUhog CMW/RPR mayday rescued events 18548 0 6.1 0 5 - - ...列含义:total为该 wq 累计处理的工作项总数,infl为当前在途(inflight)工作项数,CPUtime为累计 CPU 时间,CPUhog为 CPU 占用过久的工作项计数,CMW/RPR为并发管理/遣返(repatriation)相关事件计数,mayday为请求 rescue worker 的次数,rescued为被 rescue worker 接管的工作项数。各列的具体口径参见脚本帮助信息。mayday/rescued出现非零值,通常意味着内存压力下工作队列依赖了救援机制。
调试技巧
由于工作函数由通用的 worker 线程执行,定位行为异常的工作队列用户需要一些技巧。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:序号]即绑定在某个 CPU 上的 worker 线程。如果 kworker 占用过多 CPU("发疯"),通常有两类问题:
- 某物在快速连续地被调度(例如有代码在忙循环投递工作项);
- 单个工作项消耗大量 CPU 周期。
针对第 1 类:用 ftrace 跟踪工作项投递事件。workqueue 子系统提供workqueue_queue_work等 tracepoint(定义于 include/trace/events/workqueue.h):
$ echo workqueue:workqueue_queue_work > /sys/kernel/tracing/set_event $ cat /sys/kernel/tracing/trace_pipe > out.txt (wait a few secs) ^C如果确实存在忙循环投递,输出会被其主导,根据 trace 中的工作项函数即可锁定元凶。
针对第 2 类:直接查看肇事 worker 线程的内核栈,工作项函数会清晰地出现在栈回溯中:
$ cat /proc/THE_OFFENDING_KWORKER/stack非重入保证(Non-reentrance Conditions)
workqueue 保证:只要工作项在入队之后满足以下条件,就不会发生重入:
- 工作函数没有被修改;
- 没有人把该工作项投递到另一个工作队列;
- 该工作项没有被重新初始化(reinitiate)。
换言之,满足上述条件时,系统范围内任意时刻最多只有一个 worker 在执行该工作项。需要注意:在工作函数内部把工作项重新投递回同一个队列并不破坏这些条件,因此这种做法是安全的;而破坏上述条件时,在工作函数内必须格外小心。
深入阅读
本文档对应的内核源码内联文档(kernel-doc)是权威的一手参考:
- include/linux/workqueue.h:工作项结构、
INIT_WORK()系列初始化宏、queue_work()系列投递接口、全部WQ_*标志位定义(第 372-421 行)与alloc_workqueue()/alloc_ordered_workqueue()的宏展开与注释; - kernel/workqueue.c:
struct worker_pool(第 195 行)、struct pool_workqueue(第 271 行)、并发管理的核心计数器nr_running、alloc_workqueue_noprof()(第 6041 行)、apply_workqueue_attrs()(第 5610 行)、系统工作队列的创建(第 8224-8240 行)以及default_affinity_scope/cache_shard_size模块参数的注册。
配套的脚本 tools/workqueue/wq_dump.py 与 tools/workqueue/wq_monitor.py 是日常运维、调优与故障排查最直接的上手工具。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考