3. 内核工作队列架构演进:从单线程 workqueue 到 CMWQ
Linux 内核的工作队列(workqueue)机制经历了从简单到复杂、从低效到高并发的重大架构变迁。理解这一演进过程,是掌握现代 workqueue 使用与调优的基础。本章将系统梳理从 传统单线程 workqueue 到 CMWQ(Concurrency Managed Workqueue) 的设计动机、核心差异与架构变革。
3.1 传统 workqueue:单线程与 per-CPU 内核线程
在早期的 Linux 内核(2.x 系列)中,workqueue 的实现非常朴素:每个 workqueue 对应一个或多个内核线程,用于串行化执行挂入队列的工作项。
3.1.1 单线程 workqueue(keventd)
最初的内核只提供一个全局的 workqueue——keventd。所有驱动程序、子系统都向同一个内核线程提交工作项。这种设计存在明显的瓶颈:
- 串行化执行:所有工作项在同一个线程中排队执行,一个耗时工作会阻塞后续所有工作。
- 无并发能力:无法利用多核 CPU 并行处理工作项。
- 优先级反转:紧急工作必须等待前面的一般工作完成。
3.1.2 per-CPU workqueue(多线程但固定绑定)
为解决单线程瓶颈,内核引入了 per-CPU workqueue。每个 CPU 拥有一个独立的内核线程(例如 events/0、events/1),工作项默认绑定到提交时所在的 CPU 上执行。
| 特性 | 单线程 workqueue | per-CPU workqueue |
|---|---|---|
| 内核线程数 | 1 个(keventd) | N 个(每个 CPU 一个) |
| 并发能力 | 无 | 多 CPU 并行 |
| 工作项迁移 | 不适用 | 固定绑定,不能跨 CPU 迁移 |
| CPU 利用率 | 单核满载,其他核空闲 | 可分散负载,但存在 CPU 间不平衡 |
| 适用场景 | 简单、低频任务 | 中等并发、CPU 亲和性要求高 |
3.2 传统 workqueue 的架构缺陷
尽管 per-CPU workqueue 提升了并发度,但其设计存在几个难以克服的缺陷:
- 线程数量固定:每个 CPU 固定一个内核线程,无法根据工作负载动态增减。
- CPU 绑定僵化:工作项一旦提交到某个 CPU,即使该 CPU 繁忙、其他 CPU 空闲,也不会迁移。
- 资源浪费:即使系统没有工作项,每个 CPU 的内核线程仍然存在,占用栈空间和调度实体。
- 死锁风险:当工作项依赖另一个 CPU 上的工作项完成时,可能形成跨 CPU 的循环等待。
3.3 CMWQ 架构设计:并发管理的工作队列
Linux 3.x 系列引入了 Concurrency Managed Workqueue(CMWQ),由 Tejun Heo 主导设计。CMWQ 的核心思想是:将工作队列与内核线程池解耦,由系统统一管理并发度。
3.3.1 核心组件
- workqueue(工作队列):用户可见的队列接口,不再直接绑定内核线程。
- worker pool(工作者线程池):系统维护的线程池,分为 per-CPU pool 和 unbound pool。
- worker(工作者线程):实际执行工作项的内核线程,由 pool 动态创建和销毁。
- work item(工作项):提交到 workqueue 的具体任务。
3.4 CMWQ 的关键设计创新
3.4.1 动态线程管理
CMWQ 不再为每个 workqueue 固定分配线程,而是由系统维护一组 worker pool。当工作项增多时,pool 自动创建新的 worker 线程;当工作项减少时,空闲 worker 会被销毁。这种机制避免了资源浪费。
3.4.2 并发度自动调节
CMWQ 引入 concurrency level 概念,默认每个 CPU 的 worker pool 并发度等于该 CPU 上的活跃工作项数量。当工作项阻塞(如等待 I/O)时,pool 会创建额外的 worker 来保持 CPU 利用率。
3.4.3 工作项迁移能力
对于 unbound workqueue,工作项可以在不同 CPU 之间迁移,实现负载均衡。系统根据 CPU 负载情况动态调度工作项到最空闲的 CPU 上执行。
| 对比维度 | 传统 per-CPU workqueue | CMWQ |
|---|---|---|
| 线程管理 | 静态固定,每个 CPU 一个线程 | 动态创建/销毁,按需分配 |
| 并发控制 | 无,线程数固定 | 自动调节,基于工作项阻塞状态 |
| CPU 绑定 | 严格绑定,不可迁移 | per-CPU 绑定 / unbound 可迁移 |
| 资源开销 | 空闲时仍占用线程资源 | 空闲时销毁线程,节省资源 |
| 死锁风险 | 跨 CPU 依赖容易死锁 | 通过并发管理降低死锁概率 |
| API 兼容性 | 旧 API(create_workqueue) | 新 API(alloc_workqueue) |
3.5 CMWQ 的两种 worker pool 类型
CMWQ 将 worker pool 分为两大类,以适应不同的工作负载需求:
3.5.1 per-CPU worker pool
- 绑定到特定 CPU:每个 CPU 拥有独立的 worker pool。
- 适用场景:对 CPU 亲和性要求高、缓存局部性敏感的工作。
- 默认行为:大多数 workqueue 默认使用 per-CPU pool。
3.5.2 unbound worker pool
- 不绑定特定 CPU:工作项可以在任意 CPU 上执行。
- 适用场景:I/O 密集型、长时间运行、需要负载均衡的工作。
- 配置灵活:可以通过
workqueue_attrs设置 CPU 亲和性掩码。
3.6 CMWQ 的 API 变迁
CMWQ 引入了全新的 API 来创建和管理 workqueue,旧 API 被标记为 deprecated:
/* 旧 API(传统 workqueue)—— 不推荐在新代码中使用 */
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
/* 新 API(CMWQ)—— 当前标准 */
struct workqueue_struct *alloc_workqueue(const char *fmt,
unsigned int flags,
int max_active, ...);
/* 常用 flags */
#define WQ_UNBOUND 0x0002 /* 使用 unbound worker pool */
#define WQ_MEM_RECLAIM 0x0008 /* 在内存回收路径中安全使用 */
#define WQ_HIGHPRI 0x0010 /* 高优先级 worker pool */
#define WQ_SYSFS 0x0020 /* 通过 sysfs 导出参数 */
3.7 演进总结:从静态到动态的飞跃
workqueue 架构的演进本质上是 从静态资源分配向动态并发管理 的转变。传统 workqueue 的设计假设“每个 CPU 一个线程就足够”,而 CMWQ 认识到工作负载是动态变化的,需要系统级的管理和调度。