☰
Linux驱动开发中的中断和等待队列
2026/10/14 21:26:59 网站建设 项目流程

主要目标:降低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";

};

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询