引言:内核异步执行的基石

在 Linux 内核中,中断处理的后半夜(bottom half)、定时器回调、块设备异步 I/O 完成、各种 defer 任务——所有这些都需要一个安全、可控、可管理的异步执行机制。Workqueue(工作队列)正是 Linux 内核中最核心、最广泛使用的延迟执行框架。几乎所有驱动子系统、文件系统、网络协议栈都依赖 workqueue 将非紧急工作延后到进程上下文中执行。本文将全面解析 workqueue 的设计演进历史、CMWQ 架构革命、内核线程模型、并发控制机制、各类接口使用、调试调优方法,以及生产环境中的最佳实践和陷阱规避。

一、Workqueue 演进历史:从任务队列到 CMWq

1.1 经典 TaskQueue(Linux 2.4 时代)

Linux 2.4 内核使用 TaskQueue 作为 bottom-half 机制。TaskQueue 是全局的 BH(Bottom Half)链表,每个 BH 通过静态编号注册(如 TIMER_BH、TQUEUE_BH)。这种设计有三个致命缺陷:全局串行化——所有 BH 是全局串行执行的,一个 BH 执行时其他 BH 即使在不同 CPU 上也不能运行;静态注册——预定义的 BH 编号数量有限,无法动态扩展;无优先级——所有 BH 平等调度,无法区分工作紧急程度。

1.2 Work Queue(Linux 2.5/2.6 早期)

Linux 2.5 引入了替代方案:Work Queue。每个 work queue 关联一个内核线程(名为 events/n,n 为 CPU 编号)。驱动程序可以将自己的 work 挂载到全局共享的 events work queue 上,work 在 events/n 线程的进程上下文中被执行。这个设计解决了 BH 的全局串行问题,但引入了新的瓶颈:所有驱动共享同名 events 线程。当某个驱动的 work 函数执行耗时操作(如磁盘同步、硬件初始化)时,同一 CPU 上的其他驱动的 work 必须排队等待,造成严重的跨驱动优先级反转。在桌面和服务器上经常表现为:网络驱动的 NAPI 收包 work 被块设备驱动的同步操作饿死,导致网络延迟飙升。

1.3 CMWq 革命(Linux 2.6.36+)

Tejun Heo 在 Linux 2.6.36(2010 年 10 月)提交的 CMWq(Concurrency-Managed Workqueue)补丁彻底重构了 workqueue 子系统。CMWq 的核心思想是:不再为每个 CPU 创建独立的 workqueue 线程,而是通过一套自管理的线程池动态调度。CMWq 有两个内置工作队列:system_wq(普通优先级)和 system_high_wq(高优先级)。驱动开发者还可以使用 alloc_workqueue() 创建自定义工作队列。CMWq 线程池根据工作队列的忙碌程度自动扩缩线程数量,不再需要为每个 CPU 静态分配线程,大幅降低了内存占用(尤其是 NUMA 系统中 CPU 数量庞大的场景)。

二、CMWq 架构深度解析

2.1 核心数据结构

CMWq 的核心数据结构由三层构成:

  • struct workqueue_struct:工作队列对象,保存工作队列属性(WQ_UNBOUND/WQ_HIGHPRI/WQ_MEM_RECLAIM 等标志)、优先级、绑定的线程池
  • struct pool_workqueue:每个(工作队列 × NUMA 节点)的组合对象,作为连接工作队列与线程池的桥梁。一个工作队列在每个 NUMA 节点上都有一个 pwq
  • struct worker_pool:线程池对象,每个 CPU 拥有两个 Worker_Pool(普通优先级和高优先级)。线程池管理着一组 worker 线程,维护 pending work 列表和忙/闲状态列表

2.2 Worker 线程模型

每个 worker 线程由一个 struct worker 实例描述。worker 线程的生命周期如下:创建时绑定到某个 worker_pool 的 IDLE 列表;当有新 work 入队时,pool 从 IDLE 列表中取出一个 worker,设置当前 work 指针,唤醒线程执行 work 函数;执行完成后 worker 被放回 IDLE 列表。如果当前忙的 worker 数量已达上限(wq_pool_conf->max_active)且仍有 work 入队,新的 work 会加入 pending 列表,待有 worker 空闲时再执行。每个 worker 线程在 /proc 和 ps 中以 kworker/n:prio 命名(如 kworker/0:2 表示 CPU0 上的优先级为 2 的 worker)。

