LV06-13-中断-03-中断下半部-02-工作队列
工作队列是什么?若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
PC端开发环境 | Windows | Windows11 |
Ubuntu | Ubuntu20.04.2的64位版本 | |
VMware® Workstation 17 Pro | 17.6.0 build-24238078 | |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) | |
Win32DiskImager | Win32DiskImager v1.0 | |
Linux开发板环境 | Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04 | |
linux内核 | linux-4.19.71(NXP官方提供) |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
nxp-imx/linux-imx/releases/tag/v4.19.71 | NXP linux内核仓库tags中的v4.19.71 | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 | NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0 | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) | |
Source Code | https://elixir.bootlin.com/linux/latest/source | linux kernel源码 |
kernel/git/stable/linux.git - Linux kernel stable tree | linux kernel源码(官网,tag 4.19.71) | |
https://elixir.bootlin.com/u-boot/latest/source | uboot源码 |
一、工作队列简介
在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但是毕竟整个中断的处理还没走完,这期间 APP 是无法执行的。假设下半部要执行 1、 2 分钟,在这 1、 2 分钟里 APP 都是无法响应的。
这谁受得了?所以,如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和 APP 都一样竞争执行, APP 有机会执行,系统不会卡顿。这个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多这样的线程:
1 | ps -A | grep kworker |

