Skip to content

LV070-工作队列简介

一、概述

在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但是毕竟整个中断的处理还没走完,这期间 APP 是无法执行的。假设下半部要执行 1、 2 分钟,在这 1、 2 分钟里 APP 都是无法响应的。

这谁受得了?所以,如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和 APP 都一样竞争执行, APP 有机会执行,系统不会卡顿。这个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多这样的线程:

shell
ps -A | grep kworker
image-20250322094204703

总的来说,下半部要做的事情太多并且很复杂,我们就可以考虑使用工作队列。接下来就来了解一下吧。

二、工作队列简介

1. 什么是工作队列

工作队列是实现中断下半部分的机制之一, 是一种用于管理任务的数据结构或机制。 它通常用于多线程, 多进程或分布式系统中, 用于协调和分配待处理的任务给可用的工作线程或工作进程。

工作队列(work queue)是另外一种将工作推后执行的形式,它和前面讨论的 tasklet 有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是 工作队列允许被重新调度甚至是睡眠。

那么,什么情况下使用工作队列,什么情况下使用 tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列;如果推后执行的任务不需要睡眠,那么就选择 tasklet。另外,如果需要用一个可以重新调度的实体来执行我们的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的 I/O 操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用 tasklet。

但是多个工作(函数)是在某个内核线程中依序执行的,前面函数执行很慢,就会影响到后面的函数。在多 CPU 的系统下,一个工作队列可以有多个内核线程,可以在一定程度上缓解这个问题。

2. 基本原理

工作队列的基本原理是将需要执行的任务按顺序排列在队列中, 并提供一组工作线程或者工作进程来处理队列中的任务。 当有新的任务到达时, 它们会被添加到队列的末尾, 工作线程或工作进程从队列的头部获取任务, 并执行相应的处理操作。

工作队列将工作推后以后, 会交给内核线程去执行。 Linux 在启动过程中会创建一个工作者内核线程, 这个线程创建以后处于 sleep 状态。 当有工作需要处理的时候, 会唤醒这个线程去处理工作。

在内核中,我们并不需要自己去创建线程,可以使用“ 工作队列 ” (workqueue)。内核初始化工作队列时,就为它创建了内核线程。以后我们要使用“工作队列”,只需要把“工作”放入“工作队列中”,对应的内核线程就会取出“工作”,执行里面的函数。

在 2.xx 的内核中,工作队列的内部机制比较简单;在现在 4.x 的内核中,工作队列的内部机制做得复杂无比,但是用法是一样的。

3. 工作队列的类型

在内核中, 工作队列包括共享工作队列和自定义工作队列这两种类型。 这两种类型的工作队列具有不同的特点和用途。

  • 共享队列 是由内核管理的全局工作队列, 用于处理内核中一些系统级任务。 共享工作队列是内核中一个默认工作队列, 可以由多个内核组件和驱动程序共享使用。
  • 自定义工作队列 是由内核或驱动程序创建的特定工作队列, 用于处理特定的任务。 自定义工作队列通常与特定的内核模块或驱动程序相关联, 用于执行该模块或驱动程序相关的任务。

三、怎么描述?

1. 工作

我们把推后执行的任务叫做工作(work),在 Linux 内核中, 使用 work_struct 结构体表示一个工作项:

c
struct work_struct {
	atomic_long_t data;
	struct list_head entry; // 连接所有工作的链表
	work_func_t func;       // 要执行的函数
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的 work_struct 对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。

2. 工作队列

上面说的工作以队列结构组织成工作队列(workqueue),工作队列使用 workqueue_struct 结构体表示:

c
struct workqueue_struct {
	struct list_head	pwqs;		/* WR: all pwqs of this wq */
	struct list_head	list;		/* PR: list of all workqueues */

	//......
};

3. 工作者线程

工作有了,工作队列有了,那谁来处理工作?Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作,使用 struct worker 结构体表示工作者线程

c
struct worker {
	/* on idle list while idle, on busy hash table while busy */
	union {
		struct list_head	entry;	/* L: while idle */
		struct hlist_node	hentry;	/* L: while busy */
	};

	//......
	/* used only by rescuers to point to the target workqueue */
	struct workqueue_struct	*rescue_wq;	/* I: the workqueue to rescue */
};

每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。

4. 总结

在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。如下图,流水线相当于工作队列, 流水线上一个个等待处理的物料相当于一个个工作。 机器相当于内核线程或进程。

image-20250322095959293

参考资料:

中断处理下半部机制-CSDN 博客

软中断(softirq)机制_软中断机制-CSDN 博客