在 Linux 内核中,大量工作不能也不应该在当前执行路径中立即完成。硬件中断要求快速返回,软中断上下文不允许睡眠,系统调用路径也不能因为一次慢速 I/O 而长时间阻塞。为了解决这一问题,内核提供了多种延迟执行和异步执行机制,其中工作队列(workqueue)是驱动开发、文件系统、电源管理、网络设备、块设备子系统中非常常见的一种基础设施。
工作队列的核心价值,在于把“需要稍后处理的任务”封装成一个工作项,然后交由内核线程在进程上下文中执行。相比软中断和 tasklet,工作队列允许睡眠、允许等待锁、允许执行可能阻塞的操作,因此特别适合驱动中需要访问 I2C、SPI、USB、块设备、文件系统接口的场景。
本文围绕 Linux 内核工作队列机制展开,重点讨论其执行模型、核心数据结构、常用 API、并发语义、生命周期管理、调试方法,以及一个典型的驱动卸载阶段 use-after-free 问题的分析与解决过程。文章面向内核驱动开发、嵌入式 Linux 应用开发、系统稳定性调试等场景,力求概念准确、代码可验证、调试思路可落地。
Linux 内核中的许多事件具有突发性。例如:
这些事件发生时,当前执行上下文往往不适合做复杂处理。以硬件中断为例,中断处理程序运行在中断上下文,必须尽快结束,否则会影响其他中断响应,也可能导致系统实时性下降。中断上下文还有严格限制:不能睡眠,不能主动让出 CPU,不能调用可能阻塞的函数。
因此,内核通常将中断处理拆分为两个阶段:
工作队列就是下半部机制中的一种重要实现。它将任务提交给内核工作线程执行,运行环境是进程上下文。进程上下文意味着:
当然,工作队列也不是没有代价。相比软中断,工作队列需要经过内核线程调度,延迟更高。因此,对于极低延迟、极短处理时间的任务,软中断、tasklet 或 threaded IRQ 可能更合适;对于需要睡眠、需要复杂逻辑、需要与内核对象生命周期严格配合的任务,工作队列更合适。
工作队列机制涉及几个关键对象:
struct work_struct:普通工作项;struct delayed_work:延迟工作项;struct workqueue_struct:工作队列;struct worker:工作线程;struct worker_pool:工作线程池。一个典型执行流程如下:
中断 / 定时器 / 内核任务 | vqueue_work() / schedule_work() | vworkqueue_struct / pwq | vworker_pool 待执行队列 | vkworker 内核线程 | vwork->func(work)struct work_struct 表示一个工作项。它包含工作处理函数和内部状态。驱动通常把它嵌入到自己的私有结构体中,然后通过 container_of() 恢复上下文。
典型结构如下:
struct sensor_priv {struct i2c_client *client;struct work_struct work;};工作处理函数原型为:
static void sensor_work_func(struct work_struct *work){struct sensor_priv *priv; priv = container_of(work, struct sensor_priv, work);/* 在这里执行实际处理逻辑 */}这种设计使得一个工作项可以携带其所属设备对象,而不需要全局变量。
struct delayed_work 用于延迟执行的工作项。它内部包含一个 work_struct 和延迟提交机制。常见用途包括:
定义方式如下:
struct sensor_priv {struct delayed_work dwork;};处理函数中通过 dwork.work 恢复上下文:
static void sensor_poll_func(struct work_struct *work){struct delayed_work *dwork = to_delayed_work(work);struct sensor_priv *priv; priv = container_of(dwork, struct sensor_priv, dwork);/* 执行轮询逻辑 */}struct workqueue_struct 表示一个工作队列。驱动可以使用系统默认工作队列,也可以创建自己的工作队列。
系统默认工作队列适合简单、短小、通用的任务。若驱动有隔离性、优先级、并发度、生命周期管理方面的特殊要求,通常会创建独立工作队列。
创建工作队列常用接口:
struct workqueue_struct *alloc_workqueue(const char *fmt,unsigned int flags,int max_active);销毁接口:
void destroy_workqueue(struct workqueue_struct *wq);工作项最终由内核线程执行,这些线程通常命名为 kworker。例如:
kworker/0:1kworker/u16:2kworker/1:3其中数字与 CPU、workqueue 类型、绑核策略有关。工作队列内部通过 worker_pool 管理一组 worker。现代 Linux 工作队列采用并发管理机制,会根据负载创建或回收 worker,以避免过多线程造成调度压力,也避免单个任务阻塞整个工作队列。
对于普通驱动开发者而言,通常不需要直接操作 worker_pool,但理解其存在有助于分析 /proc、debugfs、ps、ftrace 中的执行路径。
静态初始化:
static void my_work_func(struct work_struct *work);static DECLARE_WORK(my_work, my_work_func);动态初始化:
INIT_WORK(&priv->work, my_work_func);提交到系统默认工作队列:
schedule_work(&priv->work);提交到指定工作队列:
queue_work(priv->wq, &priv->work);静态初始化:
static void my_dwork_func(struct work_struct *work);static DECLARE_DELAYED_WORK(my_dwork, my_dwork_func);动态初始化:
INIT_DELAYED_WORK(&priv->dwork, my_dwork_func);提交延迟工作项:
schedule_delayed_work(&priv->dwork, msecs_to_jiffies(100));提交到指定工作队列:
queue_delayed_work(priv->wq, &priv->dwork, msecs_to_jiffies(100));修改延迟工作项的延迟时间或重新排队:
mod_delayed_work(priv->wq, &priv->dwork, msecs_to_jiffies(500));创建一个普通工作队列:
priv->wq = alloc_workqueue("sensor_wq", 0, 0);if (!priv->wq)return -ENOMEM;创建一个 unbound 工作队列:
priv->wq = alloc_workqueue("sensor_wq", WQ_UNBOUND, 0);创建一个有序工作队列:
priv->wq = alloc_ordered_workqueue("sensor_wq", 0);有序工作队列保证工作项按提交顺序执行,适合对顺序有严格要求的场景。
等待某个工作项完成:
flush_work(&priv->work);取消某个工作项并等待其完成:
cancel_work_sync(&priv->work);取消延迟工作项并等待其完成:
cancel_delayed_work_sync(&priv->dwork);等待整个工作队列上的所有工作完成:
flush_workqueue(priv->wq);销毁工作队列:
destroy_workqueue(priv->wq);在驱动卸载路径中,常见安全顺序是:
理解工作队列的并发语义,是避免竞态、死锁、重复提交、资源释放错误的关键。
同一个 struct work_struct 在同一时刻只会有一个执行实例。若该工作项已经处于 pending 状态,再次调用 queue_work() 不会创建新的执行实例。
例如:
queue_work(wq, &priv->work);queue_work(wq, &priv->work);第二次提交时,如果第一次提交的工作项尚未执行或仍在队列中,第二次提交会失败。queue_work() 返回 false。
这意味着:
work_struct 不能表达多个并发事件;work_struct;work_struct 反而可以降低负载。除非显式使用绑核提交接口,否则不应假设工作项一定运行在某个 CPU 上。工作项可能运行在不同 CPU,也可能被迁移。
若确有需要,可以使用:
queue_work_on(cpu, wq, &priv->work);但绑核会带来负载不均衡和调度限制,应谨慎使用。
alloc_workqueue() 的 max_active 参数用于限制同一工作队列中同时执行的工作项数量。
传入 0 表示使用内核默认值。
若希望严格顺序执行,可以使用:
alloc_ordered_workqueue("my_wq", 0);其本质是限制并发度为 1。
同一个工作队列中的多个不同工作项,可能由多个 worker 并发执行。因此,多个工作项访问共享数据时,必须自行保证同步。
常见同步手段包括:
alloc_workqueue() 支持多种标志,不同标志适用于不同场景。
WQ_UNBOUND表示工作队列不绑定到提交工作项的 CPU。worker 可以在多个 CPU 上执行,适合长时间任务或需要较高并发度的任务。
典型场景:
WQ_HIGHPRI表示工作项应尽可能在高优先级 worker pool 中执行。适合对延迟敏感但仍希望运行在进程上下文的任务。
WQ_MEM_RECLAIM表示该工作队列可能参与内存回收路径。内核会保证在内存紧张时仍有机制推进该队列上的工作,避免因 worker 创建失败导致回收路径死锁。
典型场景:
普通驱动若与内存回收路径无关,通常不需要设置该标志。
WQ_CPU_INTENSIVE表示工作项是 CPU 密集型任务,不应阻塞其他工作项的并发调度。该标志适合长时间计算型任务。
WQ_FREEZABLE表示工作队列可被系统冻结。系统挂起时,freezable 工作队列中的工作项会被冻结,避免影响 suspend 流程。
适合与后台轮询、低功耗状态切换相关的任务。
Linux 内核常见的延迟执行机制包括 softirq、tasklet、workqueue、threaded IRQ 等。它们并不是简单替代关系,而是适用于不同场景。
softirq 和 tasklet 运行在软中断上下文,不能睡眠。它们适合非常短、非常关键、不能容忍调度延迟的任务。
缺点是:
workqueue 的优势在于进程上下文,可以睡眠,适合复杂处理。
缺点是:
threaded IRQ 将中断处理放到内核线程中执行,也可以睡眠。它与 workqueue 的区别在于:
很多驱动会组合使用:中断线程中只做简单判断,然后提交 work,由 workqueue 执行慢速设备访问。
工作队列相关 Bug 往往不是出现在提交阶段,而是出现在卸载、热插拔、suspend/resume、设备 unbind 等生命周期切换阶段。
示例:
#include <linux/workqueue.h>#include <linux/i2c.h>#include <linux/interrupt.h>struct sensor_priv {struct i2c_client *client;struct work_struct work;struct workqueue_struct *wq;};static void sensor_work_func(struct work_struct *work){struct sensor_priv *priv;int val; priv = container_of(work, struct sensor_priv, work); val = i2c_smbus_read_word_data(priv->client, 0x00);if (val < 0) dev_err(&priv->client->dev, "read register failed\n");}static irqreturn_t sensor_irq_handler(int irq, void *data){return IRQ_WAKE_THREAD;}static irqreturn_t sensor_irq_thread(int irq, void *data){struct sensor_priv *priv = data; queue_work(priv->wq, &priv->work);return IRQ_HANDLED;}static int sensor_probe(struct i2c_client *client){struct sensor_priv *priv;int ret; priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL);if (!priv)return -ENOMEM; priv->client = client; i2c_set_clientdata(client, priv); INIT_WORK(&priv->work, sensor_work_func); priv->wq = alloc_workqueue("sensor_wq", 0, 0);if (!priv->wq)return -ENOMEM; ret = request_threaded_irq(client->irq, sensor_irq_handler, sensor_irq_thread, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, client->name, priv);if (ret) { destroy_workqueue(priv->wq);return ret; }return 0;}推荐顺序:
static void sensor_remove(struct i2c_client *client){struct sensor_priv *priv = i2c_get_clientdata(client); free_irq(client->irq, priv); cancel_work_sync(&priv->work); destroy_workqueue(priv->wq);}这里的关键是:
free_irq() 先阻止新的中断进入;cancel_work_sync() 等待已经提交或正在执行的工作项结束;destroy_workqueue() 再销毁工作队列。如果顺序反过来,可能出现:
若驱动使用周期性延迟工作项:
static void sensor_poll_func(struct work_struct *work){struct delayed_work *dwork = to_delayed_work(work);struct sensor_priv *priv; priv = container_of(dwork, struct sensor_priv, dwork);/* 读取数据 */ queue_delayed_work(priv->wq, &priv->dwork, msecs_to_jiffies(1000));}启动:
INIT_DELAYED_WORK(&priv->dwork, sensor_poll_func);queue_delayed_work(priv->wq, &priv->dwork, msecs_to_jiffies(1000));停止:
cancel_delayed_work_sync(&priv->dwork);若轮询逻辑内部会重新提交自身,卸载路径应确保停止条件明确。例如使用状态标志:
if (!priv->polling)return;queue_delayed_work(priv->wq, &priv->dwork, msecs_to_jiffies(1000));remove 中:
priv->polling = false;cancel_delayed_work_sync(&priv->dwork);这样可以避免取消后又被重新提交。
工作队列本身是异步机制,但同步等待接口如果使用不当,很容易引入死锁。
错误示例:
static void my_work_func(struct work_struct *work){ flush_work(work);}该写法没有意义,且会导致死锁。因为当前工作项正在执行,却又等待自己完成。
同理,也不应在自身处理函数中调用:
cancel_work_sync(work);或:
cancel_delayed_work_sync(dwork);来取消自身。
若工作项运行在 wq 上,而工作项内部调用:
flush_workqueue(wq);则可能死锁。因为 flush_workqueue() 等待该工作队列上的所有工作项完成,而当前工作项本身就是其中之一。
正确做法:
work_struct 和 flush_work();completion;cancel_work_sync() 会等待工作项执行完成。若 remove 路径持有某个锁,而工作项正好也要获取该锁,则可能死锁。
例如:
remove: mutex_lock(&priv->lock); cancel_work_sync(&priv->work); mutex_unlock(&priv->lock);work: mutex_lock(&priv->lock); ...此时 remove 等待 work 完成,work 等待 remove 释放锁,形成死锁。
解决思路:
cancel_work_sync();工作项最终由 kworker 执行。可通过 ps 查看:
ps ax -o pid,comm输出示例:
PID COMMAND 12 kworker/0:1 18 kworker/1:2 25 kworker/u16:2若某个 kworker 长期占用 CPU,可结合 perf、ftrace、/proc/<pid>/stack 分析。
若内核开启 debugfs,可以查看:
cat /sys/kernel/debug/workqueue该节点可以显示工作队列、pwq、worker pool、pending work 等信息。不同内核版本输出格式略有差异,但通常可用于确认工作队列是否存在、是否有工作项积压。
可跟踪:
queue_workqueue_work_onqueue_delayed_workprocess_one_work示例:
cd /sys/kernel/tracingecho 1 > events/workqueue/enablecat trace具体可用事件与内核版本有关。若系统支持 workqueue tracepoint,可直接观察 work 提交和执行过程。
若工作项访问了已释放对象,KASAN 通常会输出类似信息:
BUG: KASAN: slab-use-after-free in sensor_work_func+0x...这类问题通常说明:
开启 CONFIG_PROVE_LOCKING 后,内核会检查锁依赖。若工作队列等待路径与设备锁形成循环依赖,lockdep 有机会在死锁发生前给出告警。
下面给出一个驱动开发中非常典型的问题。
某 I2C 传感器驱动在中断线程中提交工作项,工作项中读取传感器寄存器。驱动加载、触发中断、读取数据均正常。但在快速执行加载、触发中断、卸载后,内核出现 KASAN 报错。
复现步骤:
modprobe sensor_drvecho test > /sys/kernel/debug/sensor/trigger_irqrmmod sensor_drv内核日志类似:
BUG: KASAN: slab-use-after-free in sensor_work_func+0x48/0xd0Read of size 8 at addr ffff888105a3b000 by task kworker/1:3/87Workqueue: sensor_wq sensor_work_funcCall Trace: dump_stack print_address_description print_report kasan_report sensor_work_func process_one_work worker_thread kthread ret_from_fork日志中关键信息有三点:
sensor_work_func;Workqueue: sensor_wq;slab-use-after-free。这说明 kworker 正在执行驱动的工作项,而工作项访问的对象已经被释放。
原始 remove 代码如下:
static void sensor_remove(struct i2c_client *client){struct sensor_priv *priv = i2c_get_clientdata(client); free_irq(client->irq, priv); kfree(priv);}问题很明显:
free_irq() 只能保证之后不再有新的中断线程提交 work;free_irq() 之前,可能已经有 work 被提交;kfree(priv) 释放对象后,kworker 仍可能访问 priv。正确做法是在释放对象前取消并等待工作项完成。
修复后的 remove:
static void sensor_remove(struct i2c_client *client){struct sensor_priv *priv = i2c_get_clientdata(client); free_irq(client->irq, priv); cancel_work_sync(&priv->work); destroy_workqueue(priv->wq); kfree(priv);}若使用延迟工作项:
free_irq(client->irq, priv);cancel_delayed_work_sync(&priv->dwork);destroy_workqueue(priv->wq);kfree(priv);若工作项内部还会重新提交自身,应增加停止标志:
static void sensor_poll_func(struct work_struct *work){struct delayed_work *dwork = to_delayed_work(work);struct sensor_priv *priv; priv = container_of(dwork, struct sensor_priv, dwork);if (!priv->polling)return;/* 轮询逻辑 */ queue_delayed_work(priv->wq, &priv->dwork, msecs_to_jiffies(1000));}remove:
priv->polling = false;free_irq(client->irq, priv);cancel_delayed_work_sync(&priv->dwork);destroy_workqueue(priv->wq);kfree(priv);修复后需要反复验证:
modprobe sensor_drvrmmod sensor_drv同时制造中断:
echo test > /sys/kernel/debug/sensor/trigger_irq验证标准:
dmesg 无 workqueue 警告;虽然工作队列运行在进程上下文,可以睡眠,但并不意味着工作项可以无限制执行。过长的处理函数会:
对于长任务,可以拆分为多个阶段,或者使用延迟工作项分步执行。
工作队列受调度器影响,不能保证严格纳秒级或微秒级延迟。若系统有硬实时要求,应考虑:
每个 work_struct 都应有明确的生命周期归属。通常归属于某个设备对象、子系统对象或驱动私有结构体。
对象释放前,必须确保:
很多 Bug 不是因为没有 cancel,而是因为 cancel 之后又有新的提交。
常见新提交来源:
因此,安全卸载的核心不是简单调用 cancel_work_sync(),而是先切断提交来源。
flush_workqueue() 的等待范围很大,容易造成不必要的阻塞。多数场景下,应优先使用:
flush_work(&priv->work);或:
cancel_work_sync(&priv->work);只等待与当前对象相关的工作项,而不是等待整个工作队列。
如果多个工作项必须按顺序执行,可以使用 ordered workqueue:
alloc_ordered_workqueue("my_ordered_wq", 0);但需要注意,顺序执行会降低并发度。如果任务之间没有依赖关系,不必强制顺序执行。
对于长时间、低实时性、可并发的后台任务,可以使用:
WQ_UNBOUND这样可以避免长时间任务绑定在某个 CPU 上影响该 CPU 的其他工作项调度。
若驱动参与 suspend/resume,需要考虑:
否则可能出现 suspend 卡住、resume 后设备状态异常、工作项访问未唤醒设备等问题。
硬件中断只通知事件,真正读取 I2C/SPI 寄存器放到 work 中执行。
优点:
GPIO 按键中断触发后,不立即上报,而是提交延迟工作项。延迟一段时间后再次读取 GPIO 状态,确认是否为有效按键。
没有中断的传感器,可以使用 delayed work 定时读取数据。
输入设备驱动中,中断采集原始数据,work 中调用 input 子系统上报事件。
例如背光、LED、电源域等。用户操作停止后,延迟一段时间再关闭,避免频繁开关。
复杂驱动状态机可以在 work 中执行状态切换,避免在中断上下文中做复杂判断。
schedule_work() 只是提交工作项,真正执行时间取决于:
因此不能假设提交后立即完成。
cancel_work_sync() 只处理调用时已经提交或正在执行的工作项。如果之后还有路径继续提交同一个工作项,问题依旧存在。
free_irq() 只能阻止新的中断处理,不能保证之前已经提交的工作项已经完成。因此仍需要 cancel_work_sync() 或 flush_work()。
destroy_workqueue() 会进行清理,但驱动仍应显式取消工作项。否则可能出现:
工作项可以睡眠,但不能因此忽略性能。长时间睡眠或长时间等待会占用 worker,影响其他任务。
同一个 work_struct 不会并发执行,但可能被多次顺序执行。设计时要考虑:
工作队列不只是一个简单的任务队列,它体现了内核异步执行框架的几个重要设计思想。
工作队列将任务从中断上下文、软中断上下文或原子上下文,转移到进程上下文。这样可以使用更丰富的内核 API,也更容易处理复杂逻辑。
工作队列内部通过 worker pool 和并发管理,避免每个任务都创建独立内核线程。系统可以根据负载动态调整 worker 数量,兼顾吞吐和资源消耗。
工作项、工作队列、worker 三者分离,使驱动可以只关心任务本身,而不必手动管理线程创建、唤醒、退出。
工作队列可以与中断、定时器、completion、wait queue、电源管理、设备模型组合,形成完整的异步处理链路。
工作队列是 Linux 内核中非常基础但又极其重要的机制。它将异步任务从不可睡眠的上下文转移到进程上下文,使驱动能够安全地执行慢速 I/O、复杂状态处理和可阻塞操作。理解工作队列,不仅要掌握 INIT_WORK、schedule_work、queue_work、cancel_work_sync 等 API,更要理解其背后的执行模型、并发语义和生命周期约束。
实际开发中,工作队列相关 Bug 往往集中在几个方向:
flush_workqueue 或 cancel_work_sync 引发死锁;work_struct 被误用于多个并发事件;解决这些问题的核心思路并不复杂:明确工作项归属,切断提交来源,释放对象前完成取消与等待,避免自等待和锁循环,合理选择工作队列类型与标志。
对于驱动开发而言,工作队列用得好,可以显著降低中断压力、提升系统稳定性;用得不好,则容易引入竞态、死锁和内存安全问题。掌握工作队列机制,是 Linux 驱动开发从“能跑”走向“稳定可维护”的关键一步。