总的来说,下半部要做的事情太多并且很复杂,我们就可以考虑使用工作队列。接下来就来了解一下吧。
1. 工作队列是什么?
工作队列是实现中断下半部分的机制之一, 是一种用于管理任务的数据结构或机制。 它通常用于多线程, 多进程或分布式系统中, 用于协调和分配待处理的任务给可用的工作线程或工作进程。
工作队列(work queue)是另外一种将工作推后执行的形式,它和前面讨论的tasklet有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许被重新调度甚至是睡眠。
那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列;如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行我们的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。
但是多个工作(函数)是在某个内核线程中依序执行的,前面函数执行很慢,就会影响到后面的函数。在多 CPU 的系统下,一个工作队列可以有多个内核线程,可以在一定程度上缓解这个问题。
2. 基本原理
工作队列的基本原理是将需要执行的任务按顺序排列在队列中, 并提供一组工作线程或者工作进程来处理队列中的任务。 当有新的任务到达时, 它们会被添加到队列的末尾, 工作线程或工作进程从队列的头部获取任务, 并执行相应的处理操作。
工作队列将工作推后以后, 会交给内核线程去执行。 Linux 在启动过程中会创建一个工作者内核线程, 这个线程创建以后处于 sleep 状态。 当有工作需要处理的时候, 会唤醒这个线程去处理工作。
在内核中,我们并不需要自己去创建线程,可以使用“ 工作队列 ” (workqueue)。内核初始化工作队列时,就为它创建了内核线程。以后我们要使用“工作队列”,只需要把“工作”放入“工作队列中”,对应的内核线程就会取出“工作”,执行里面的函数。
在 2.xx 的内核中,工作队列的内部机制比较简单;在现在 4.x 的内核中,工作队列的内部机制做得复杂无比,但是用法是一样的。
3. 工作队列的类型
在内核中, 工作队列包括共享工作队列和自定义工作队列这两种类型。 这两种类型的工作队列具有不同的特点和用途。
- 共享队列是由内核管理的全局工作队列, 用于处理内核中一些系统级任务。 共享工作队列是内核中一个默认工作队列, 可以由多个内核组件和驱动程序共享使用。
- 自定义工作队列是由内核或驱动程序创建的特定工作队列, 用于处理特定的任务。 自定义工作队列通常与特定的内核模块或驱动程序相关联, 用于执行该模块或驱动程序相关的任务。
4. 怎么描述?
4.1 工作
我们把推后执行的任务叫做工作(work),在 Linux 内核中, 使用 work_struct 结构体表示一个工作项:
1 | struct work_struct { |
这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的 work_struct 对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。
4.2 工作队列
上面说的工作以队列结构组织成工作队列(workqueue),工作队列使用 workqueue_struct 结构体表示:
1 | struct workqueue_struct { |
4.3 工作者线程
工作有了,工作队列有了,那谁来处理工作?Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作,使用 struct worker 结构体表示工作者线程
1 | struct worker { |
每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。
4.4. 总结
在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。如下图,流水线相当于工作队列, 流水线上一个个等待处理的物料相当于一个个工作。 机器相当于内核线程或进程。

二、共享工作队列
1. 相关的api
1.1 初始化函数
1.1.1 动态创建工作:INIT_WORK
在实际的驱动开发中, 我们只需要定义工作项(work_struct)即可, 关于工作队列和工作者线程我们基本不用去管。 简单创建工作很简单, 直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作, INIT_WORK 宏定义如下:
1 |
|
INIT_WORK 宏接受两个参数: _work 和 _func, 分别表示要初始化的工作项和工作项的处理函数。示例如下:
1 | void __cfg80211_scan_done(struct work_struct *wk) |
1.1.2 静态创建工作:DECLARE_WORK
也可以使用 DECLARE_WORK 宏在编译时静态地完成工作的创建和初始化, 宏定义如下:
1 |
参数 n 表示定义的工作(work_struct), f 表示工作对应的处理函数。简单示例如下:
1 | static void do_poweroff(struct work_struct *dummy) |
这段代码创建了一个全局静态变量:static work_struct poweroff_work,且被初始化了,其执行函数为do_poweroff()。
1.2 调度/取消调度
1.2.1 schedule_work()
和 tasklet 一样, 工作也是需要调度才能运行的, 工作的调度函数为 schedule_work():
1 | static inline bool schedule_work(struct work_struct *work) |
参数是指向工作项的指针。 这个函数作用是将工作项提交到工作队列中, 并请求调度器在合适的时机执行工作项,该函数会返回一个布尔值, 表示工作项是否成功被提交到工作队列。
这个函数会把 work 提供给系统默认的 work queue: system_wq,它是一个队列:
1 | struct workqueue_struct *system_wq __read_mostly; |
谁来执行 work 中的函数?schedule_work() 函数不仅仅是把 work 放入队列,还会把kworker 线程唤醒。此线程抢到时间运行时,它就会从队列中取出 work,执行里面的函数。
Tips:把work放入工作队列,work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。
1.2.2 cancel_work_sync()
如果想要取消工作项的调度,可以使用 cancel_work_sync() 函数:
1 | bool cancel_work_sync(struct work_struct *work) |
参数是指向工作项的指针。 这个函数的作用是取消该工作项的调度。 如果工作项已经在工作队列中, 它将被从队列中移除。 如果工作项已经在工作队列中, 它将被从队列中移除, 并等待工作项执行完成。 函数返回一个布尔值, 表示工作项是否成功取消。
2. 参考示例
1 | /* 定义工作(work) */ |
3. 共享工作队列demo
3.1 demo源码
源码可以看这里:13_interrupt/07_nodts_workqueue_share · 苏木/imx6ull-driver-demo - 码云 - 开源中国
3.2 开发板测试
拷贝驱动到开发板中,加载驱动:
1 | insmod sdriver_demo.ko |

然后按下按键再释放,就会触发按键中断,按键中断中会提交一个工作项到工作队列,在工作队列的处理函数中,延时了1s:

下面的两次是在工作队列的处理函数未执行完的时候又按了一次。大概也都是间隔了1s。
三、自定义工作队列
共享队列是由内核管理的全局工作队列, 自定义工作队列是由内核或驱动程序创建的特定工作队列, 用于处理特定的任务。接下来就来学习一下自定义工作队列吧。
1. 数据结构
1.1 工作
在 Linux 内核中, 使用 work_struct 结构体表示一个工作项:
1 | struct work_struct { |
这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的 work_struct 对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。
1.2 工作队列
工作以队列结构组织成工作队列(workqueue),工作队列使用 workqueue_struct 结构体表示:
1 | struct workqueue_struct { |
2. 相关的api
2.1 创建工作队列
2.1.1 create_workqueue()
1 |
|
参数 name 是创建的工作队列的名字。使用这个函数可以给每个 CPU 都创建一个 CPU 相关的工作队列。这是一个宏,传入的是名字,我们怎么得到返回值?返回值是什么样的?我们展开看一下,可以看到最后是调用了__alloc_workqueue_key()函数:
1 | struct workqueue_struct *__alloc_workqueue_key(const char *fmt, |
这个函数创建成功返回一个 struct workqueue_struct 类型指针, 创建失败返回 NULL。
2.1.2 create_singlethread_workqueue()
1 |
create_singlethread_workqueue()函数只会给一个 CPU 创建一个 CPU 相关的工作队列。参数 name 是创建的工作队列的名字。 使用这个函数只会给一个 CPU 创建一个 CPU 相关的工作队列。 创建成功之后返回一个 struct workqueue_struct 类型指针, 创建失败返回 NULL。
2.2 调度/取消工作队列
2.2.1 queue_work_on()
当工作队列创建好之后, 需要将要延迟执行的工作项放在工作队列上, 调度工作队列,可以使用queue_work_on()函数:
1 | bool queue_work_on(int cpu, struct workqueue_struct *wq, |
queue_work_on()函数还有其他变种, 比如 queue_work() 函数, 这里略过, 其实思路是一致的, 用于将定义好的工作项立即添加到工作队列中, 并在工作队列可用时立即执行。
该函数有三个参数, 第一个参数是一个整数 cpu, 第二个参数是一个指向 struct workqueue_struct 的指针 wq, 第三个参数是一个指向 struct work_struct 的指针 work。
该函数的返回类型是布尔值, 表示是否成功调度工作队列。
2.2.2 cancel_work_sync()
如果要取消一个已经调度的工作, 使用 cancel_work_sync() 函数:
1 | bool cancel_work_sync(struct work_struct *work) |
函数的作用是取消一个已经调度的工作, 如果被取消的工作已经正在执行, 则会等待他执行完成再返回。
2.3 刷新工作项
在 Linux 内核中,使用 flush_workqueue() 函数将刷新该工作队列中所有已提交但未执行的工作项。
1 | void flush_workqueue(struct workqueue_struct *wq) |
该函数参数是一个指向 struct workqueue_struct 类型的指针 wq。 函数的作用是刷新工作队列, 告诉内核尽快处理工作队列上的工作。
2.4 删除工作队列
如果要删除自定义的工作队列, 使用 destroy_workqueue() 函数 :
1 | void destroy_workqueue(struct workqueue_struct *wq) |
该函数参数是一个指向 struct workqueue_struct 类型的指针 wq。
3. 自定义工作队列demo
3.1 demo源码
demo源码可以看这里:13_interrupt/08_nodts_workqueue_custom · 苏木/imx6ull-driver-demo - 码云 - 开源中国
3.2 开发板测试
我们拷贝驱动到开发板中,加载驱动:
1 | insmod sdriver_demo.ko |

然后按下按键,再抬起,可以看到如下打印信息:

我们还可以看到一条线程,名为 custom_workqueue :
1 | ps -T # 我用的开发板文件系统不需要-T参数也能查看线程情况 |

四、延迟工作
前面我们学习了共享工作队列和自定义工作队列, 为了更形象地理解学习,将流水线比作工作队列, 流水线上一个个等待处理的物料比作一个个工作。 机器比作内核线程或进程。 在这里要学习的延迟工作, 可以类比为将物料延迟一定时间, 再放到生产线上加工。 延迟工作不仅可以在自定义工作队列中实现 也可以在共享工作队列上实现。 现在, 我们对延迟工作有了一个感性的认识, 接下来详细的学习下延迟工作吧。
1. 延迟工作简介
1.1 简介
延迟工作是一种将工作的执行延迟到稍后时间点进行处理的技术。 通常情况下, 当某个任务需要花费较长时间, 不需要立即执行或需要按时执行时, 延迟工作就会派上用场。
延迟工作的基本思想是将任务放入一个队列中, 然后由后台的工作进程会任务调度程序来处理队列中的任务。 任务可以在指定的延迟时间后执行, 也可以根据优先级, 任务类型或者其他条件进行排序和处理。
1.2 应用场景
延迟工作在许多应用场景中都非常有用, 尤其是在需要处理大量任务, 提供系统性能和可靠性的情况下。 以下是一些常用的应用场景:
(1)延迟工作常用于处理那些需要花费较长时间的任务, 比如发送电子邮件, 处理图像等。通过将这些任务放入队列中并延迟执行, 可以避免阻塞应用程序的主线程, 提高系统的响应速度。
(2)延迟工作可以用来执行定时任务, 比如定时备份数据库, 通过将任务设置为在未来的某个时间点执行, 提高系统的可靠性和效率。
比如说开发板上的按键, 现在我们想通过驱动程序读取按键的状态, 那么只需要读取这个按键所连接的 GPIO 的状态就可以了。理想型的按键电压变化过程如图 :

在上图中, 按键没有按下的时候按键值为 1, 当按键在 t1 时刻按键被按下以后按键值就变为 0, 这是最理想的状态。 但是实际的按键是机械结构, 加上刚按下去的一瞬间人手可能也有抖动, 实际的按键电压变化过程如下图 :

在上图中, t1 时刻按键被按下, 但是由于抖动的原因, 直到 t2 时刻才稳定下来, t1 到t2 这段时间就是抖动。 一般这段时间就是十几 ms 左右, 从上图中可以看出在抖动期间会有多次触发, 如果不消除这段抖动的话软件就会误判, 本来按键就按下了一次, 结果软件读取 IO值发现电平多次跳变以为按下了多次。
所以我们需要跳过这段抖动时间再去读取按键的 IO 值,也就是至少要在 t2 时刻以后再去读 IO 值。我们可以使用内核定时器来实现消抖。 按键采用中断驱动方式, 当按键按下以后触发按键中断, 在按键中断中开启一个定时器,定时周期为 10ms, 当定时时间到了以后就会触发定时器中断, 最后在定时器中断处理函数中读取按键的值, 如果按键值还是按下状态那就表示这是一次有效的按键。定时器按键消抖如下图 :

在上图中 t1~t3 这一段时间就是按键抖动, 是需要消除的。 设置按键为下降沿触发, 因此会在 t1、 t2 和 t3 这三个时刻会触发按键中断, 每次进入中断处理函数都会重新开器定时器中断, 所以会在 t1、 t2 和 t3 这三个时刻开器定时器中断。 但是 t1~t2 和 t2~t3 这两个时间段是小于我们设置的定时器中断周期(也就是消抖时间, 比如 10ms), 所以虽然 t1 开启了定时器, 但是定时器定时时间还没到呢 t2 时刻就重置了定时器, 最终只有 t3 时刻开启的定时器能完整的完成整个定时周期并触发中断, 我们就可以在中断处理函数里面做按键处理了, 这就是定时器实现按键防抖的原理, Linux 里面的按键驱动用的就是这个原理!
除了使用定时器方式进行消抖, 也可以使用这里要学习的延迟工作。
2. 数据结构
2.1 struct delayed_work
在 Linux 内核中, 使用 struct delayed_work 来描述延迟工作:
1 | struct delayed_work { |
- work: 这是一个 struct work_struct 类型的成员, 用于表示延迟工作的基本工作结构。 struct work_struct 是表示工作的常见数据结构, 用于定义要执行的工作内容。
- timer: 这是一个 struct timer_list 类型的成员, 用于管理延迟工作的定时器。 struct timer_list 是 Linux 内核中的定时器结构, 用于设置延迟时间和触发工作执行的时机。
使用 struct delayed_work 结构体, 可以将需要执行的工作封装成一个延迟工作, 并使用定时器来控制工作的延迟执行。 通过设置定时器的延迟时间, 可以指定工作在一定时间后执行。
3. 相关的api
3.1 初始化延迟工作函数
3.1.1 DECLARE_DELAYED_WORK
静态定义并初始化延迟工作使用宏 DECLARE_DELAYED_WORK
1 |
n 代表延迟工作的变量名, f 是延迟工作的处理函数。
3.1.2 INIT_DELAYED_WORK
动态定义并初始化延迟工作使用宏 INIT_DELAYED_WORK
1 |
n 代表延迟工作的变量名, f 是延迟工作的处理函数。
3.2 调度/取消调度
3.2.1 schedule_delayed_work()
在共享工作队列上调度延迟工作, 使用函数 schedule_delayed_work() :
1 | static inline bool schedule_delayed_work(struct delayed_work *dwork, |
该函数是一个内联函数, 用于在给定的延迟时间后调度延迟工作执行。
函数参数:
- dwork:是指向延迟工作的指针, 即要被调度的延迟工作。
- delay:表示延迟的时间长度, 以内核时钟节拍数 jiffies 为单位。
3.2.2 queue_delayed_work()
在自定义工作队列上调度延迟工作,使用函数queue_delayed_work():
1 | static inline bool queue_delayed_work(struct workqueue_struct *wq, |
该函数是一个内联函数, 用于将延迟工作加入工作队列后在指定的延迟时间后执行。
函数参数 :
- wq:是指向工作队列结构的指针, 即要将延迟工作加入的目标工作队列。
- dwork:指向延迟工作的指针, 也就是要被加入工作队列的延迟工作。
- delay:表示延迟的时间长度, 以内核时钟节拍数 jiffies 为单位。
3.2.3 cancel_delayed_work_sync()
取消延迟工作调度可以用 cancel_delayed_work_sync()函数:
1 | bool cancel_delayed_work_sync(struct delayed_work *dwork) |
该函数是一个外部声明的函数, 用于取消延迟工作并等待其完成。
函数参数:
- dwork:指向延迟工作的指针, 也就是要被取消的延迟工作。
返回值:返回 true, 说明成功取消延迟工作并等待其完成。 返回 false, 说明无法取消延迟工作或等待其完成。
4. 延迟工作demo
4.1 demo源码
demo源码可以看这里:13_interrupt/09_nodts_workqueue_custom_delay · 苏木/imx6ull-driver-demo - 码云 - 开源中国
4.2 开发板测试
我们拷贝驱动到开发板中,加载驱动:
1 | insmod sdriver_demo.ko |

然后按下按键再抬起,可以看到如下打印信息:

上面的测试是按了一次,延迟3秒后执行工作,工作中又延迟1s,所以一共是4s。下面是连按了两次,会发现只执行了一次工作处理函数。这是因为我们都是同一个硬件中断,上半部肯定会执行2次,但是下半部有可能只执行一次,在软中断那节的笔记中有提到。
五、工作队列传参
前面学习的 tasklet 可以给中断下文传参, 如果我们使用工作队列来实现中断的下半部分, 那么如何用工作队列给中断下文传参呢?
1. 通过工作项
在 Linux 内核的工作队列中, 可以通过使用工作项的方式向工作队列传递参数。 工作项是一个抽象的结构, 可以用于封装需要执行的工作及其相关的参数。
- (1)首先我们定义工作项结构
如下所示, 在结构体 struct work_data 中定义了需要传递给工作项处理函数的参数 a 和 b, 然后定义一个类型为 struct work_data 的变量 test_workqueue_work。
1 | struct work_data { |
- (2)接下来在模块初始化函数 interrupt_irq_init 中创建了一个工作队列 test_workqueue 和一个工作项 test_workqueue_work。
1 | test_workqueue = create_workqueue("test_workqueue"); // 创建工作队列 |
- (3)然后在模块初始化函数中, 为工作项的参数 a 和 b 赋值。
1 | test_workqueue_work.a = 1; |
- (4)当中断触发时, 在中断处理函数 test_interrupt 中, 通过调用 queue_work 函数将工作项test_workqueue_work.test_work 提交到工作队列 test_workqueue 中。
1 | queue_work(test_workqueue, &test_workqueue_work.test_work); |
- (5)然后工作项处理函数 test_work 定义了一个指针 pdata, 将工作项转换为 struct work_data结构, 并通过该结构访问参数 a 和 b。 如下所示:
1 | void test_work(struct work_struct *work) |
这样, 当工作队列被调度执行时, 工作项处理函数 test_work 将能够访问到传递给工作项的参数 a 和 b,并在内核日志中打印他们的值。
Tips:注意, 工作项处理函数中的 container_of 宏用于从工作项结构的指针获取整个 struct work_data 结构的指针。 这样可以通过指针偏移来访问工作项结构中的其他字段, 例如参数 a和 b。
2. 工作队列传参demo
2.1 demo源码
demo源码可以看这里:13_interrupt/10_nodts_workqueue_custom_param · 苏木/imx6ull-driver-demo - 码云 - 开源中国
2.2 开发板测试
我们拷贝驱动到开发板中,加载驱动:
1 | insmod sdriver_demo.ko |

然后按下按键再抬起,可以看到如下打印信息:

我这里是这样赋值的:
1 | p_chrdev->irq_key.key_work.a = p_chrdev->irq_key.gpio; |
所以打印出来的就是gpio引脚号和中断号。
六、并发管理工作队列
在现代的软件开发中, 我们常常面临着需要同时处理多个任务的挑战。 这些任务可能是并行的、 独立的, 或者需要以某种顺序进行处理。 为了高效地管理这些并发任务, 我们需要一种有效的机制来协调它们的执行。 这就是并发管理工作队列发挥作用的地方。
1. 工作队列的实现
前面,我们学习了共享工作队列和自定义工作队列, 在使用工作队列时,我们首先定义一个work结构体, 然后将work添加到workqueue(工作队列)中, 最后worker thread执行 workqueue。 当工作队列中有新 work 产生时, 工作线程(worker thread) 会执行工作队列中每个 work。 当执行完结束的时候, worker thread 会睡眠, 等到新的中断产生, work 再继续添加到工作队列, 然后工作线程执行每个工作, 周而复始。
在单核线程的系统中, 通常会为每个 CPU(核心) 初始化一个工作线程并关联一个工作队列。 这种默认设置确保每个 CPU 都有一个专门的线程来处理与其绑定的工作队列上的工作项。 如下图 :

在多核线程系统中, 工作队列的设计与单核线程系统有所不同。 在多核线程系统中, 通常会存在多个工作队列, 每个工作队列与一个工作线程(Worker Thread) 绑定。 这样可以充分利用多个核心的并行处理能力。 如下图 :

当有新的工作项产生时, 系统需要决定将其分配给哪个工作队列。 一种常见的策略是使用负载均衡算法, 根据工作队列的负载情况来平衡分配工作项, 以避免某个工作队列过载而导致性能下降。 每个工作队列独立管理自己的工作项。 当有新的工作项添加到工作队列时, 工作线程会从其关联的工作队列中获取待执行的工作项, 并执行相应的处理函数。 在多核线程系统中,多个工作线程可以同时执行各自绑定的工作队列中的工作项。 这样可以实现并行处理, 提高系统的整体性能和响应速度。
2. workqueue 队列弊端
了解了工作队列是如何实现的, 接下来我们看看传统的工作队列有什么弊端呢?假如说有三个 work 放到了同一个工作队列上, 接下来 CPU 会启动工作线程去执行这三个work, 如下图 :

在上图中, 工作项 w0、 w1、 w2 被排队到同一个 CPU 上的绑定工作队列上。 w0 工作项执行的时候, 先工作 5 毫秒, 然后休眠 10 毫秒, 然后CPU再工作 5 毫秒, 然后完成。 工作项 w1 和 w2 都是工作 5ms, 然后休眠 10 ms, 然后完成。 传统工作队列的弊端如下所示:
(1)在工作项 w0 工作甚至是睡眠时, 工作项 w1 w2 是排队等待的, 在繁忙的系统中, 工作队列可能会积累大量的待处理工作项, 导致任务调度的延迟, 这可能会影响系统的响应性能,并增加工作项的处理时间。
(2)在工作队列中, 不同的工作项可能具有不同的处理时间和资源需求。 如果工作项的处理时间差异很大, 一些工作线程可能会一直忙于处理长时间的工作项, 而其他工作线程则处于空闲状态, 导致资源利用不均衡。
(3)在多线程环境下, 多个工作线程同时访问和修改工作队列可能会导致竞争条件的发生。为了确保数据的一致性和正确性, 需要采用适当的同步机制, 如锁或原子操作, 来保护共享数据, 但这可能会引入额外的同步开销。
(4)工作队列通常按照先进先出(FIFO) 的方式处理工作项, 缺乏对工作项优先级的细粒度控制。 在某些场景下, 可能需要根据工作项的重要性或紧急程度进行优先级调度, 而工作队列本身无法提供这种级别的优先级控制。
(5)当工作线程从工作队列中获取工作项并执行时, 可能需要频繁地进行上下文切换, 将处理器的执行上下文从一个线程切换到另一个线程。 这种上下文切换开销可能会影响系统的性能和效率。
3. 什么是并发管理工作队列?
通过上面的了解,我们认识到传统的工作队列无论是单核系统还是多核系统上都是有缺陷的。 比如无法充分利用多核处理器的计算能力以及对于不同优先级的工作项无法提供公平的调度。 为了解决这些问题, Con Kolivas 提出了 CMWQ 调度算法。
CMWQ 全称是 concurrency Managed Workqueue, 意为并发管理工作队列。 并发管理工作队列是一种并发编程模式, 用于有效地管理和调度待执行的任务或工作项。 它通常用于多线程或多进程环境中, 以实现并发执行和提高系统的性能。 CMWQ 工作实现如下图 :

当我们需要在一个系统中同时处理多个任务或工作时, 使用并发管理工作队列是一种有效的方式。
想象一下, 我们是一个餐厅的服务员, 有很多顾客同时来到餐厅用餐。 为了提高效率, 我们需要将顾客的点菜请求放到一个队列中, 这就是工作队列。 然后, 我们和其他服务员可以从队列中获取顾客的点菜请求, 每个服务员独立地为顾客提供服务。 通过这种方式, 我们可以并发地处理多个顾客的点菜请求, 而不需要等待上一个顾客点完菜再去处理下一个顾客的请求。 每个服务员可以独立地从队列中获取任务, 并根据需要执行相应的服务。 这种独立获取任务的过程就是从工作队列中取出任务并执行的过程。
通过并发管理工作队列, 我们能够更高效地处理顾客的点菜请求, 提高服务的速度和质量。同时, 这种方式也能够更好地利用我们的工作能力, 因为每个服务员都可以独立处理任务, 而不会相互干扰或等待。
总的来说, 通过并发管理工作队列, 我们可以同时处理多个任务或工作, 提高系统的并发性和性能。 每个任务独立地从队列中获取并执行, 这种解耦使得整个系统更加高效、 灵活, 并且能够更好地应对多任务的需求。
4. 相关的api
4.1 alloc_workqueue()
alloc_workqueue() 是 Linux 内核中的一个函数, 用于创建和分配一个工作队列。 工作队列是一种用于管理和调度工作项的机制, 可用于实现并发处理和异步任务处理。
1 |
函数参数:
- fmt: 指定工作队列的名称格式。
- flags: 指定工作队列的标志, 可以控制工作队列的行为和属性, 如 WQ_UNBOUND 表示无绑定的工作队列, WQ_HIGHPRI 表示高优先级的工作队列等。
- max_active: 指定工作队列中同时活跃的最大工作项数量。
返回值:返回一个指向工作队列结构体(struct workqueue_struct) 的指针, 或者返回 NULL 表示创建失败。
5. 并发管理工作队列demo
5.1 demo源码
demo源码可以看这里:13_interrupt/11_nodts_workqueue_custom_cmwq · 苏木/imx6ull-driver-demo - 码云 - 开源中国
5.2 开发板测试
这个demo的现象并不是很明显,这里先大概了解一下:

七、中断线程化
中断线程化是实时 Linux 项目开发的一个新特性, 目的是降低中断处理对系统实时延迟的影响。
1. 什么是中断线程化 ?
中断线程化是一种优化技术, 用于提高多线程程序的性能。 想象一下, 我们正在做一项任务, 但是总是被别人的打扰所中断, 每次都要停下手头的工作去处理别人的事情。 这样频繁的中断会让我们的工作效率变低, 因为我们需要反复切换任务, 无法专心做好自己的工作。
在多线程程序中, 也存在类似的问题。 有时硬件或其他事件会发出中断信号, 打断正在执行的线程, 需要切换到中断处理程序去处理这些事件。 这种频繁的中断切换会导致额外的开销和延迟, 影响程序的性能。
为了解决这个问题, 中断线程化提出了一种优化方案。 它将中断处理程序从主线程中独立出来, 创建一个专门的线程来处理这些中断事件。 这样, 主线程就不再受到中断的干扰, 可以专注于自己的工作, 不再频繁地被打断。
中断线程化的核心思想是将中断处理和主线程的工作分开, 让它们可以并行执行。 中断线程负责处理中断事件, 而主线程负责执行主要的工作任务。 这样一来, 不仅可以减少切换的开销, 还可以提高整个程序的响应速度和性能。
需要注意的是, 中断线程化还需要处理线程之间的同步和数据共享问题。 因为中断线程和主线程可能会同时访问和修改共享的数据, 所以需要合理地进行同步操作, 确保数据的一致性和正确性。
总而言之, 中断线程化是一种优化技术, 通过将中断处理和主线程的工作分开, 提高多线程程序的性能。 让主线程不再频繁被中断, 可以专注于自己的工作, 从而提高程序的效率和响应速度。
中断线程化的处理仍然可以看作是将原来的中断上半部分和中断下半部分。 上半部分还是用来处理紧急的事情, 下半部分也是出路比较耗时的操作, 但是下半部分会交给一个专门的内核线程来处理。 这个内核线程只用于这个中断。 当发生中断的时候, 会唤醒这个内核线程, 然后由这个内核线程来执行中断下半部分的函数。
2. 相关的api
2.1 request_threaded_irq()
request_threaded_irq() 是 Linux 内核中用于请求并注册一个线程化的中断处理函数的函数。 定义如下:
1 | int request_threaded_irq(unsigned int irq, irq_handler_t handler, |
函数参数:
- irq: 中断号, 表示要请求的中断线路。
- handler: 是在发生中断时首先要执行的处理程序, 非常类似于上半部, 该函数最后会返回 IRQ_WAKE_THREAD 来唤醒中断, 一般 handler 设为 NULL, 用系统提供的默认处理。
- thread_fn: 线程化的中断处理函数, 非常类似于下半部。 如果此处设置为 NULL 则表示没有使用中断线程化。
- irqflags: 中断标志, 用于指定中断的属性和行为。
- devname: 中断的名称, 用于标识中断请求的设备。
- dev_id: 设备标识符, 用于传递给中断处理函数的参数。
返回值:返回一个整数值, 表示中断请求的结果。 如果中断请求成功, 返回值为 0, 否则返回一个负数错误代码。
Tips:我们可以提供上半部函数,也可以不提供:
- 如果不提供:内核会提供默认的上半部处理函数:irq_default_primary_handler,它是直接返回 IRQ_WAKE_THREAD。
- 如果提供的话:返回值必须是: IRQ_WAKE_THREAD。在 thread_fn 中,如果中断被正确处理了,应该返回 IRQ_HANDLED。
2.2 free_irq()
1 | const void *free_irq(unsigned int irq, void *dev_id) |
这个函数用于释放中断。
2.3 简单示例
1 |
|
3. request_threaded_irq()机制
前面我们学习了这个函数:

我们可以只提供 thread_fn,系统会为这个函数创建一个内核线程。发生中断时,系统会立刻调用 handler 函数,然后唤醒某个内核线程,内核线程再来执行thread_fn 函数。 那,这是怎么过程呢?
3.1 数据结构关系
调用 request_threaded_irq() 后内核的数据结构关系如下:

3.2 request_threaded_irq()函数
我们来看一下这个函数:
1 | int request_threaded_irq(unsigned int irq, irq_handler_t handler, |
这个函数中会先分配,设置一个 irqaction 结构体,后面会调用 __setup_irq() 函数做一些进一步处理。
3.2.1 __setup_irq()
我们来看一下__setup_irq(),这个函数超级长,上百行,我们主要看关键的部分:
1 | static int |
这里面我们主要关注 setup_irq_thread() 函数。
3.2.2 setup_irq_thread()
来看一下 setup_irq_thread() 函数:
1 | static int |
可以看到是在这里创建了对应的线程。
3.3 中断的执行过程
对于 GPIO 中断,这部分我没有去试,直接参考的伟东山的linux教程,教程中使用 QEMU 的调试功能找出了所涉及的函数调用,其他板子可能稍有不同。调用关系如下,反过来看:
1 | Breakpoint 1, gpio_keys_gpio_isr (irq=200, dev_id=0x863e6930) at drivers/input/keybo |
我们只需要分析__handle_irq_event_percpu():

线程的处理函数为 irq_thread():

4. 中断线程化demo
4.1 demo源码
demo源码可以看这里:13_interrupt/12_nodts_threaded_irq · 苏木/imx6ull-driver-demo - 码云 - 开源中国
4.2 开发板测试
这个其实就和前面工作队列那些现象一样了:

但是我们可以看到有一条名为 irq/79-key-0 的线程被创建了:

加载驱动前后的线程情况:

线程的名字其实就是我们申请的中断的时候的名字组合出来的:

参考资料: