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/