Linux内核工作队列workqueue深度解析:并发管理与动态线程池优化
1. 工作队列是什么?为什么需要CMWQ?
如果你写过Linux驱动,肯定遇到过这种情况:中断处理函数里不能干太多活儿,否则系统响应会变慢。这时候,我们通常会把耗时的任务“推迟”到中断之外去执行。Linux内核提供了好几种“下半部”机制,其中工作队列(workqueue) 是最常用、也最灵活的一种。
简单来说,工作队列就像一个“任务快递站”。你把一个任务(称为 work,包含要执行的函数)打包好,投递到这个站里。内核里有一群专门的“快递员”(称为 worker,本质是内核线程),他们会从站里取出任务,挨个送货上门(执行函数)。任务执行完了,快递员就休息;新任务来了,快递员又被叫起来干活。
听起来很美好,对吧?但在CMWQ(Concurrency Managed Workqueue,并发管理工作队列)出现之前,这个“快递站”的管理可有点混乱。老版本的工作队列主要有两个大问题:
第一,浪费资源。 很多驱动开发者喜欢自己创建专属的工作队列。系统有多少个CPU,这个队列就创建多少个内核线程。在现在动辄几十核、上百核的服务器上,系统刚启动,可能就把进程号(PID)资源用光了,更别说大量线程占用的内存。
第二,并发性差,容易“堵车”。 老版本的工作队列,每个CPU上的任务是串行执行的。想象一下,如果某个任务在执行过程中“睡着了”(比如等待I/O),那后面排队的任务全都得干等着,哪怕其他CPU核心闲得发慌,也不能过来帮忙。这就像只有一个收银台的超市,前面顾客纠结半天,后面队伍越排越长。
更糟的是,如果多个任务之间有依赖关系,还可能引发死锁。
为了解决这些痛点,内核开发者Tejun Heo在2.6.36左右引入了CMWQ。它的核心思想是“解耦”和“池化”:
- 解耦:把用户看到的工作队列(前端接口)和实际干活的内核线程池(后端实现)分开。
- 池化:创建一些全局共享的线程池(
worker_pool),所有工作队列都来“租用”这些池子里的线程,而不是自己养一大群。
CMWQ就像一家高效的物流公司,它建了几个大型的“配送中心”(线程池),所有商家的订单(work)都汇总到这里。配送中心根据订单量、配送员(worker)的忙闲情况,动态地调整配送员数量。订单少时就留几个人值班,订单暴增时就临时招聘,配送员空闲太久就辞退。这样既保证了配送效率,又不会养一堆闲人浪费资源。
所以,CMWQ的设计目标很明确:保持API兼容,用共享线程池按需提供灵活的并发能力,并且自动管理,让开发者不用操心线程细节。
2. CMWQ的核心数据结构:一张关系网
要理解CMWQ怎么工作,得先认识几个关键的数据结构。它们之间的关系,我画了张图帮你理解(想象这是一家物流公司的组织架构图):
[用户视角] workqueue_struct (工作队列,如“顺丰快递”)
|
| 通过 pool_workqueue 连接
v
[系统资源] worker_pool (线程池,如“华北配送中心”)
|
| 管理多个
v
worker (工作者线程,如“配送员张三”)
|
| 执行
v
work_struct (工作任务,如“订单A”)
下面我们来拆解每个角色:
2.1 工作任务:work_struct
这是最小的工作单元,就像一张“订单”。
struct work_struct {
atomic_long_t data; // 重要!低比特存状态,高比特存池子ID或pwq指针
struct list_head entry; // 链表节点,用于挂到队列里
work_func_t func; // 任务处理函数,就是“订单”的具体内容
};
data 字段是个多面手,它用不同的比特位记录了work的状态和历史。比如,WORK_STRUCT_PENDING 位表示订单已下单但还没处理;WORK_STRUCT_PWQ 位表示高比特存的是 pool_workqueue 的指针。这个设计非常巧妙,用一块内存存了多种信息。
2.2 工作者线程:worker
这就是干活的“配送员”,每个worker对应一个内核线程(名字通常是 kworker/uXX:YY 或 kworker/XX:YY)。
struct worker {
struct task_struct *task; // 对应的内核线程结构
struct worker_pool *pool; // 属于哪个线程池
struct work_struct *current_work; // 当前正在送哪个“订单”
work_func_t current_func; // 当前订单的处理函数
struct list_head entry; // 空闲时,挂到池子的空闲链表
struct hlist_node hentry; // 忙碌时,挂到池子的忙碌哈希表
struct list_head scheduled; // 专门派给本线程的订单链表
};
worker有两种状态:idle(空闲,在休息室待命)和 busy(忙碌,正在送货)。池子根据状态把它们放在不同的列表里管理。
2.3 线程池:worker_pool
这是“配送中心”,管理者一群worker。CMWQ创建了两种池子:
- Bound型(绑定CPU):每个CPU都有两个,一个普通优先级(nice=0),一个高优先级(nice=-20)。它们只处理绑定到本CPU的任务,利用CPU缓存局部性提升性能。
- Unbound型(不绑定CPU):可以在多个CPU间迁移,由系统调度器决定在哪运行。适合长时间运行或并发需求波动大的任务。
struct worker_pool {
int cpu; // 绑定的CPU号,-1表示unbound
int id; // 池子的唯一ID
struct list_head worklist; // 等待处理的订单链表(公共队列)
int nr_workers; // 手下有多少个配送员
int nr_idle; // 有多少个在休息
struct list_head idle_list; // 休息的配送员链表
DECLARE_HASHTABLE(busy_hash, ...); // 正在忙的配送员哈希表
struct list_head workers; // 所有配送员(不管忙闲)都在这
atomic_t nr_running; // 正在路上跑的配送员数量(关键!)
};
nr_running 这个计数器是并发管理的核心,后面会讲到。
2.4 连接器:pool_workqueue (pwq)
这是工作队列和线程池之间的“中介”或“合同”。一个工作队列(比如你自己创建的)通过多个pwq连接到不同的线程池上(如果是bound类型,就每个CPU一个pwq)。
struct pool_workqueue {
struct workqueue_struct *wq; // 属于哪个工作队列(哪家快递公司)
struct worker_pool *pool; // 连接到哪个线程池(哪个配送中心)
int nr_active; // 当前活跃的订单数
int max_active; // 最大允许的活跃订单数(并发度)
struct list_head delayed_works; // 因为超过max_active而延迟的订单
};
max_active 是个重要参数,它控制着每个CPU上、这个工作队列最多能同时执行多少个任务。默认是256,如果你希望任务严格串行执行,可以设为1。
2.5 工作队列:workqueue_struct
这是用户直接打交道的对象,代表一个“快递品牌”。
struct workqueue_struct {
char name[WQ_NAME_LEN]; // 队列名字,比如“my_driver_wq”
unsigned int flags; // 标志位,决定队列行为
struct list_head pwqs; // 它所有的“合同”(pwq)都链在这里
struct pool_workqueue __percpu *cpu_pwqs; // Per-CPU的pwq指针(bound类型用)
struct pool_workqueue **numa_pwq_tbl; // 按NUMA节点存的pwq(unbound类型用)
struct worker *rescuer; // “救援线程”,内存紧张时备用
};
系统已经默认创建了几个全局工作队列,比如 system_wq (events)、system_highpri_wq,大多数情况下直接用它们就够了,不用自己创建。
3. 动态线程池管理:CMWQ的智能大脑
CMWQ最精髓的部分,就是 worker_pool 如何智能地管理 worker 的数量,实现“要多少有多少,不多也不少”的理想状态。它本质上实现了一个动态的状态机。
3.1 worker的三种状态与转换规则
- IDLE(空闲):worker没事干,在
pool->idle_list上睡觉。对应内核线程处于TASK_INTERRUPTIBLE状态。 - RUNNING(运行):worker正在执行
work->func。它会被记录在pool->busy_hash中,并且pool->nr_running计数器加1。 - SUSPEND(睡眠):worker在执行任务的过程中主动睡眠了(比如调用了
schedule())。此时它不在运行,但任务还没完。
管理规则非常清晰:
- 规则一:只要
pool->worklist里有活(有待处理work),池子里至少得有一个RUNNING状态的worker在干活。 - 规则二:如果一个
RUNNING的worker在送货途中“睡着”了(进入SUSPEND),为了保证其他订单不被耽误,池子会立刻唤醒一个IDLE的worker,或者创建一个新的worker来接班。 - 规则三:如果活很多,但
RUNNING的worker数量大于1,CMWQ会让多余的worker在干完当前活后进入IDLE状态,避免过度竞争。 - 规则四:如果没活了,所有worker都会进入
IDLE状态。 - 规则五:如果一个worker在
IDLE状态待了太久(默认300秒),它会被销毁,释放资源。
3.2 并发水平如何感知?挂钩调度器!
关键问题来了:worker_pool 怎么知道一个worker是 RUNNING 还是 SUSPEND 了呢?难道要不停地去问每个线程“你睡着了吗”?
CMWQ用了一个很巧妙的办法:挂钩到进程调度器。当内核进行任务切换时,会调用 __schedule() 函数。CMWQ在这里安插了“眼线”:
wq_worker_waking_up():当一个worker被唤醒、即将投入运行时调用。这里会给pool->nr_running加1。wq_worker_sleeping():当一个worker即将睡眠时调用。这里会给pool->nr_running减1。更重要的是,如果减完之后发现nr_running变成了0(没有worker在跑了),但worklist里还有活,它就会立刻唤醒一个IDLEworker!
// 简化逻辑
if (atomic_dec_and_test(&pool->nr_running) && !list_empty(&pool->worklist)) {
// 没人跑活了,但还有订单!赶紧叫醒一个休息的。
wake_up_worker(pool);
}
这就完美实现了规则一和规则二:永远保证有活干的时候,至少有一个跑腿的。
3.3 应对CPU密集型任务:WQ_CPU_INTENSIVE
如果一个work是CPU密集型的(比如大量计算),它虽然不会睡眠,但会长时间霸占CPU。这会导致同一个池子里的其他work被“饿死”。为了解决这个问题,CMWQ提供了 WQ_CPU_INTENSIVE 标志。
当给工作队列设置了这个标志,它里面的work在执行时,会被暂时“踢出”并发管理。具体来说,在 process_one_work() 函数里:
if (unlikely(pwq->wq->flags & WQ_CPU_INTENSIVE)) {
worker_set_flags(worker, WORKER_CPU_INTENSIVE); // 设置标志
// 这个操作内部会 atomic_dec(&pool->nr_running)
}
看,它把当前worker的 nr_running 计数给减了!对 worker_pool 来说,这个worker就好像“睡着”了一样。于是,规则二触发,池子会创建或唤醒新的worker来执行其他排队的work。这样就实现了CPU密集型任务和非密集型任务的并发执行,而不是串行等待。
任务执行完毕后,会清除 WORKER_CPU_INTENSIVE 标志,nr_running 计数恢复,worker重新纳入池子的常规管理。
3.4 动态创建与销毁worker
谁来负责创建新worker?是 manage_workers() 函数。当 worker_thread() 发现活多但人少(!may_start_working(pool))时,就会调用它。
创建过程有个“经理锁”(pool->manager_arb)保护,确保同一时间只有一个线程在创建worker。在 maybe_create_worker() 函数里,它会循环调用 create_worker(),直到worker数量足够,或者创建失败。
销毁则是由一个定时器 idle_timer 触发的。在 init_worker_pool 时,就设置了超时回调 idle_worker_timeout(默认300秒)。定时器到期后,会检查空闲worker是否太多:
static bool too_many_workers(struct worker_pool *pool) {
int nr_idle = pool->nr_idle + managing; // 空闲数
int nr_busy = pool->nr_workers - nr_idle; // 忙碌数
// 如果空闲>2 且 (空闲数-2)*4 >= 忙碌数,则认为“太多”
return nr_idle > 2 && (nr_idle - 2) * MAX_IDLE_WORKERS_RATIO >= nr_busy;
}
如果判定为太多,就会从 idle_list 尾部取出空闲最久的worker,给它打上 WORKER_DIE 标志,然后唤醒它。这个worker被唤醒后,在 worker_thread() 开头看到这个标志,就会清理资源,结束线程,完成自我销毁。
4. 工作队列的使用与实战技巧
理解了原理,用起来就简单了。通常你只需要和几个API打交道:
4.1 创建与初始化
使用系统默认队列(推荐):
// 静态声明并初始化一个work
static DECLARE_WORK(my_work, my_work_function);
// 或者动态初始化
INIT_WORK(&my_work, my_work_function);
系统队列 system_wq 已经够用,除非你有特殊需求(比如严格串行、高优先级、CPU密集等)。
创建自定义队列:
struct workqueue_struct *my_wq = alloc_workqueue("my_wq", WQ_UNBOUND, 0);
flags 是关键:
WQ_UNBOUND: 不绑定CPU,任务可能在任何CPU执行,适合长时间运行或避免CPU绑定的任务。WQ_HIGHPRI: 高优先级,worker的nice值为-20。WQ_CPU_INTENSIVE: 声明为CPU密集型,不影响并发管理。WQ_MEM_RECLAIM: 如果你的work可能在内存回收路径中被调用,必须设置这个!它会创建一个“救援线程”,防止内存紧张时创建worker失败导致死锁。WQ_FREEZABLE: 参与系统休眠冻结流程。
max_active 通常设为0(用默认值256)即可,除非你需要严格限制并发数。
4.2 调度与执行
提交任务:
// 提交到系统默认队列
schedule_work(&my_work);
// 提交到指定CPU的系统队列
schedule_work_on(cpu_id, &my_work);
// 提交到自己创建的队列
queue_work(my_wq, &my_work);
// 提交延迟任务(使用jiffies)
schedule_delayed_work(&my_delayed_work, HZ); // 延迟1秒
schedule_work() 内部会调用 queue_work_on(),其核心是 __queue_work()。这个函数会:
- 根据workqueue类型(bound/unbound)找到对应的
pool_workqueue。 - 检查work是否正在同一个CPU的其他worker上执行(利用
data字段的历史信息),如果是,则把work迁移到那个worker的私有scheduled链表,利用缓存亲和性。 - 判断
pwq->nr_active是否小于pwq->max_active,是则加入pool->worklist,否则加入pwq->delayed_works(延迟执行队列)。 - 调用
insert_work(),设置work的PENDING标志,并将其链入列表。 - 如果此时池子里没有
RUNNING的worker(__need_more_worker(pool)),就唤醒一个IDLEworker。
任务执行流:
worker线程 worker_thread() 被唤醒后,在一个大循环里:
- 从
pool->worklist取出一个work。 - 调用
process_one_work()。 - 在这个函数里,先检查是否有冲突(同一个work是否正在其他worker上跑)。
- 将worker加入
busy_hash,设置当前执行的work信息。 - 关键一步:
set_work_pool_and_clear_pending(work, pool->id)。这里清除了PENDING标志,并把当前worker_pool的ID写入work->data的高比特位。从此,这个work处于“执行中”状态。 - 释放锁,执行
work->func。 - 执行完毕,重新获取锁,清理状态,将worker从
busy_hash移除。
4.3 清理与同步
等待任务完成:
// 等待一个特定work完成(同步)
cancel_work_sync(&my_work);
// 刷新一个工作队列,等待所有已提交任务完成
flush_workqueue(my_wq);
cancel_work_sync() 是个同步操作,如果work已经在执行,它会等待其执行完毕。它的实现也很有趣:通过插入一个特殊的“屏障work”(wq_barrier)到目标work后面,当屏障work被执行时,说明前面的work肯定执行完了。
销毁队列:
destroy_workqueue(my_wq);
4.4 实战踩坑经验
- 内存回收路径:如果你的驱动模块的work可能会在内存紧张、触发回收的路径里被调用,创建队列时务必加上
WQ_MEM_RECLAIM标志。我吃过亏,有一次没加,在内存压力测试时系统偶尔会卡死,排查了好久才发现是workqueue创建worker失败,又没有救援线程,导致死锁。 - CPU密集型任务:如果你的work里主要是计算,很少睡眠,记得用
WQ_CPU_INTENSIVE。否则它会阻塞同池子里的其他work,即使其他CPU有空闲。实测过一个图像处理驱动,加上这个标志后,整体吞吐量提升了约30%。 - 不要滥用自定义队列:除非需要特殊的优先级、并发控制或排序语义,否则优先使用系统默认队列
system_wq。系统队列经过充分优化,而且共享池子效率更高。 - 延迟工作:
schedule_delayed_work()依赖于内核的定时器。在系统休眠(suspend)时,定时器可能会暂停,导致延迟时间变长。如果你的驱动对时间敏感,需要考虑这一点。 - 调试技巧:如果发现
kworker线程CPU占用很高,可以用ftrace追踪:
看看是哪个work在频繁排队。也可以查看具体worker线程的栈:echo workqueue:workqueue_queue_work > /sys/kernel/debug/tracing/set_event cat /sys/kernel/debug/tracing/trace_pipecat /proc/<kworker_pid>/stack
5. 性能调优与高级话题
5.1 负载均衡与亲和性
对于 unbound 工作队列,CMWQ提供了亲和性作用域(Affinity Scope) 的概念,这是Linux 6.5之后引入的增强特性。你可以通过 apply_workqueue_attrs() 来配置,或者通过sysfs调节(如果队列有 WQ_SYSFS 标志)。
亲和性作用域决定了unbound worker可以在哪些CPU范围内被调度:
cpu:每个CPU独立,类似per-cpu但无并发管理。smt:同一个物理核的超线程之间。cache(默认):共享最后一级缓存(LLC)的CPU之间。这是平衡局部性和利用率的最佳折衷,也是默认选项。numa:同一个NUMA节点内。system:所有CPU。
还有一个 affinity_strict 开关。如果设为0(默认),调度器在worker启动后可以将其迁移到任何CPU;如果设为1,则worker严格限制在亲和性作用域内。后者能保证隔离性,但可能降低整体CPU利用率。
选择哪种?根据你的负载:
- 工作负载均匀分布在多个发起者(线程):用默认的
cache就行,性能最好。 - 工作负载来自少数固定线程:可能需要用
system作用域来避免局部CPU过载,或者仔细调整。 - 需要模拟旧版内核行为:使用严格的
numa作用域。
5.2 监控与调试工具
内核提供了强大的调试工具,位于 tools/workqueue/ 目录下:
-
wq_dump.py:一个drgn脚本,可以详细打印系统内所有workqueue、worker_pool的配置和映射关系。一目了然地看到哪个队列在用哪个池子,池子里有多少worker。sudo tools/workqueue/wq_dump.py -
wq_monitor.py:实时监控工作队列的运行状态,包括排队任务总数、正在执行的数量、总CPU耗时等。sudo tools/workqueue/wq_monitor.py events这对于发现哪个队列是性能瓶颈非常有用。
5.3 与其它下半部机制对比
最后,简单对比一下工作队列和其他延迟执行机制,方便你选型:
| 机制 | 执行上下文 | 可否睡眠 | 并发性 | 优先级 | 适用场景 |
|---|---|---|---|---|---|
| Softirq | 中断上下文 | 否 | 同一CPU上串行 | 最高 | 网络、块设备等高性能路径 |
| Tasklet | 软中断上下文 | 否 | 同一CPU上串行,不同CPU可并行 | 高 | 需要简单串行保障的中断下半部 |
| Workqueue (CMWQ) | 进程上下文 | 可以 | 动态并发管理 | 可调(普通/高优先级) | 大多数需要推迟执行的耗时任务,尤其是可能睡眠的操作 |
| IRQ Work | 中断上下文(通过IPI) | 否 | 严格按序 | 很高 | 需要在中断上下文执行但不想用softirq的极轻量任务 |
简单总结:需要睡眠、或者任务比较耗时、或者希望利用多核并发,就用工作队列。 对性能极其敏感、且绝对不能睡眠的,用softirq或tasklet。
CMWQ这套机制从引入至今,一直是Linux内核异步任务处理的基石。它的设计非常优雅,通过解耦、池化和动态管理,在资源消耗和并发性能之间取得了出色的平衡。理解它之后,你不仅能更高效地使用工作队列,也能从中学习到一种优秀的内核并发资源管理模型。下次当你提交一个work时,可以想象一下背后那个智能的物流系统正在为你高效地调度配送资源。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)