Linux 工作队列
一、引言:为什么需要工作队列?
在Linux内核中,中断处理程序需要快速执行完毕,但有时我们有一些任务比较耗时或可能“睡眠”(比如等待磁盘读写),这些任务不适合在中断上下文中执行。为此,内核提供了多种“下半部”机制来延迟执行这些任务。工作队列(Work Queue)就是其中一种强大而灵活的选择,它允许任务在内核线程的进程上下文中安全运行,从而可以调用包括可能引发睡眠在内的几乎所有内核API。
一句话定义:工作队列是一种将任务(工作)推迟到进程上下文中执行的内核机制,由专门的内核线程(工作者线程)处理,其最大优势是允许任务睡眠和重新调度。
二、核心概念:工作队列的组成
工作队列机制主要涉及三个核心数据结构,它们的关系如附图所示(可参考您上传的第4张图“工作、工作队列和工作者线程之间的关系”):
-
工作(Work): 由
struct work_struct表示,它封装了需要推迟执行的具体任务,核心是一个指向处理函数的指针。 一句话定义:一个work_struct结构体代表一个具体的延迟任务,包含待执行的函数及其参数。 -
工作队列(Workqueue): 由
struct workqueue_struct表示,它是同类型工作者线程的集合管理器。 一句话定义:workqueue_struct是同一类工作者线程的抽象,系统允许多种类型的工作队列共存。 -
工作者线程(Worker Thread): 是执行工作的内核线程。每个CPU(或每个工作队列类型)通常都有一个这样的线程。缺省的通用线程名为
events/n。 一句话定义:工作者线程是执行工作的实体,它在进程上下文中运行,可以睡眠,每个CPU通常关联一个默认线程。
简单来说,你创建工作(定义函数),将其提交到一个工作队列,最终由关联的工作者线程取出并执行。
三、工作原理:工作者线程的生命周期
工作者线程的核心是一个循环,其简化逻辑如下(如您上传的第3张图所示):
- 休眠等待:线程将自己设置为可中断休眠状态,并加入等待队列。
- 检查工作:如果工作链表为空,则调用
schedule()进入睡眠。 - 执行工作:当有工作被加入队列(
schedule_work),线程被唤醒,将状态设为运行,并调用run_workqueue()函数。 - 遍历执行:
run_workqueue()循环遍历工作链表,依次执行每个work_struct中预设的函数 (func)。 - 回归休眠:所有工作执行完毕后,线程再次进入第1步,等待新工作。
这个机制确保了任务在后台被有序、异步地处理。
四、如何使用工作队列?
1. 创建与初始化工作
有两种方式创建 struct work_struct:
- 静态创建(编译时):
DECLARE_WORK(name, func, data) - 动态创建(运行时):
INIT_WORK(&work, func, data)
其中 func 是工作处理函数,data 是传递给它的参数。
2. 编写处理函数
处理函数的原型是:void work_handler(void *data)。由于此函数在进程上下文中运行,因此可以安全地睡眠、使用信号量等同步机制,但不能直接访问用户空间内存(除非在特定的内核路径下)。
3. 调度工作
- 立即调度:
schedule_work(&work)将工作提交到系统默认的events工作队列。 - 延迟调度:
schedule_delayed_work(&work, delay)可以延迟指定的“滴答”数后再执行。
4. 刷新与取消
- 刷新队列:
flush_scheduled_work()等待默认队列中所有工作执行完毕。可能会睡眠。 - 取消延迟工作:
cancel_delayed_work(&work)尝试取消一个已调度的延迟工作。
5. 创建专用工作队列
如果默认队列无法满足需求(例如任务非常繁重,避免影响系统其他部分),可以创建自己的队列:
struct workqueue_struct *my_wq = create_workqueue("my_name");
queue_work(my_wq, &work); // 向该队列提交工作
五、关键抉择:何时使用工作队列?何时用Tasklet?
这是设计“下半部”时的核心决策点,可参考您提供的对比总结。
| 特性 | 工作队列 (Work Queue) | 任务小函数 (Tasklet) |
|---|---|---|
| 执行上下文 | 进程上下文 | 软中断上下文 |
| 可否睡眠/阻塞 | 可以 | 绝对不可以 |
| 并发特性 | 可在不同CPU上并行执行 | 同一tasklet在多个CPU上串行执行 |
| 延迟 | 较高,受线程调度影响 | 较低,响应更快 |
| 实现基础 | 内核线程 | 软中断 |
选择准则(黄金法则):
- 如果需要执行可能睡眠或阻塞的操作(如:获取信号量、等待I/O、调用
kmalloc(GFP_KERNEL)),必须使用工作队列。 - 如果只是进行快速、非阻塞的中断后处理(如:修改一些内部状态、统计计数),优先考虑使用tasklet或软中断以获得更低延迟。
六、总结
工作队列是Linux内核中功能最全面的延迟执行机制。它将工作交给独立的内核线程,在进程上下文中执行,从而获得了睡眠和调度的能力。对于大多数需要在内核中执行复杂、耗时或可能阻塞操作的驱动和子系统来说,工作队列是安全而方便的选择。在具体使用时,开发者应首先判断任务属性(是否需要睡眠),根据上述“黄金法则”在工作队列和tasklet之间做出正确选择。系统的缺省events队列足以应对大部分场景,而在有特殊性能隔离需求时,可以创建专用工作队列。
留下评论