2.3 自管理机制:忙碌度与并发控制

CMWq 的"并发管理"体现在三个方面:动态扩缩——当所有 worker 都在忙碌且有 pending work 时,pool 可以创建新的 worker;忙时节流——当 worker 数量达到 max_active 且正在新 worker 创建中的 work 可能被暂时挂在 pending 链表等待;NUMA 亲和——UNBOUND 工作队列的线程池按 NUMA 节点分区优先,work 在哪个节点入队就更可能在该节点的 worker pool 上执行,避免跨节点内存访问延迟。

2.4 内置工作队列类型

内核提供了四个内置工作队列,按优先级从低到高排列:

  • system_wq:普通优先级,UNBOUND 或 per-CPU 模式,最常用
  • system_high_wq:高优先级,per-CPU 模式,用于延迟敏感任务
  • system_long_wq:普通优先级,专门容纳可能长时间运行的 work
  • system_unbound_wq:普通优先级,UNBOUND 模式,不绑定 CPU,适合需要跨 NUMA 灵活性的场景
  • system_power_efficient_wq:节能模式,选择低功耗 CPU 执行
  • system_freezable_wq:可冻结,在系统挂起/恢复时自动暂停/恢复

三、Workqueue API 全景指南

3.1 DECLARE_WORK:静态声明 work

静态声明是最简单的注册方式,适合在编译时就确定 work 回调函数的场景:

static void my_work_fn(struct work_struct *work);
static DECLARE_WORK(my_work, my_work_fn);

/* my_work 的地址可以全局使用 */
/* 但注意:同一个 work_struct 不能同时登记到多个 workqueue */

DECLARE_WORK 展开为一个名为 my_work 的 struct work_struct 变量,其 data 字段保存了 work 的所属 workqueue 信息和运行状态位。使用静态声明时必须注意:如果需要在不同场景用不同函数处理同一个 work,可以在回调函数内通过 container_of 反推外层结构体。

3.2 INIT_WORK:动态初始化 work

当 work 已经被声明(可能是嵌入在自定义结构体中的成员)但需要在运行时绑定回调函数时使用:

struct my_device {
    struct work_struct deferred_work;
    spinlock_t lock;
    /* ... */
};

static void my_work_handler(struct work_struct *work) {
    struct my_device *dev = container_of(work, struct my_device, deferred_work);
    /* 处理设备相关的工作 */
}

/* 在设备 probe 时调用 */
INIT_WORK(&dev->deferred_work, my_work_handler);

INIT_WORK 会重置 work_struct 的内部状态,必须在第一次将 work 调度到 workqueue 之前调用。如果 work 已经在执行中调用 INIT_WORK,会导致竞态和内存损坏。

3.3 queue_work / schedule_work:提交 work

queue_work(wq, work) 将 work 提交到指定的 workqueue。schedule_work(work) 是 queue_work(system_wq, work) 的便捷封装。提交过程:禁用本地 CPU 中断(避免和 work 执行端的竞争),将 work 挂入目标 workqueue 的 pending 列表,检查并唤醒需要执行的 worker 线程。queue_work 可以在中断上下文、软中断、进程上下文中安全调用。

3.4 queue_delayed_work:延迟提交 work

queue_delayed_work(wq, dwork, delay) 将在 delay 个 jiffies 后执行:

struct delayed_work my_dwork;

INIT_DELAYED_WORK(&my_dwork, my_work_handler);
queue_delayed_work(system_wq, &my_dwork, msecs_to_jiffies(100)); /* 100ms 后执行 */

延迟的实现依赖内核定时器(timer_list),在到期时将 work 从定时器列表转移到 workqueue 的 pending 列表。queue_delayed_work 在内部使用 mod_timer 来重置定时器。

3.5 alloc_workqueue:创建自定义工作队列

当内置工作队列无法满足需求时可以创建自定义工作队列:

/* 创建一个 UNBOUND 工作队列,内部线程池动态伸缩 */
struct workqueue_struct *my_wq = alloc_workqueue("my_driver/%s",
                                                   WQ_UNBOUND | WQ_MEM_RECLAIM,
                                                   4, /* max_active */
                                                   "devname");
/* 使用自定义工作队列 */
queue_work(my_wq, &my_work);
/* 销毁(释放所有资源) */
destroy_workqueue(my_wq);

第二个参数是 flags,可以组合:WQ_UNBOUND(不绑定 CPU)、WQ_HIGHPRI(高优先级 worker 池)、WQ_CPU_INTENSIVE(标记为 CPU 密集型,不参与自动并发计算)、WQ_MEM_RECLAIM(标记为内存回收关键路径,保证至少一个忙碌 worker)、WQ_FREEZABLE(可冻结),WQ_POWER_EFFICIENT(节能模式)。第三个参数是 max_active,0 表示默认值(UNBOUND 队列默认 256 × 每个 NUMA 节点的 CPU 数)。

3.6 取消 work 的几种方式

  • cancel_work_sync(work):取消 work,如果 work 正在执行则等待其完成后返回 true。必须在可睡眠上下文调用
  • cancel_work(work):仅尝试取消,不等待。可在任何上下文调用
  • cancel_delayed_work_sync(dwork):取消延迟 work,如果正在执行则等待完成
  • flush_workqueue(wq):等待 workqueue 中所有 work 全部执行完毕
  • drain_workqueue(wq):类似 flush,但还会阻止新 work 入队

四、并发控制:单串行、幂等性与内存序

4.1 WQ_UNBOUND vs Per-CPU 的并发差异

默认情况下在 per-CPU 模式下,queue_work_on(cpu, wq, work) 会将 work 提交到指定 CPU 的 pending 列表。同一 CPU 上的 worker 线程同一时间只能执行一个 work(即 work 在单个 CPU 上是串行的),但不同 CPU 可以并行执行各自的 work。这种"每个 CPU 串行、跨 CPU 并行"的模型非常适合简单的 per-CPU 数据处理任务(如网络收包统计),但也意味着如果 work 内睡眠时间过长会导致同一 CPU 上的其他 work 饿死。

WQ_UNBOUND 模式打破了 per-CPU 绑定,work 可以在任何 CPU 上的 worker 池中被执行。这使得 work 不会被特定 CPU 上的长任务阻塞,但也引入了 SMP 竞态问题——多个 worker 可能在不同 CPU 上同时执行不同 work,如果这些 work 访问共享数据,必须使用锁保护。

4.2 cancel_work_sync 的内存序保证

cancel_work_sync 有一个非常重要的副作用:在它返回之后,所有在它之前开始的 queue_work 调用都保证对 worker 线程可见(happens-before 关系)。这意味着在 cancel_work_sync 返回后,如果 work 不在 hwist 中,就可以安全释放 work 所引用的资源,不必担心 worker 仍在访问。这种保证依赖内部的自旋锁和 barrier。

4.3 避免重复入队

某些场景下需要避免同一个 work 被重复提交(导致 work 链表损坏或性能浪费)。标准做法是使用 work_pending(work) 标志位或 flags 字段的 WORK_STRUCT_PENDING_BIT 来检查 work 是否已在队列中。注:真正的幂等性保证需要额外机制,如自定义状态变量。不能仅靠 work_pending 实现真正的幂等,因为它在 work 刚刚被从 pending 列表摘下(即将执行)但还未开始执行时会短暂返回 false。

五、调试与可观测性

5.1 /proc 和 /sys 接口

/proc/workqueue 是 CMWq 子系统的主调试文件,显示了所有内置和自定义工作队列的状态。其内容按 CPU 列展示每个 worker_pool 的忙碌程度。

5.2 sysfs 接口:/sys/devices/virtual/workqueue/

