1工作队列workqueue引入#
定时器、 tasklet,它们都是在中断上下文中执行(softirq中完成的),它们无法休眠。那么如果一旦中断要处理耗时复杂的操作,就会显得很卡。那么使用内核线程来处理这些耗时的工作,那就可以解决系统卡顿的问题。
Linux内核中工作队列workqueue就是线程化处理的一种方式,“工作队列”(workqueue), 它是内核自带的内核线程。要使用“工作队列”,只需要把“工作”放入“工作队列"中,对应的内核线程就会取出 “工作”,执行里面的函数。
工作队列的应用场合:
要做的事情比较耗时,甚至可能需要休眠,那么可以使用工作队列。
缺点:多个工作(函数)是在某个内核线程中依序执行的,前面函数执行很慢,就会影响到后面的函数。
在多 CPU 的系统下,一个工作队列可以有多个内核线程,可以在一定程度上缓解这个问题。
工队队列的源码机制在Linux-4.9.88\kernel\workqueue.c,头文件在Linux-4.9.88\include\linux\workqueue.h
1.1 work_struct描述#
1 | struct work_struct { |


表示一个work结构,一个任务或者叫做一个工作,里面的.func表示是要执行的任务函数,类型定义为:
1 | typedef void (*work_func_t)(struct work_struct *work); |
1.1.1 定义一个work#
1 |
|

定义一个work为n, 并且初始化函数f.
如果代码中定义好了一个work_struct结构体,那么可以用INIT_WORK函数来初始化:

1 |
1.1.2 使用work#
初始化完work后,调用schedule_work即可调度工作队列进行处理当前任务。
调用 schedule_work 时,就会把work_struct 结构体放入队列system_wq中,并唤醒对应的内核线程。内核线程就会从队列里把 work_struct 结构体取出来,执行里面的函数。
1 | /** |
可以看到system_wq是内核自带的队列,结构属性为struct workqueue_struct
如果不想用内核自带的system_wq来调度我们的work, 那么可以调用create_workqueue函数自行创建工队队列。然后用queue_work函数使能.
1.1.3 工作队列相关函数#
| 函数名 | 作用 |
|---|---|
| create_workqueue | 在 Linux 系统中已经有了现成的 system_wq 等工作队列,你当然也可以自己调用 create_workqueue 创建工作队列,对于 SMP 系统,这个工作队列会有多个内核线程与它对应,创建工作队列时,内核会帮这个工作队列创建多个内核线程 |
| create_singlethread_workqueue | 如果想只有一个内核线程与工作队列对应,可以用本函数创建工作队列,创建工作队列时,内核会帮这个工作队列创建一个内核线程 |
| destroy_workqueue | 销毁工作队列 |
| schedule_work | 调度执行一个具体的 work,执行的 work 将会被挂入 Linux 系统提供的工作队列 |
| schedule_delayed_work | 延迟一定时间去执行一个具体的任务,功能与 schedule_work 类似,多了一个延迟时间 |
| queue_work | 跟 schedule_work 类似,schedule_work 是在系统默认的工作队列上执行一个work,queue_work 需要自己指定工作队列 |
| queue_delayed_work | 跟 schedule_delayed_work 类似,schedule_delayed_work 是在系统默认的工作队列上执行一个 work,queue_delayed_work 需要自己指定工作队列 |
| flush_work | 等待一个 work 执行完毕,如果这个 work 已经被放入队列,那么本函数等它执行完毕,并且返回 true;如果这个 work 已经执行完华才调用本函数,那么直接返回 false |
| flush_delayed_work | 等待一个 delayed_work 执行完毕,如果这个 delayed_work 已经被放入队列,那么本函数等它执行完毕,并且返回 true;如果这个 delayed_work 已经执行完华才调用本函数,那么直接返回 false |
2 编写代码及解析#
2.1 workqueue用例驱动源码#
驱动代码
1 |
|
2.2 分析#


为每一个按键都建立一个work_struct,并且初始化work。
1 | INIT_WORK(&gpio_keys_100ask[i].work, key_work_func); |
key_work_func是work里面函数,参数为该work自身。该函数只是简单打印该work的自身属性(work名字,work进程id),然后输出按键值。通过container_of找到父亲结构体gpio_key。
注意:current是Linux内核自带的一个变量,外部驱动要引用它只需要包含头文件:
1 |


中断到来后,这时候上半部完成清中断等一些列重要操作,使能workqueue工作队列,调用函数schedule_work。
内核从系统工作队列system_wq从取出该work,执行里面的函数(key_work_func)。
可以看到current信息:pid为428,内核线程名字为[kworker/0:1]

3 工作队列内部机制原理#
3.1 Linux 2.x 的工作队列创建过程#
代码在kernel\workqueue.c中:
1 | init_workqueues//函数主体如下 |


会先分配一个workqueue结构创建一个system_wq工作队列,为每一个 CPU,都创建一个名为“events/X”的内核线程,X 从 0 开始。在创建 workqueue 的同时创建内核线程。
3.2 Linux 4.x 的工作队列创建过程#
Linux4.x 中,内核线程和工作队列是分开创建的。先创建内核线程,在 kernel\workqueue.c 中
对每一个cpu,都会创建2个work_pool结构体:
1 | init_workqueues //函数主体如下: |

create_worker 函数代码如下:
创建好内核线程后,再创建 workqueue:这里workqueue会和普通优先级的work_pool建立联系,以后给workqueue添加work的时候会放入work_pool中,执行对应work的时候唤醒相对应的work线程,比如kwork/0:1

3.3 schedule_work#
schedule_work 会将 work 添加到默认的工作队列也就是 system_wq 中。
1 | static inline bool schedule_work(struct work_struct *work) |
3.3.1 __queue_work#
继续调用__queue_work.
1 | static void __queue_work(int cpu, struct workqueue_struct *wq, |
主要由3个部分组成:
- 获取 cpu 参数
- 检查冲突
- 添加 work 到队列
insert_work
3.3.1.1 insert_work#
1 | static void insert_work(struct pool_workqueue *pwq, struct work_struct *work, |
简单地说,就是将 work 插入到worker_pool->worklist中。
添加完之后,就会唤醒 worker_pool 中第一个处于idle状态worker->task内核线程,work 就会进入到待处理状态。
3.4 worker_thread调度#
1 | static int worker_thread(void *__worker) |
worker_thread函数主要包括以下的几个主要部分:
- 管理 worker
- 执行 work当执行完
1
2
3
4
5
6
7
8
92.1 从当前worker_pool->worklist 中的链表元素取出work
2.2 move_linked_works 将会在执行前将 work 添加到 worker->scheduled 链表中
,该接口和 list_add_tail 不同的是,这个接口会先删除链表中存在的节点并重新添加,
保证不会重复添加,且始终添加到最后一个节点。
2.3 process_scheduled_works 函数正式执行 work,该函数会遍历 worker->scheduled 链表,
执行每一个 work,执行之前会做一些必要的检查,比如在同一个 cpu 上,
一个 worker 不能在多个 worker 线程中被并发执行(这里的并发执行指的是同时加入到 schedule 链表),
是否需要唤醒其它的 worker 来协助执行(碰到 cpu 消耗型的work 需要这么做),
执行 work 的方式就是调用 work->funcworker_pool->worklist中所有的work之后,当前线程就会陷入睡眠.
3.5 linux5.1.x版本的workqueue bug#
在多核cpu调度时,使用workqueue会小概率出现WARNING: CPU: x PID: xx at linux_5.10/kernel/workqueue.c:1796 worker_enter_idle
的call trace提示,然后cpu进入idel休眠状态。
由于如果在 WORKER_NOT_RUNNING 检查时和下面的 nr_running 增量之间被unbind_workers()抢占,我们可能会破坏 nr_running 重置并在新的未绑定池上留下意外的 pool->nr_running == 1 。
为了 防止这样的竞态产生,linux内核patch参考:
https://lore.kernel.org/lkml/20220114081544.899493450@linuxfoundation.org/
