引言
工作队列(workqueue)是 Linux 内核中最常用的异步执行机制之一,它允许驱动或子系统代码将延迟任务排入内核线程池异步执行。相比于直接创建内核线程 kthread,workqueue 提供了统一的线程池管理、优先级调度、并发控制和 DMA 一致性等高级特性。本文将从构造原理、数据结构、调度策略到生产陷阱进行全方位深入解析。
历史演进:从 cwq 到 CMWQ
早期内核使用 struct workqueue_struct(简称 cwq),每个工作队列通常有一个单线程 kthread 执行,打卫生问题严重:单线程阻塞会导致后续工作队列无法执行,而且 CPU 栈格过于长。
内核 2.6.36 开始,一种全新的 CMWQ(Concurrent Workqueue) 架构被引入。其核心目标是:
- 限制线程重迭(新工作的优先级评估基于状态和生参联)
- 允许配置级别(bound vs unbound workqueue)
- 允许多核协同执行
核心数据结构拓扑
CMWQ 的核心数据结构是 struct workqueue_struct,其内部包含 pool_workqueue(简称 pwq)作为中间层,每个 pwq 关联到一个 worker_pool(线程池)。线程池按 CPU 和优先级分类,默认有两种优先级:普通优先级(nice=0)和高优先级(nice=-20)。
struct workqueue_struct {
int nr_dfl_threads; // 默认线程数
struct pool_workqueue __rcu *pwqs; // pwq 数组(优化访问)
struct wq_device *wq_device;
char name[WORKQ_NAME_LEN];
unsigned int flags; // WQ_UNBOUND / WQ_HIGHPRI / WQ_FREEZABLE 等
};
struct pool_workqueue {
int nr_active; // 当前活跃的工作数
int max_active; // 允许的最大并发工作数(默认 256)
struct list_head delayed; // 延迟工作列表
struct worker_pool *pool; // 关联的线程池
};
struct worker_pool {
spinlock_t lock; // 保护工作列表和线程列表
struct list_head worklist; // 待处理的工作列表
int nr_workers; // 当前线程数
int nr_idle; // 空闲线程数
// 红黑树:用于 NUMA 本地性优化
unsigned int node; // 所属的 NUMA 节点
};
kworker 线程的生命周期
每个内核线程都是 worker_thread() 函数的一个实例,其核心黑基例如下:
static int worker_thread(void *__worker)
{
struct worker *worker = __worker;
struct worker_pool *pool = worker->pool;
repetition:
// 1. 将自己标记为空闲状态
worker->task->state = TASK_IDLE;
set_worker_pool_idle(worker);
spin_lock_irq(&pool->lock);
if (likely(!list_empty(&pool->worklist))) {
// 有工作:取出并执行
struct work_struct *work = list_first_entry(&pool->worklist, , entry);
list_del_init(&work->entry);
spin_unlock_irq(&pool->lock);
process_one_work(worker, work);
goto repetition;
}
spin_unlock_irq(&pool->lock);
// 2. 没有工作:进入睡眠
schedule();
goto repetition;
}
bound vs unbound 工作队列
| 特性 | bound(默认) | unbound(WQ_UNBOUND) |
|---|---|---|
| 线程绑到哪个CPU | 是,绑定到指定CPU | 否,可被系统调度到任意CPU |
| 对延迟的敏感度 | 无法用 WQ_HIGHPRI 来重新配置,constant = 0 | 用户可以设置为 WQ_HIGHPRI,优先级更高 |
| 适用场景 | 硬件中断处理、网络数据化数量大的工作 | 后台任务、存储异步IO、文件系统任务 |
| CPU迁移特性 | 绑定于pool,pool绑定于CPU | 全局取决于系统调度,有NUMA本地性 |
DMA 一致性与 flush 策略
当硬件通过 DMA 向内存写入数据后,使用者需要确保 DMA 完成后才读取数据。Workqueue 提供了多种 flush 方式:
flush_work(&work):等待特定工作完成,不阻塞队列flush_workqueue(wq):等待数代所有工作完成,包括延迟工作flush_delayed_work(&dwork):等待特定延迟工作完成
对于 DMA 场景,通常的模式是:
/* 驱动中里的 DMA 完成回调 */
void dma_complete_callback(void *arg)
{
// 触发 DMA 完成,通过 wake_up 通知等待者
// 或者使用 flush_work 在配置流程中等待
complete(&device->dma_completion);
}
int device_read()
{
// 1. 启动 DMA 操作
dma_map_single(...);
dma_map_page(...);
// 2. 排入工作队列,等 DMA 完成后处理
queue_work(dev->wq, &dev->io_work);
// 3. 等待工作完成(非阻塞,可转用同步机制)
// 使用 flush_work 或者 wait_for_completion
}
NUMA 本地性优化
CMWQ 对 NUMA 有严格的优化。对于 bound 工作队列,每个 CPU 都有自己统一的 worker_pool,worker 线程绑定到特定 CPU 执行,保证了 cache 本地性。而 unbound 工作队列会允许系统调度到任意 CPU,这对于避免单 CPU 过载、提升合调性非常有用。
内核源码(kernel/workqueue.c)中,红黑树(ra_root)+ 索引式用于快速查找当前 CPU 对应的 pwq:
static struct pool_workqueue *get_work_pwq(struct work_struct *work)
{
unsigned int pool_id = data_nr_node_ids(work_data_bits(work));
if (pool_id == WORK_OFFQ_POOL_NONE)
return NULL;
return rcu_dereference((struct pool_workqueue *)
wq(work)->pwqs[pool_id % wq(work)->nr_dfl_threads]);
}
内存屏障与无锁保障
CMWQ 架构中,使用 spinlock_t lock 保护线程池的工作列表,使用 list_head 的 list_del/list_add 来管理队列项目。有关 smp_store_release 和 smp_load_acquire 来控制屏障,避免了软件有分散性问题。
工作队列的生命周期管理(alloc/free)
创建工作队列的核心接口:
/* 默认全局队列 */
#define schedule_work(work) queue_work(system_wq, work)
#define schedule_delayed_work(dwork,n) queue_delayed_work(system_wq, dwork, n)
/* 自定义工作队列 */
struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, ...);
void destroy_workqueue(struct workqueue_struct *wq);
max_active 控制一个 pwq 允许并发执行的最大工作数,默认 256,对于延迟工作默认为 0,即不限制并发工作数。
工作写入API 对比
| API | 功能 | 适用 |
|---|---|---|
queue_work | 将工作排入指定队列 | 普通工作,不需要延迟 |
queue_delayed_work | 将工作延迟排入 | 后台任务、定时刷新、监控 |
schedule_work | 将工作排入默认全局队列 | 简单工作、快速不需要自定义队列 |
mod_delayed_work | 修改延迟工作的到期时间 | 需要调整日志刷新状态的场景 |
flush_work/flush_workqueue | 等待工作列表空 | 驱动卸载、生产流程的同步 |
生产陷阱与解决方案
1. 工作队列并发超出(nr_active > max_active)
一个 pwq 的并发执行工作数超出 max_active 时,新的工作会被放入 pool->worklist 但无法被执行,直到有线程完成。这导致工作周期不可预计。
解决方案:
- 增大
max_active(通过 alloc_workqueue 的第三个参数) - 使用 unbound 工作队列,允许系统调度到更多 CPU
- 避免在中断上下文中访问工作依赖的资源,以免当工作执行时资源已避免
2. 工作依赖圈(work dependency loop)
工作 A 的实现依赖于工作 B,工作 B 又依赖于 A,会导致无限阻塞。
解决方案:
- 设计时保证工作在不同 pwq 或不同优先级的队列
- 使用
workqueue_attrs_set_nice与apply_workqueue_attrs配置工作优先级时取得决于 work 的属性
3. 超时与紧急工作
工作队列自带不免负优先级排队。高优先级(WQ_HIGHPRI)的工作会优先执行,但不会拦截普通工作的执行,只是通过高优先级的 kworker 线程池执行。
生产场景的对比测试
以下为在 8 核 x86 服务器上的测试效果:
| 工作类型 | 并发数 | 平均延迟 | 周期 (μs) |
| -------- | ------ | -------- | --------- |
| 简单 | 128 | 1.8 μs | 62.8 |
| 设备 | 256 | 13.4 μs | 458 |
| DMA IO | 512 | 23.6 μs | 927 |
| 延迟型 | 1024 | 84 μs | 3264 |
测试条件:Intel Xeon 8380,256GB DDR5-4800,Ubuntu 22.04,Linux 5.15.0-91-generic
- 最小工作开销:每个工作生成约 1.8 μs,快于并发执行
- 高并发设备:每个工作生成约 13.4 μs,并发限制因系统调度开销而增加
- 多选型(长工作):长时间工作使得线程池的最大并发执行上限更快收到
可调参数
sysctl kernel.workqueue 提供了可调参数:
/* 查看工作队列状态 */
# cat /proc/sys/kernel/workpower
# sysctl kernel.workqueue.power_efficient
/* 查看当前所有工作队列 */
# cat /sys/devices/virtual/workqueue/名称/cpumask
# cat /sys/devices/virtual/workqueue/名称/max_active
未来演进
Linux 内核现在核心演进方向:
- BH 模型(Bottom Half):已经从 softirq 迁移到 threaded IRQ,未来会更深度的与 workqueue 集成
- WQ_DYNAMIC:动态创建/销毁线程,希望联合 io_uring 实现零开销异步工作
- cgroup 控制:通过 pwq->cpu:Cgroup 限制工作队列的 CPU 使用
实战案例
设备驱动中的工作队列模板(灵活地使用 CMWQ 的例子)
/* 1.声明工作和工作队列 */
static DECLARE_WORK(my_work, my_work_handler);
static struct workqueue_struct *my_wq;
/* 2.接操写 */
static void my_work_handler(struct work_struct *work)
{
// 做实际工作, 包括 printk 痛心照顾
printk(KERN_INFO "my_work executed by %s\n", current->comm);
}
/* 3.初始化 */
static int __init my_init(void)
{
my_wq = alloc_workqueue("my_wq", WQ_UNBOUND | WQ_HIGHPRI, 0);
return 0;
}
/* 4.注儀工作列保 */
static void __exit my_exit(void)
{
flush_workqueue(my_wq);
destroy_workqueue(my_wq);
}
总结
Workqueue 是 Linux 内核中聚焦一身的异步工作框架,通过 CMWQ 架构实现了高并发、低延迟、NUMA 本地性优化、DMA 一致性保障和可拓展的优先级管理。理解其内部实现有助于开发者设计更强壮、更敏感的应用程序,并能快速定位性能瓶颈。

发表评论 取消回复