从 Linux 5.17 起,每个 UNBOUND 工作队列在 /sys/devices/virtual/workqueue/ 下有自己的 sysfs 目录,其中包含可调参数:

  • max_active:该工作队列的最大活跃 work 数(读写)
  • nice:worker 线程的 nice 值(读写)。降低 nice 值可让 worker 获得更多 CPU 时间,适用于对延迟敏感的 delay 任务

使用 echo -5 > /sys/devices/virtual/workqueue/my_driver/nice 可以动态调整 worker 优先级,这对于生产环境中排查高负载场景非常有效。

5.3 使用 ftrace 追踪 workqueue 事件

内核自 4.9 后内置了 workqueue 专用的 tracepoint:workqueue_queue_work、workqueue_execute_start、workqueue_execute_end、workqueue_activate_work。通过这些 tracepoint 可以精确追踪每个 work 从入队到执行完毕的完整生命周期。

5.4 hung_task 检查与 workqueue 卡死排查

如果某个 work 函数执行了一个阻塞操作(如等待永远不会完成的 IO),会导致 worker 线程被占满,同一 pool 上的其他 work 全部饿死。内核的 hung_task_detector 会检测到长时间 D 状态的进程,但 worker 线程的名字(kworker/n:prio)可以作为排查入口。检查 TOP 中的 kworker CPU 占用率可以初步判断是否存在这种问题。

六、生产环境最佳实践与陷阱规避

6.1 必须在可睡眠上下文调用的 API

flush_workqueue、flush_work、cancel_work_sync、cancel_delayed_work_sync 都会在条件不满足时睡眠等待。绝不能在中断上下文或持有自旋锁时调用它们(否则造成内核 BUG:scheduling while atomic)。若是 drivers 中需要在 remove 或 shutdown 中等待 work 完成,通常的做法是在模块初始化时使用 devm_add_action_or_reset 注册一个自动执行的 cancel 回调,或使用 destroy_workqueue 来隐式 cancel 所有 work。

6.2 WQ_MEM_RECLAIM 的正确使用

标记为 WQ_MEM_RECLAIM 的工作路径需要保证至少有一个正在执行的 worker,因此在内存分配路径上不会被阻塞。注意:这不是"该 workqueue 不会被阻塞"的保证,而是"该 workqueue 自身在内存紧张时可以正常工作"的保证。如果一个 work 在执行中调用了可能触发直接内存回收(direct reclaim)的分配操作(如 GFP_KERNEL 分配大块内存),它可能递归进入回收路径。此时使用 WQ_MEM_RECLAIM 标志确保有一个 worker 不会被阻塞,可以处理其他关键任务。

6.3 不要在持有 spin_lock 时调用 queue_work

虽然 queue_work 本身可以在中断上下文中被调用,但如果持有一个自定义的自旋锁 B 的同时调用了某个 work 函数,该 work 函数也试图获取同一把锁,就会造成经典的自旋锁死锁——work 函数持有锁等待 spin_unlock,spin_lock 的持有者在当前 CPU 上抢占执行 work 导致死锁。解决方式是使用 workqueue 的自旋锁机制:在驱动中为 workqueue 提供一个独立的锁,并在 work 回调中释放。

6.4 避免在 work 函数中执行长时间阻塞操作

work 函数在执行时相当于一个绑定到 pool 的线程上下文。如果 work 函数调用了同步 IO(如 blkdev_issue_flush)、睡眠等待(如 wait_for_completion_timeout)或用户态拷贝操作,会阻塞同一 worker_pool 上的其他 work。解决方案:将长时间操作拆分到 system_long_wq 或独立的 UNBOUND 队列,并为之设置适当的 max_active 限制对共享资源的并发争用。

6.5 queue_work 的 CPU 亲緣性陷阱

queue_work(wq, work) 内部调用 queue_work_on(raw_smp_processor_id(), wq, work),即总是提交到当前 CPU。如果在 CPU 0 上大量入队 work 会导致 CPU 0 上的 worker 池过载,而其他 CPU 空闲。解决方案是显式使用 queue_work_on(cpu, wq, work) 指定目标 CPU,或者在 UNBOUND 队列下交给系统自动调度。

