引言

工作队列(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 一致性保障和可拓展的优先级管理。理解其内部实现有助于开发者设计更强壮、更敏感的应用程序,并能快速定位性能瓶颈。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部