对于Linux中等待队列和工作队列的讲解和使用|RK3399

前言

在 Linux 字符设备驱动开发中,中断处理、进程同步、阻塞 IO 实现是三大核心基础能力,而等待队列和工作队列正是实现这些能力的底层基石。

  • 等待队列:解决了进程等待特定事件时的 CPU 资源浪费问题,是内核实现阻塞 IO 的核心载体,应用层绝大多数的 IO 阻塞逻辑,底层都是由等待队列实现的。
  • 工作队列:是 Linux 中断 "顶半部 - 底半部" 机制的核心实现之一,完美解决了中断服务函数不能执行耗时操作、不能休眠的核心约束。

一、等待队列(Wait Queue)

1.1 为什么需要等待队列?

我们先思考一个场景:应用层调用read函数读取按键值,此时没有按键按下,该如何处理?如果用死循环轮询按键电平,会让进程一直占用 CPU,导致系统资源被极大浪费,这显然是不可行的。

而等待队列的核心价值,就是实现事件的条件等待:希望等待特定事件的进程,将自己放入等待队列,主动放弃 CPU 进入休眠状态;当事件发生时,内核唤醒队列中的对应进程,进程再继续执行后续逻辑。

等待队列在内核中应用极广,可用于中断处理、进程同步、延时等场景,是驱动实现阻塞 IO 的标准方案。

1.2 等待队列的核心数据结构

等待队列的核心是等待队列头 ,结构体类型为wait_queue_head_t,定义在头文件<linux/wait.h>中,原型如下:

cpp 复制代码
struct __wait_queue_head {
    spinlock_t lock;               // 自旋锁,保护等待队列的临界资源
    struct list_head task_list;    // 循环链表头,用于挂载等待的进程
};
typedef struct __wait_queue_head wait_queue_head_t;

注意:驱动开发者无需直接操作结构体内部成员,所有对等待队列的操作,都通过内核封装好的 API 完成。

1.3 等待队列的核心工作原理

等待队列的完整工作流程,分为进程休眠入队事件触发唤醒两个核心环节。

1.3.1 休眠入队过程
  1. 先定义并初始化一个等待队列头,它就是等待队列的本体;
  2. 在需要等待事件的代码位置,调用wait_event系列函数,判断等待条件condition
    • condition为真:不进入休眠,直接往下执行;
    • condition为假:将当前进程加入等待队列,设置为对应睡眠状态,放弃 CPU 调度;
  3. 进程会一直保持休眠,直到被唤醒且condition为真,才会完全退出等待状态。
1.3.2 唤醒出队过程
  1. 当事件发生时(通常在中断处理函数、定时器回调中),调用wake_up系列函数;
  2. 函数将等待队列中的目标进程置为TASK_RUNNING可运行状态,加入内核调度队列;
  3. 进程被调度器调度运行后,会再次检查condition,为真则完全退出等待,继续执行后续代码。
1.3.3 内核进程的两种核心睡眠状态

等待队列的不同 API,对应了内核两种不同的进程睡眠状态,这是选择 API 的核心依据:

睡眠状态 说明 特点
TASK_UNINTERRUPTIBLE 不可中断睡眠 进程仅当等待条件满足时才会被唤醒,无法被外部信号打断,极端情况下可能导致进程永久休眠
TASK_INTERRUPTIBLE 可中断睡眠 进程既可以在条件满足时被唤醒,也可以被外部信号提前唤醒,驱动开发中最常用,兼顾了阻塞功能和系统健壮性

1.4 等待队列核心 API 全解析

等待队列的 API 分为三大类:初始化 API、休眠 API、唤醒 API,均定义在<linux/wait.h>中。

1.4.1 等待队列初始化 API

分为静态初始化和动态初始化两种方式,功能完全等价,适用于不同的代码场景。

① 静态初始化:DECLARE_WAIT_QUEUE_HEAD (name)
cpp 复制代码
// 宏原型
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
  • 功能:定义并直接初始化一个名为name的等待队列头变量
  • 适用场景:全局变量定义,驱动开发中最常用的初始化方式
  • 示例:
cpp 复制代码
// 全局定义并初始化一个按键等待队列头
static DECLARE_WAIT_QUEUE_HEAD(key_wq);
② 动态初始化:init_waitqueue_head (q)
cpp 复制代码
// 宏原型
#define init_waitqueue_head(q)\
do {\
    static struct lock_class_key __key;\
    __init_waitqueue_head((q), #q, &__key);\
} while (0)
  • 功能:动态初始化一个已定义的等待队列头变量
  • 适用场景:局部变量、结构体内嵌的等待队列头
  • 示例:
cpp 复制代码
wait_queue_head_t key_wq;        // 定义等待队列头变量
init_waitqueue_head(&key_wq);    // 动态初始化
1.4.2 进程休眠 API

核心是wait_event系列宏,不同宏对应不同的睡眠特性,驱动开发中 90% 的场景都会使用wait_event_interruptible

① wait_event(wq, condition)
  • 功能:将当前进程加入等待队列wq,进入不可中断睡眠 状态,直到condition为真才会被唤醒
  • 参数:
    • wq:等待队列头(非指针)
    • condition:等待条件,布尔表达式,为 0 时进入休眠,非 0 时不进入休眠
  • 注意:该函数无法被信号打断,可能导致进程永久休眠,驱动开发中极少使用
② wait_event_interruptible(wq, condition)
  • 功能:驱动开发最核心的休眠 API,将当前进程加入等待队列wq,进入可中断睡眠状态
  • 返回值:
    • 0:条件condition成立,进程正常被唤醒
    • -ERESTARTSYS:进程被外部信号打断唤醒
  • 示例:
cpp 复制代码
int ret;
// 等待按键事件发生,key_event_flag是事件标志位,事件发生时置1
ret = wait_event_interruptible(key_wq, key_event_flag != 0);
if (ret == -ERESTARTSYS) {
    printk("进程被信号打断,退出阻塞\n");
    return -ERESTARTSYS;
}
// 条件满足,执行后续的按键值读取逻辑
③ wait_event_timeout(wq, condition, timeout)
  • 功能:不可中断睡眠 + 超时机制,到达指定时间后,即使条件不满足也会强制唤醒进程,避免永久休眠
  • 参数:timeout为超时时间,单位为内核时钟节拍,可使用HZ(内核 1 秒对应的时钟节拍数)设置,例如5*HZ代表 5 秒超时
  • 返回值:0 = 超时返回;>0 = 条件满足正常唤醒,返回剩余的时钟节拍数
④ wait_event_interruptible_timeout(wq, condition, timeout)
  • 功能:可中断睡眠 + 超时机制,功能最完善的休眠 API,兼顾了可中断特性和防死等能力
1.4.3 进程唤醒 API

核心是wake_up宏,必须与休眠 API 成对使用:

cpp 复制代码
// 宏原型
#define wake_up(x)__wake_up(x, TASK_NORMAL, 1, NULL)
  • 功能:唤醒等待队列x中的进程,可唤醒处于TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE状态的进程
  • 参数:x为等待队列头的指针
  • 注意:唤醒操作通常放在事件发生的位置,最常见的是中断服务函数中;必须先修改condition为真,再调用唤醒函数,否则进程被唤醒后会再次进入休眠
  • 示例:
cpp 复制代码
// 按键中断服务函数中,事件发生后唤醒等待队列
key_event_flag = 1;  // 先将等待条件置为真
wake_up(&key_wq);    // 唤醒等待队列中的进程

1.5 等待队列典型使用场景

等待队列最经典的应用,就是按键驱动的阻塞 IO 实现

  1. 应用层调用read函数读取按键值,进入驱动的file_operations->read接口;
  2. 若没有按键按下,调用wait_event_interruptible让进程进入休眠,阻塞read调用;
  3. 按键按下触发中断,在中断服务函数中设置事件标志,调用wake_up唤醒进程;
  4. 进程被唤醒后,读取按键值,通过copy_to_user返回给应用层,完成read调用。

【等讲完工作队列将结合一起作示例使用】


二、工作队列(Work Queue)

2.1 为什么需要工作队列?------ 中断顶半部 - 底半部机制

在 Linux 中断编程中,有两个不可突破的核心约束:

  1. 中断服务程序不属于任何进程上下文,绝对不能执行休眠、调度相关的函数
  2. 中断处理必须尽可能快地完成,否则会屏蔽同类型中断,影响系统实时性,甚至导致中断丢失。

但实际开发中,很多中断触发后需要执行耗时操作(比如按键消抖、大量数据处理、复杂逻辑运算),如果都放在中断服务函数中,会严重违反上述约束。

为此,Linux 内核提出了中断顶半部 - 底半部机制:

  • 顶半部:就是中断服务函数本身,只完成最紧急、耗时极短的操作(比如清除中断标志、读取寄存器状态、登记中断事件),然后立即退出;
  • 底半部:完成中断事件中耗时、不紧急的操作,内核会在中断关闭的安全窗口,调度执行底半部代码。

工作队列,就是 Linux 内核中最常用的底半部实现机制之一,它的核心优势是:

  • 工作队列的代码运行在内核进程上下文,可以正常执行休眠、调度操作;
  • 可以将耗时操作完全推迟到内核线程中执行,不占用中断处理的时间窗口。

2.2 工作队列的核心原理

Linux 系统在启动时,会创建默认的内核工作者线程,这些线程启动后处于睡眠状态,循环等待工作队列中的任务被调度执行。

工作队列的核心概念分为两个:

  • 工作(Work) :用struct work_struct结构体表示,里面封装了要推迟执行的函数(工作函数),是需要执行的任务本体;
  • 队列(Queue):用来挂载工作的内核链表,内核工作者线程会循环从队列中取出工作,执行对应的工作函数。

2.3 工作队列的分类

工作队列分为共享工作队列自定义工作队列两大类,对应不同的使用场景,核心对比如下:

类型 说明 优点 缺点
共享工作队列 内核系统默认创建的全局工作队列,所有驱动模块都可以直接挂载工作 无需自己创建队列,资源消耗极低,使用简单**【常用】** 队列中的工作会受到其他模块工作的影响,若前面的工作耗时过长,会导致当前工作延迟执行
自定义工作队列 驱动开发者自己创建的工作队列,对应专属的内核工作线程 工作执行不受其他模块影响,实时性更好 需要创建新的内核线程,系统资源开销大,不建议大量创建

2.4 工作队列核心数据结构

工作的核心结构体是struct work_struct,定义在<linux/workqueue.h>中,原型如下:

cpp 复制代码
struct work_struct {
    atomic_long_t data;          // 工作状态相关参数
    struct list_head entry;      // 挂载到工作队列时使用的链表节点
    work_func_t func;            // 工作执行函数指针
};

其中工作函数的原型固定为:

cpp 复制代码
typedef void (*work_func_t)(struct work_struct *work);

注意:驱动开发者无需直接操作结构体内部成员,所有初始化、调度操作都通过内核封装的 API 完成。

2.5 工作队列核心 API 全解析

工作队列的 API 分为三大类:工作初始化 API、工作调度 API、自定义工作队列管理 API。

2.5.1 工作初始化 API

核心是INIT_WORK宏,用于初始化工作结构体并绑定工作函数:

cpp 复制代码
// 宏原型
INIT_WORK(struct work_struct *work, work_func_t func)
  • 功能:初始化一个work_struct工作结构体,绑定对应的工作执行函数
  • 参数:
    • work:需要初始化的工作结构体的地址
    • func:工作触发后要执行的工作函数
  • 示例:
cpp 复制代码
// 定义工作结构体
static struct work_struct key_work;

// 工作函数实现:底半部耗时操作放在这里
static void key_work_func(struct work_struct *work)
{
    // 工作队列运行在进程上下文,可以正常使用休眠延时
    msleep(20); // 按键消抖延时20ms
    printk("按键底半部处理完成\n");
    // 后续可执行按键值读取、input事件上报等操作
}

// 驱动初始化中:初始化工作,绑定工作函数
INIT_WORK(&key_work, key_work_func);
2.5.2 工作调度 API

分为共享队列调度和自定义队列调度两类。

① 共享工作队列调度:schedule_work
cpp 复制代码
// 函数原型
bool schedule_work(struct work_struct *work)
  • 功能:将初始化好的工作,挂载到内核默认的共享工作队列中,等待内核线程执行
  • 参数:work为要调度的工作结构体地址
  • 特点:使用简单,资源消耗低,适合耗时短、对实时性要求不高的场景,是驱动开发中最常用的调度 API
  • 示例:
cpp 复制代码
// 中断顶半部中:调度工作到共享队列,立即退出中断
schedule_work(&key_work);
② 自定义工作队列调度:queue_work
cpp 复制代码
// 函数原型
bool queue_work(struct workqueue_struct *wq, struct work_struct *work)
  • 功能:将工作挂载到自定义的工作队列中,由自定义队列对应的专属内核线程执行
  • 参数:
    • wq:自定义工作队列的地址
    • work:要调度的工作结构体地址
2.5.3 自定义工作队列管理 API

如果需要创建专属的自定义工作队列,使用以下 API:

① 创建自定义工作队列:create_singlethread_workqueue
cpp 复制代码
// 宏原型
create_singlethread_workqueue(name)
  • 功能:创建一个单线程的自定义工作队列,对应一个专属的内核工作者线程
  • 参数:name为自定义工作队列的名字,用于内核标识
  • 返回值:成功返回工作队列结构体指针,失败返回 NULL
  • 示例:
cpp 复制代码
static struct workqueue_struct *my_wq;

// 驱动初始化中:创建自定义工作队列
my_wq = create_singlethread_workqueue("key_wq");
if (!my_wq) {
    printk("创建自定义工作队列失败\n");
    return -ENOMEM;
}
② 刷新工作队列:flush_workqueue
cpp 复制代码
// 函数原型
void flush_workqueue(struct workqueue_struct *wq)
  • 功能:通知内核尽快处理完指定工作队列中的所有工作,通常在销毁队列前调用,确保队列中没有未完成的工作
  • 参数:wq为自定义工作队列的地址
③ 销毁自定义工作队列:destroy_workqueue
cpp 复制代码
// 函数原型
void destroy_workqueue(struct workqueue_struct *wq)
  • 功能:销毁自定义工作队列,释放对应的内核资源,必须在驱动卸载时调用,避免内存泄漏
  • 参数:wq为要销毁的自定义工作队列的地址
  • 示例:
cpp 复制代码
// 驱动卸载时:先刷新队列,再销毁
flush_workqueue(my_wq);
destroy_workqueue(my_wq);

2.6 工作队列典型使用场景

工作队列最经典的应用,就是按键中断的顶半部 - 底半部实现

  1. 按键按下触发 GPIO 中断,进入中断服务函数(顶半部);
  2. 顶半部仅执行调度工作队列的操作,立即退出中断,整个中断处理耗时极短;
  3. 内核线程调度执行工作函数(底半部),完成按键消抖、电平读取、事件上报等耗时操作。

三、等待队列与工作队列的核心区别与组合使用

3.1 核心区别对比

特性 等待队列 工作队列
核心用途 实现进程的条件等待、阻塞 IO 实现中断底半部,推迟耗时操作执行
运行上下文 调用 wait_event 的用户进程上下文 内核工作者线程的进程上下文
核心功能 让进程休眠,等待事件发生后唤醒 把任务推迟到内核线程中异步执行
与中断的关系 通常在中断中调用 wake_up 唤醒队列 通常在中断中调度工作,中断退出后执行
休眠能力 休眠是核心功能 工作函数中可以正常执行休眠操作

3.2 驱动开发中的组合使用

在实际的驱动开发中,等待队列和工作队列经常组合使用,最典型的就是完整的按键 input 子系统驱动:

  1. 驱动初始化:申请 GPIO、设置输入模式、申请中断、初始化工作队列、初始化等待队列、注册 input 设备;
  2. 按键按下触发中断,进入顶半部:调度工作队列,立即退出中断;
  3. 工作队列底半部执行:按键消抖、读取按键电平、上报 input 事件、设置事件标志、唤醒等待队列;
  4. 应用层 read 调用:通过等待队列阻塞,事件发生后被唤醒,读取按键值返回。
cpp 复制代码
#include <linux/module.h>   	//和模块编程相关的头文件
#include <linux/init.h>			//和初始化相关的头文件
#include <linux/gpio.h>			//和gpio库相关的头文件
#include <linux/interrupt.h>	//和中断相关的头文件
#include <linux/workqueue.h>	//和中断相关的头文件
#include <linux/delay.h>		//和延时相关的头文件
#include <linux/miscdevice.h>	//和杂项设备相关的头文件
#include <linux/fs.h>			//和文件操作相关的头文件
#include <linux/uaccess.h>			//和文件操作相关的头文件



#define KEYGPIO 5//按键对应io



struct work_struct work;//创建工作队列结构体
wait_queue_head_t wq;//创建等待队列头

int irq;//创建变量储存中断编号
int condition = 0;//等待队列休眠条件
int flag = 0;//按键按下标志位



//实现中断服务函数
irqreturn_t key_irq_handler_t(int irq, void *arg)
{
	//调度工作即可
	schedule_work(&work);
	return IRQ_HANDLED;
}




//创建工作函数
void key_work_test(struct work_struct *workp)
{
	//关闭中断
	disable_irq(irq);

	//读取电平和做消抖
	u32 temp = gpio_get_value(KEYGPIO);
	
	//延时
	msleep(20);

	if(temp == gpio_get_value(KEYGPIO))
	{
		if(temp && flag)
		{
			flag=0;
			printk("按键松开!!!\r\n");
			condition = 1;
		}else if(temp == 0 && flag == 0)
		{
			flag=1;
			printk("按键按下!!!\r\n");
			condition = 1;
		}
		
		//唤醒等待队列
		wake_up(&wq);
	}
	
	//打开中断
	enable_irq(irq);
}

/*********************************杂项设备模型******************************************/
//读函数实现
ssize_t xxx_read(struct file *file, char __user *buf, size_t count, loff_t *off)
{
	//等待按键发生动作
	wait_event_interruptible(wq, condition);
	condition = 0;//重置休眠条件

	if(flag)
	{
		copy_to_user(buf,"press ",6);
	}else
	{
		copy_to_user(buf,"releas",6);
	}
	return 0;
}


//创建文件操作相关的结构体
struct file_operations xxx_fops={
	.read = xxx_read,
};

//创建杂项设备核心结构体
struct miscdevice led_misc={
	.minor = 255,
	.name = "misc_test",
	.fops = &xxx_fops,
};
/*********************************回调函数部分*****************************************/

//执行回调函数,对应执行的功能
//static  静态函数:仅本文件可用
//__init  给内核标识用的,我们不用管
static int __init xxx_init(void)
{
	int err = 0;
    //初始化工作队列
	INIT_WORK(&work, key_work_test);

	//初始化等待队列头
	init_waitqueue_head(&wq);
	
	/*********************向下操作物理硬件**************************/
	//初始化按键
		//占用gpio口
	err = gpio_request(KEYGPIO, "test");
	if(err)
	{
		printk("gpio_request error\r\n");
		goto gpio_request_err;
	}

		//设置gpio为输入模式
	err = gpio_direction_input(KEYGPIO);
	if(err)
	{
		printk("gpio_direction_input error\r\n");
		goto gpio_direction_input_err;
	}

	//注册中断
		//获取中断编号
	irq = gpio_to_irq(KEYGPIO);
	if(irq < 0)
	{
		err = irq;
		printk("gpio_to_irq error\r\n");
		goto gpio_to_irq_err;
	}

		//注册中断
	err = request_irq(irq,key_irq_handler_t, IRQF_TRIGGER_RISING |IRQF_TRIGGER_FALLING , "test", NULL);
	if(err)
	{
		printk("request_irq error\r\n");
		goto request_irq_err;
	}

	/*********************向上提供操作服务**************************/
	err = misc_register(&led_misc);//注册杂项设备
	if(err)
	{
		printk("misc_register error\r\n");
		goto misc_register_err;
	}
	

	printk("安装成功\r\n");
	return 0;

/****************错误处理部分***********************/
misc_register_err:
	free_irq(irq, NULL);
request_irq_err:
gpio_to_irq_err:
gpio_direction_input_err:
	gpio_free(KEYGPIO);
gpio_request_err:
	return err;
}
/*************************************************/

//执行回调函数,对应执行的功能
static void __exit xxx_exit(void)
{
	//注销杂项设备
	misc_deregister(&led_misc);

	//注销中断
	free_irq(irq, NULL);

	//释放gpio口
	gpio_free(KEYGPIO);


	printk("卸载成功\r\n");
}

//安装回调函数 --- 安装模块会执行该函数
module_init(xxx_init);
//卸载回调函数 --- 卸载模块会执行该函数
module_exit(xxx_exit);


//声明描述
MODULE_LICENSE("GPL"); 
//可选项 -- 安装前可通过指令查看信息
MODULE_AUTHOR("作者");
MODULE_DESCRIPTION("功能描述");
MODULE_VERSION("版本号");

使用nfs网络连接将生成的.ko文件传输给板子,然后安装模块,结果如下:

相关推荐
F1FJJ2 小时前
Shield CLI 命令全解析:15 个命令覆盖所有远程访问场景
网络·数据库·网络协议·容器·开源软件
齐齐大魔王2 小时前
linux-核心工具
linux·运维·服务器
醇氧2 小时前
Linux 系统的启动过程
linux·运维·服务器
IMPYLH2 小时前
Linux 的 dircolors 命令
linux·运维·服务器·数据库
齐齐大魔王2 小时前
linux-基础操作
linux·运维·服务器
是翔仔呐2 小时前
第13章 SPI通信协议全解:底层时序、4种工作模式与W25Qxx Flash芯片读写实战
c语言·开发语言·stm32·单片机·嵌入式硬件·学习·gitee
攻城狮在此2 小时前
华为汇聚交换机DHCP中继配置
网络·华为
婷婷_1722 小时前
【PCIe验证每日学习·阶段复盘01】Day1~Day7 纯理论深度复盘
网络·程序人生·芯片·每日学习·pcie 验证·ic 验证·pcie学习
bwz999@88.com2 小时前
ubuntu24.04更换国内源
linux·运维·服务器