七、Workqueue 与其他内核异步机制的对比

7.1 Workqueue vs Tasklet vs Softirq

维度SoftirqTaskletWorkqueue
执行上下文中断/软中断软中断(基于 Softirq)进程上下文(kernel thread)
是否可睡眠否否是
是否可跨 CPU同类型 Softirq 可在不同 CPU 并行同类型 Tasklet 严格串行可跨 CPU(取决于模式)
注册方式静态(编译时)静态或动态动态
适用场景高频硬中断底半部(NET_RX, BLOCK)低延迟底半部需要睡眠、动态创建、不可预测延迟

7.2 Workqueue vs threaded IRQ

Threaded IRQ(request_threaded_irq)本质上是内核在硬中断 handler 执行完毕后创建一个临时的 work 来执行中断的后半部处理。区别在于 threaded IRQ 每个中断源有一个独立的 handler thread,名字为 irq/%d-handler。对于简单的设备驱动使用 threaded IRQ 更方便,对于需要共享工作线程、支持动态延迟、需要 flush/取消功能的复杂场景使用 workqueue。

7.3 Workqueue vs Timer

定时器(timer_list)只能在中断上下文执行回调,且不支持取消和串行化保证。当定时任务需要睡眠或需要保证串行执行时(如监控定时刷新),应该使用 delayed_work 替代定时器。

八、综合案例:网络设备驱动中的 workqueue 应用

我们以 Intel e1000e 网卡驱动为例,分析 workqueue 在真实驱动中的使用模式。e1000e 驱动在 probe 时初始化了 e1000e_reset_task 和 e1000e_watchdog_task 两个 work 函数,分别用于处理 MAC 重置(在 PCIe 链路状态变化时触发)和 NIC 看门狗(检测硬件异常)。

/* 初始化 */
INIT_WORK(&adapter->reset_task, e1000e_reset_task);
INIT_WORK(&adapter->watchdog_task, e1000e_watchdog_task);

/* 在错误检测中断或看门狗超时时调度 */
schedule_work(&adapter->reset_task);

/* reset_task 内部可以睡眠等待硬件,因为它运行在 workqueue 进程中 */
static void e1000e_reset_task(struct work_struct *work) {
    struct e1000_adapter *adapter = container_of(work, ...);
    /* 安全地执行可能睡眠的硬件操作 */
    e1000e_down(adapter, true); /* 会等待所有 TX/RX 完成 */
    e1000e_up(adapter);
    /* ... */
}

九、内核中的 Workqueue 使用统计

通过分析内核源码文件可以看到 workqueue 的使用极为广泛。detach_scsi_device、scsi_suspend_device、blk_add_timer 等底层核心机制都使用 workqueue。每个文件系统(ext4、xfs、btrfs)都有自己的 rescuer workqueue。网络子系统中几乎所有的驱动都依赖 workqueue。可以看出 workqueue 是内核中异步任务的"基础设施管道"。

十、总结与实践建议

Workqueue 是 Linux 内核异步执行的核心框架。总结以下实践建议:

  1. 确定执行上下文中是否可以睡眠:不能睡眠用 Softirq/Tasklet,可以睡眠用 Workqueue
  2. 确定是否需要跨 CPU 并行:需要严格串行用 per-CPU workqueue 或自定义串行队列
  3. 确定是否需要延迟执行:使用 delayed_work 而不是 hrtimer + work 的组合方式
  4. 自定义工作队列时使用 alloc_workqueue 并设置合理的 max_active
  5. 生产环境中使用 perf trace 或 ftrace 追踪 work 生命周期,发现延迟异常及时通过 sysfs 调整 nice 或 max_active
  6. 避免在持有非 workqueue 自旋锁的上下文中对同一个 work cancel_sync

掌握 Workqueue 不仅需要理解其 API 接口,更需要理解其背后的并发管理模型、内存序保证、进程上下文行为和生产环境中的典型故障模式。只有在层级化的认知框架下,才能在不同的硬件和工作负载场景中做出正确的调度器配置和参数调优决策。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部