首先,需要明确中断的定义,中断是硬件或者软件主动向CPU发送的一种“紧急信号”,目的是让CPU暂停当前正在执行的代码流,优先处理这个紧急事件,这个紧急事件期间不能被阻塞,处理完成后再回到之前暂停的位置继续执行。没有中断,CPU只能傻乎乎的逐条执行代码,无法及时响应键盘、鼠标、网卡等外部软硬设备的请求。
硬件中断实例:GPIO电平变化而引起的中断:
1、注册中断:告诉内核,这个中断号由你负责
2、触发:电平满足条件时候,CPU自动跳到中断号对应的中断函数中去。
在linux驱动中,用request_irq注册中断,其实就是一件事:把硬件中断号和中断处理函数绑定起来,并告诉内核:以后这个中断号一响,就执行我这个函数。
中断注册函数如下:intrequest_irq(unsigned int irq, // 中断号irq_handler_t handler, // 你的中断处理函数unsigned long flags, // 标志:边沿/电平触发、是否共享等const char *name, // 名字,/proc/interrupts 可见void *dev // 设备标识,用于共享中断时区分);
注册成功后,内核内部会有一张“中断号-->处理函数”的表。当这个中断号被触发时,内核就查这张表,然后在中断上下文中调用我注册的自定义函数。注册本身不连接硬件引脚,只是让内核知道,这个中断号你来处理,用这个函数处理。
以GPIO中断为例,常见写法:
dts中:my_button{compatible = "ny ,button",gpios = <&gpio 15 GPIO_ACTIVE_LOW>;interrupts = <15 IRQ_TYPE_EDGE_FALLING>; //下降沿触发};2、在驱动中注册中断处理函数static int irqreturn_t button_irq_handler(int irq, void *dev_id){printk("GPIO 中断触发!\n");return IRQ_HANDLED;}static int my_probe(struct platform_device *pdev){int irq;int ret;irq = platform_get_irq(pdev,0);//注册中断ret = request_irq(irq, button_irq_handler, IRQF_TRIGGER_FALLING, "my_button", NULL);return ret;}
在Linux中,还有很多种的“触发中断”的途径,本质上都是:软件和硬件产生一个中断请求->CPU跳到对应的中断处理函数。可以按“来源”分成几大类。
硬件中断:
1、定时器中断(timer)
来源:Soc内部的定时器/计数器
触发条件:定时到期,计数达到设定值。
2、网络设备中断
来源:网卡
触发条件:收到数据包(RX中断),发送完成(TX完成中断),错误链路发生变化
用途:驱动收到中断后,把数据包从DMA环形缓冲区中取出来,交给协议栈。
3、存储设备中断(EMMC/SD/SSD/NVMe)
来源:EMMC/SD控制器、SATA/NVMe控制器。
触发条件:数据读/写完成,DMA传输完成、错误或者超时等。通知驱动,刚刚数据读写完成了,可以向上层返回了。
4、USB中断
来源:USB主控制器(如xHCI、EHCI)等
触发条件:有设备插入或者拔出,有新的USB数据包到达,传输完成或者出错等都会触发中断。
5、DMA控制器中断
来源:DMA控制器。DMA(Direct Memory Access,直接内存访问)控制器是一种专门用来在“外设”和“内存”之间搬运数据的硬件模块,它可以不经过 CPU,直接在设备和内存之间传输数据。
触发条件:一块数据从外设搬到内存完成
用途:大块数据搬运完后通知驱动,避免CPU轮询。
6、显示控制器(GPU/DISPLAY)中断
来源:LCD控制器、GPU。
触发条件:帧传输完成(VYSNC),显示错误异常,用于垂直同步,local dimming等显示开发功能。
7、串口UART中断
来源:UART控制器
触发条件:传输完成,收到第一个字符RX、发送完成TX、线路错误。
实现read()/write的异同步通知。
8、I2C/SPI中断
来源:I2C/SPI控制器
触发条件:传输完成、收到ACK/NACK、总线错误等
用途:驱动通过这些中断知道刚才的I2C/SPI读写结束了。
9、电源管理相关中断
来源:PMIC、RTC
触发条件:电池电量低、开关机按键、RTC闹钟到点
用途:系统休眠、环形、闹钟唤醒等
10、错误/异常类中断
来源:CPU或者总线
触发条件:内存访问错误、外部设备错误、看门狗超时等
用途:内核异常处理、复位系统等。
代码示例:硬件定时器中断的Demo(硬件触发)
场景:模拟一个按键接到某个 GPIO,按下时产生中断,执行中断处理函数。
#include<linux/module.h>#include<linux/kernel.h>#include<linux/init.h>#include<linux/gpio.h>#include<linux/interrupt.h>#include<linux/of.h>#include<linux/platform_device.h>static int irq_number;static int my_gpio_pin; // 不用 #define 写死,而是从设备树读取/* ------------------- 中断处理函数 ------------------- */staticirqreturn_tbutton_irq_handler(int irq, void *dev_id){pr_info("GPIO 中断触发!IRQ: %d\n", irq);return IRQ_HANDLED;}/* ------------------- Probe:从设备树获取 GPIO 并注册中断 ------------------- */staticintmy_button_probe(struct platform_device *pdev){int ret;/* 1. 从设备树中获取 GPIO 号 */my_gpio_pin = of_get_named_gpio(pdev->dev.of_node, "gpios", 0);if (my_gpio_pin < 0) {dev_err(&pdev->dev, "从设备树获取 GPIO 失败: %d\n", my_gpio_pin);return my_gpio_pin;}dev_info(&pdev->dev, "从设备树获取到 GPIO: %d\n", my_gpio_pin);/* 2. 申请 GPIO 作为输入 */ret = devm_gpio_request_one(&pdev->dev, my_gpio_pin,GPIOF_IN, "my_button");if (ret) {dev_err(&pdev->dev, "申请 GPIO %d 失败: %d\n", my_gpio_pin, ret);return ret;}/* 3. GPIO 转中断号 */irq_number = gpio_to_irq(my_gpio_pin);if (irq_number < 0) {dev_err(&pdev->dev, "GPIO 转中断号失败: %d\n", irq_number);return irq_number;}dev_info(&pdev->dev, "GPIO %d -> IRQ %d\n", my_gpio_pin, irq_number);/* 4. 注册中断 */ret = devm_request_irq(&pdev->dev,irq_number,button_irq_handler,IRQF_TRIGGER_FALLING,"my_button_irq",pdev);if (ret) {dev_err(&pdev->dev, "注册中断失败: %d\n", ret);return ret;}return 0;}staticintmy_button_remove(struct platform_device *pdev){return 0;}static const struct of_device_id my_button_of_match[] = {{ .compatible = "my,button" },{ /* Sentinel */ }};MODULE_DEVICE_TABLE(of, my_button_of_match);static struct platform_driver my_button_driver = {.probe = my_button_probe,.remove = my_button_remove,.driver = {.name = "my_button",.of_match_table = my_button_of_match,},};module_platform_driver(my_button_driver);MODULE_LICENSE("GPL");
确定 GPIO 引脚:`MY_GPIO_PIN = 15`
申请 GPIO 作为输入:`devm_gpio_request_one(..., GPIOF_IN, ...)`
GPIO 转中断号:`irq_number = gpio_to_irq(MY_GPIO_PIN)`
注册中断:`devm_request_irq(..., button_irq_handler, IRQF_TRIGGER_FALLING, ...)`
实现中断处理函数:`button_irq_handler()`里做实际工作
设备树里声明:
my_button {compatible = "my,button";gpios = <&gpio0 15 GPIO_ACTIVE_HIGH>;};
触发过程: 按键按下 → GPIO 从高变低 → 触发下降沿中断 → 内核调用 `button_irq_handler()`。
开发疑问:
Q:my_button_probe他这样调用,of_get_named_gpio这个函数就知道是compatible对应的dts中的数据了吗?
A:内核在启动时会:
compatible属性;of_match_table中有没有匹配的 compatible;Q:of_get_named_gpio()是怎么“知道”去哪个节点拿数据的?
int of_get_named_gpio(const struct device_node *np, const char *propname, int index);第一个参数 np就是设备树节点指针。
在 probe 函数里:
static int my_button_probe(struct platform_device *pdev){ // pdev->dev.of_node 指向的就是匹配成功的那个设备树节点 my_gpio_pin = of_get_named_gpio(pdev->dev.of_node, "gpios", 0);}关键点:
pdev->dev.of_node是由内核在匹配成功后自动填充的;compatible = "my,button"的那个节点;of_get_named_gpio(pdev->dev.of_node, "gpios", 0)就会去这个节点里找 gpios属性,并返回 GPIO 号。这个例子用内核定时器,每隔一段时间触发一次“硬件定时器中断”,在中断处理里打印信息。
#include <linux/module.h>#include <linux/kernel.h>#include <linux/init.h>#include <linux/timer.h>static struct timer_list my_timer;/* ------------------- 1. 定时器回调函数(中断上下文) ------------------- */static void timer_irq_handler(struct timer_list *t){ pr_info("定时器中断触发!jiffies = %lu\n", jiffies); // 重新设置定时器,1 秒后再次触发 mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000));}/* ------------------- 2. 模块初始化时注册定时器 ------------------- */static int __init timer_demo_init(void){ pr_info("定时器模块加载\n"); // 初始化定时器 timer_setup(&my_timer, timer_irq_handler, 0); // 启动定时器,1 秒后首次触发 mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000)); return 0;}/* ------------------- 3. 模块卸载时注销定时器 ------------------- */static void __exit timer_demo_exit(void){ del_timer(&my_timer); pr_info("定时器模块卸载\n");}module_init(timer_demo_init);module_exit(timer_demo_exit);MODULE_LICENSE("GPL");中断相关关键点是:
timer_irq_handler(),但真正触发它的是硬件定时器到期产生的中断。mod_timer()激活/重置定时器,用 del_timer()删除。步骤 | GPIO 按键示例 | 定时器示例 |
1. 确定源 | 按键接 GPIO 15 | 硬件定时器(系统时钟) |
2. 得到中断号 |
→ | 内核定时器内部使用硬件中断号 |
3. 注册中断 |
|
+ |
4. 写处理函数 |
|
|
5. 触发条件 | GPIO 下降沿 | 定时器到期 |
6. 触发后执行 | 内核自动调用你注册的 handler | 内核自动调用你的 timer callback |
1、软中断:
来源:内核代码主动触发的中断(如网络栈、块设备层)
特点:运行在软中断上下文,不能睡眠,通常用于延时处理,减轻硬中断负担。
2、任务队列/Tasklet
来源:内核代码调试:tasklet_schedule()等。
特点:基于软中断实现;相对更容易使用,不容易睡眠
3、信号(Signal)
来源:用户态的kill()、pthread_kill。
特点:用户态线程之间的软件中断,会让进程在用户态进入信号处理函数。在内核中也会转化为一个中断/异常来处理。
类别 | 例子 | 触发源 | 常见用途 |
GPIO | 按键、外部传感器 | 外部电平变化 | 按键检测、外部信号输入 |
定时器 | 系统时钟、高精度定时器 | 内部计时器 | 调度、超时、周期性任务 |
网络 | 网卡 RX/TX | 外部数据包 | 数据接收、发送完成 |
存储 | eMMC、NVMe、SATA | 外部存储设备 | 读写完成、DMA 完成 |
USB | 设备插拔、数据传输 | USB 总线 | 热插拔、数据传输完成 |
DMA | 内存 ↔外设搬运完成 | DMA 控制器 | 大块数据搬运完成通知 |
显示 | LCD 控制器、GPU | 显示控制器 | 帧同步、双缓冲切换 |
串口 | UART RX/TX | 串口线 | 字符收发完成 |
I2C/SPI | 传感器、EEPROM | I2C/SPI 总线 | 外设通信完成 |
电源/RTC | 电池低电、RTC 闹钟 | PMIC/RTC | 休眠唤醒、闹钟 |
错误类 | 缺页、总线错误、Watchdog | CPU/总线 | 异常处理、系统复位 |
软件中断 | SoftIRQ、Tasklet、Signal | 内核/用户态 | 延迟处理、线程间通知 |
1、请解释 Linux 中“中断上下文”和“进程上下文”的区别,为什么中断上下文不能睡眠?考察:对中断上下文的理解,睡眠的后果。
2、中断的上半部和下半部有什么区别?各自适合做什么工作?考察:Top half / Bottom half 概念,实际应用场景。
3、自旋锁和互斥锁的区别?在中断上下文应该使用哪种锁?为什么?考察:锁的选择,中断上下文限制。
4、如果一个驱动需要在中断处理函数中访问一个全局变量,应该如何保护?考察:共享数据保护,是否考虑多核、中断嵌套等。
5、你用过哪些下半部机制(softirq、tasklet、workqueue)?它们的执行上下文分别是什么?考察:对下半部实现的掌握。
6、如何测量中断处理函数的执行时间?如果发现执行时间过长,你会怎么优化?考察:性能分析与优化思路。
7、中断共享(shared interrupt)需要注意什么?如何在驱动中实现?考察:IRQF_SHARED的使用,中断号冲突排查。
8、在高负载系统中,中断风暴(interrupt storm)是什么?如何排查和解决?考察:实际问题分析与解决能力