主要目标:降低cpu占用率
今天来讲一下Linux驱动开发中的中断和等待队列。
这两个东西分别是干嘛的呢?
首先学过mcu的朋友,对中断一定不会陌生,它的作用就是当中断触发时打断cpu当前执行的任务,来执行中断的操作。当中断任务执行结束后,cpu再继续执行之前的任务。
那等待队列是干啥的呢?
类似于mcu的信号量,等待一个触发的条件,再让cpu来执行。但这俩又不完全一样,它们在“同步”和“阻塞”的目的上是相似的。
MCU 中的信号量/标志位轮询:在裸机或 RTOS 中,你通常设置一个全局变量 key_pressed = 0。主循环不断检查 if (key_pressed == 1)。如果为 0,主循环继续跑其他代码或进入低功耗模式(WFI)。这本质上是一种主动查询或基于标志位的协作式调度。
Linux 等待队列:它是一种内核级的进程休眠机制。当 read 发现没数据时,它不是简单地返回或继续跑循环,而是告诉内核:“把我这个进程挂起(Remove from CPU scheduler),放到这个队列里睡觉,直到有人叫醒我”。
核心区别:
主动性 vs 被动性:
MCU 信号量通常需要你在主循环里主动去查(除非你用 RTOS 的任务挂起 API,那才更像等待队列)。
Linux 等待队列是被动休眠。进程调用 wait_event 后,它就彻底从 CPU 调度器中消失了,完全不占用 CPU 时间片,直到被唤醒。
上下文切换:
MCU 中改变标志位通常不涉及复杂的上下文保存/恢复(除非触发任务切换)。
Linux 中,wake_up 会触发内核调度器,进行完整的进程上下文切换(保存当前寄存器、加载被唤醒进程的寄存器等),开销比 MCU 大,但功能更强(支持多进程、权限管理、信号处理等)。
结论:你可以把等待队列理解为 “RTOS 中带有条件判断的任务挂起(Task Suspend with Condition)”,而不是简单的二进制信号量。它让进程在没有数据时“彻底消失”,从而极大降低 CPU 负载。
知道了这两个东西分别是干啥的,我们能用他们来做什么呢?
降低cpu的占用率。(降低 CPU 占用:依靠等待队列让无事件时的进程彻底休眠,不再消耗 CPU 周期;依靠中断让 CPU 仅在硬件事件发生时才介入)
以按键为例:我们设置一个检测按键上升沿的中断,然后在read()函数里设置好等待队列条件为假,让等待队列挂起,当按键被按下,触发中断,中断服务函数被执行,此时等待队列条件为真,等待队列被释放,释放后继续执行read()函数里操作,把读取到的值传递到应用层。
在上述过程中,cpu只有在中断触发后才分配时间片给read函数,其他时间都不再执行read函数,这就会大幅降低cpu的占用率。
在这个过程中,cpu在read函数中遇到等待队列函数后就不再继续执行read函数了,但是如何让等待队列释放,唤醒read函数继续执行呢?原理是什么?
(也就是说既然 read 函数在休眠(没分到时间片),哪是谁检测到按键并告诉 CPU 执行 read?)
这是初学者最大的误区:并不是“谁告诉 CPU 去执行 read 函数”,而是“硬件中断打断了 CPU 当前的工作,内核在中断处理程序中唤醒了休眠的 read 进程”。
这里有一个关键的硬件机制:GPIO 控制器的中断功能。
详细流程拆解:
休眠状态(Read 进程):
应用层调用 read()。
驱动发现没按键按下,调用 wait_event_interruptible()。
关键点:此时,read 函数的代码执行暂停了。这个进程的状态变为 TASK_INTERRUPTIBLE(可中断休眠)。
CPU 在干嘛? CPU 此时正在运行其他进程(比如桌面界面、网络服务、或者空闲任务 idle)。它完全不知道 read 的存在,也不分配时间片给 read。
硬件事件发生(按键按下):
你按下了按键,GPIO 引脚电平发生变化(例如从低变高)。
谁检测到的? 不是 CPU,也不是驱动程序,而是 SoC 内部的 GPIO 控制器硬件电路。
GPIO 控制器内部有一个比较器或边沿检测电路,它独立于 CPU 核心运行。一旦检测到电平跳变,它会向 CPU 的中断控制器(如 GIC)发送一个电信号(Interrupt Request, IRQ)。
中断响应(CPU 被硬件打断):
CPU 收到中断信号后,无论它当前正在运行什么代码(哪怕是空闲任务),都会立即暂停当前工作。
CPU 硬件自动保存当前上下文,跳转到预先注册好的中断处理函数(ISR, Interrupt Service Routine)。
注意:此时执行的代码是驱动中注册的 irq_handler,而不是 read 函数。
中断处理与唤醒(关键步骤):
在 irq_handler 中,驱动代码执行以下操作:
读取 GPIO 状态,确认是按键按下(可能包含消抖逻辑)。
将键值存入全局缓冲区。
设置条件标志 data_available = 1。
调用 wake_up_interruptible(&my_wait_queue)。
wake_up 做了什么? 它通知内核调度器:“等待在 my_wait_queue 上的那个进程(也就是之前休眠的 read 进程),现在条件满足了,把它状态改为 TASK_RUNNING(就绪态),放入调度队列。”
重要:wake_up 并没有立即执行 read 函数的剩余代码。它只是把 read 进程标记为“可以运行了”。
进程恢复与数据返回:
中断处理函数执行完毕,CPU 返回到刚才被打断的地方(可能是空闲任务或其他进程)继续运行。
在下一次内核调度时机(例如中断返回时、时间片用完时),调度器发现 read 进程现在是“就绪态”且优先级合适。
调度器切换上下文,恢复 read 函数的执行。
wait_event_interruptible 宏检测到 data_available 为真,于是从休眠中返回。
read 函数继续执行后续代码:copy_to_user 将数据发给应用层,然后返回。
总结回答上面的疑问:
是谁检测到按键?
是 GPIO 硬件控制器。它独立于 CPU 软件运行,专门监控引脚电平。
是谁告诉 CPU要去执行中断服务函数了?
是 中断控制器(GIC) 通过硬件电信号通知 CPU 核心。
CPU 去执行 read 函数了吗?
没有直接执行 read。 CPU 先去执行中断服务函数(ISR)。在 ISR 中,内核通过 wake_up 机制唤醒了休眠的 read 进程。随后,由内核调度器决定何时恢复 read 进程的运行。
形象比喻:
Read 进程:像一个在候诊室睡觉的病人(休眠)。
GPIO 硬件:像门口的门铃按钮。
中断:病人按铃(硬件触发),护士(中断处理程序)听到铃声。
Wake_up:护士走到候诊室,把病人的状态牌从“睡觉”翻成“待诊”(唤醒),但护士不会立刻给病人看病,而是回到前台。
调度器:医生(CPU)看完手头的急诊(中断处理完),查看待诊列表,发现这个病人醒了,于是叫他的号(上下文切换),病人开始看病(read 函数继续执行并返回数据)。
在这个过程中,病人在睡觉时完全不占用医生的时间(CPU 资源),只有按铃和处理的那一瞬间需要医生介入。这就是为什么它能降低 CPU 占用率。
代码示例:
(此示例中的按键为双边沿触发)
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/miscdevice.h>
//1、添加头文件
#include <linux/irq.h>
#include <linux/interrupt.h>
//9、添加等待队列头文件
#include <linux/wait.h>
#include <linux/sched.h>
//10、申请等待队列列头
DECLARE_WAIT_QUEUE_HEAD(mywait);
//杂项设备开发框架+linux设备树和平台总线开发框架
struct miscdevice misc;
struct file_operations ops;
int key1,key2;
//3、定义中断号
int irq1;
int irq2;
int value;
int ret;
//12、定义等待队列条件
int wait_flag;
// 5、定义中断服务函数
irqreturn_t myirqfunc(int irq, void *arg)
{
if (irq == irq1)
{
value = 1;
}
else if (irq == irq2)
{
value = 2;
}
//进入中断说明按键完成一次按下
// value = 1; //先赋值再释放
//14、等待队列条件置为真
wait_flag = 1;
//15、等待队列释放
wake_up_interruptible(&mywait);
return 0;
}
//1、定义open文件接口
int myopen(struct inode *inode,struct file *file)
{
gpio_request(key1,"key1");
gpio_direction_input(key1);
gpio_request(key2,"key2");
gpio_direction_input(key2);
// 2、获取中断号
// irq1 = gpio_to_irq(key1);
// 使能中断 --- 对于当前板子不需要
// 4、注册中断服务函数
ret = request_irq(irq1,myirqfunc,IRQ_TYPE_EDGE_BOTH,"irq1",NULL);
ret = request_irq(irq2,myirqfunc,IRQ_TYPE_EDGE_BOTH,"irq2",NULL);
return 0;
}
//3、定义close文件接口
int myclose(struct inode *inode, struct file *file)
{
// 释放中断服务函数
free_irq(irq1,NULL);
free_irq(irq2,NULL);
// 失能中断 --- 对于当前板子不需要
return 0;
}
//定义read文件接口
ssize_t myread(struct file *file, char __user *buf, size_t size, loff_t *offt)
{
//13、等待队列条件清0
wait_flag = 0;
//11、等待队列挂起
wait_event_interruptible(mywait ,wait_flag);//程序会一直停在这个分号后
ret = copy_to_user(buf,&value,size);
value = 0;
return 0;
}
//4、定义probe函数
int myprobe(struct platform_device *pdev)
{
printk("匹配成功!!!\n");
//8、获取节点信息
key1 = of_get_named_gpio(pdev->dev.of_node,"gpios",0);
key2 = of_get_named_gpio(pdev->dev.of_node,"gpios",1);
//8、设备树获取中断号
irq1 = platform_get_irq(pdev,0);
irq2 = platform_get_irq(pdev,1);
ops.owner = THIS_MODULE;
//2、把open加入核心结构体
ops.open = myopen;
//4、把release加入核心结构体
ops.release = myclose;
//把read加入核心结构体
ops.read = myread;
misc.fops = &ops;
misc.minor = 255;
misc.name = "key";
misc_register(&misc);
return 0;
};
//5、定义remove函数
int myremove(struct platform_device *pdev)
{
misc_deregister(&misc);
return 0;
};
//6、定义匹配核心结构体
struct of_device_id myid = {
.compatible = "key-platdata",
};
// 3、定义驱动端核心结构体
struct platform_driver pdrv = {
.probe = myprobe,
.remove = myremove,
.driver.owner = THIS_MODULE,
.driver.name = "1012",
.driver.of_match_table = &myid,
};
static int __init myinit(void)
{
// 2、驱动端注册
platform_driver_register(&pdrv);
return 0;
}
static int __exit myexit(void)
{
// 7、驱动端注销
platform_driver_unregister(&pdrv);
}
module_init(myinit);
module_exit(myexit);
MODULE_LICENSE("GPL");
设备树文件:
xyd_key: xyd-key {
compatible = "key-platdata";
gpios =
<&gpio1 RK_PA7 GPIO_ACTIVE_LOW>,
<&gpio1 RK_PB1 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>;
interrupts = <RK_PA7 IRQ_TYPE_EDGE_BOTH>,
<RK_PB1 IRQ_TYPE_EDGE_BOTH>;
status = "okay";
};