前言
在 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 休眠入队过程
- 先定义并初始化一个等待队列头,它就是等待队列的本体;
- 在需要等待事件的代码位置,调用
wait_event系列函数,判断等待条件condition:- 若
condition为真:不进入休眠,直接往下执行; - 若
condition为假:将当前进程加入等待队列,设置为对应睡眠状态,放弃 CPU 调度;
- 若
- 进程会一直保持休眠,直到被唤醒且
condition为真,才会完全退出等待状态。
1.3.2 唤醒出队过程
- 当事件发生时(通常在中断处理函数、定时器回调中),调用
wake_up系列函数; - 函数将等待队列中的目标进程置为
TASK_RUNNING可运行状态,加入内核调度队列; - 进程被调度器调度运行后,会再次检查
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:进程被外部信号打断唤醒
- 0:条件
- 示例:
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_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程 - 参数:
x为等待队列头的指针 - 注意:唤醒操作通常放在事件发生的位置,最常见的是中断服务函数中;必须先修改
condition为真,再调用唤醒函数,否则进程被唤醒后会再次进入休眠 - 示例:
cpp
// 按键中断服务函数中,事件发生后唤醒等待队列
key_event_flag = 1; // 先将等待条件置为真
wake_up(&key_wq); // 唤醒等待队列中的进程
1.5 等待队列典型使用场景
等待队列最经典的应用,就是按键驱动的阻塞 IO 实现:
- 应用层调用
read函数读取按键值,进入驱动的file_operations->read接口; - 若没有按键按下,调用
wait_event_interruptible让进程进入休眠,阻塞read调用; - 按键按下触发中断,在中断服务函数中设置事件标志,调用
wake_up唤醒进程; - 进程被唤醒后,读取按键值,通过
copy_to_user返回给应用层,完成read调用。
【等讲完工作队列将结合一起作示例使用】
二、工作队列(Work Queue)
2.1 为什么需要工作队列?------ 中断顶半部 - 底半部机制
在 Linux 中断编程中,有两个不可突破的核心约束:
- 中断服务程序不属于任何进程上下文,绝对不能执行休眠、调度相关的函数;
- 中断处理必须尽可能快地完成,否则会屏蔽同类型中断,影响系统实时性,甚至导致中断丢失。
但实际开发中,很多中断触发后需要执行耗时操作(比如按键消抖、大量数据处理、复杂逻辑运算),如果都放在中断服务函数中,会严重违反上述约束。
为此,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 工作队列典型使用场景
工作队列最经典的应用,就是按键中断的顶半部 - 底半部实现:
- 按键按下触发 GPIO 中断,进入中断服务函数(顶半部);
- 顶半部仅执行调度工作队列的操作,立即退出中断,整个中断处理耗时极短;
- 内核线程调度执行工作函数(底半部),完成按键消抖、电平读取、事件上报等耗时操作。
三、等待队列与工作队列的核心区别与组合使用
3.1 核心区别对比
| 特性 | 等待队列 | 工作队列 |
|---|---|---|
| 核心用途 | 实现进程的条件等待、阻塞 IO | 实现中断底半部,推迟耗时操作执行 |
| 运行上下文 | 调用 wait_event 的用户进程上下文 | 内核工作者线程的进程上下文 |
| 核心功能 | 让进程休眠,等待事件发生后唤醒 | 把任务推迟到内核线程中异步执行 |
| 与中断的关系 | 通常在中断中调用 wake_up 唤醒队列 | 通常在中断中调度工作,中断退出后执行 |
| 休眠能力 | 休眠是核心功能 | 工作函数中可以正常执行休眠操作 |
3.2 驱动开发中的组合使用
在实际的驱动开发中,等待队列和工作队列经常组合使用,最典型的就是完整的按键 input 子系统驱动:
- 驱动初始化:申请 GPIO、设置输入模式、申请中断、初始化工作队列、初始化等待队列、注册 input 设备;
- 按键按下触发中断,进入顶半部:调度工作队列,立即退出中断;
- 工作队列底半部执行:按键消抖、读取按键电平、上报 input 事件、设置事件标志、唤醒等待队列;
- 应用层 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文件传输给板子,然后安装模块,结果如下:
