当前位置:首页>Linux>Linux菜鸟专题-中断功能说明

Linux菜鸟专题-中断功能说明

  • 2026-10-10 12:42:00
Linux菜鸟专题-中断功能说明
    在笔者学习Linux相关知识找嵌入式开发工作时,对于中断概念都是一知半解,只到工作后,开发实际驱动功能时,才慢慢理解何为中断,本文讲以多方面切入说面,何为中断。

    首先,需要明确中断的定义,中断是硬件或者软件主动向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:内核在启动时会:

  1. 解析设备树,找到所有节点;
  2. 对每个节点,看它的 compatible属性;
  3. 在内核的驱动列表里找 of_match_table中有没有匹配的 compatible;
  4. 如果匹配成功,就认为“这个驱动可以管这个设备”,然后调用这个驱动的 probe 函数,并把该设备节点作为参数传进去。

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");

中断相关关键点是:

  • 内核定时器依赖硬件定时器中断(如 APIC Timer、ARM Generic Timer)。
  • 写的是 timer_irq_handler(),但真正触发它的是硬件定时器到期产生的中断。
  • 用 mod_timer()激活/重置定时器,用 del_timer()删除。

把“注册中断号 + 中断函数 + 触发”串起来

步骤

GPIO 按键示例

定时器示例

1. 确定源

按键接 GPIO 15

硬件定时器(系统时钟)

2. 得到中断号

gpio_to_irq(15)

→ irq_number

内核定时器内部使用硬件中断号

3. 注册中断

devm_request_irq(irq, handler, flags, ...)

timer_setup()

+ mod_timer()

4. 写处理函数

button_irq_handler()

timer_irq_handler()

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

内核/用户态

延迟处理、线程间通知

linux驱动中断常见面试题:

1、请解释 Linux 中“中断上下文”和“进程上下文”的区别,为什么中断上下文不能睡眠?考察:对中断上下文的理解,睡眠的后果。

2、中断的上半部和下半部有什么区别?各自适合做什么工作?考察:Top half / Bottom half 概念,实际应用场景。

3、自旋锁和互斥锁的区别?在中断上下文应该使用哪种锁?为什么?考察:锁的选择,中断上下文限制。

4、如果一个驱动需要在中断处理函数中访问一个全局变量,应该如何保护?考察:共享数据保护,是否考虑多核、中断嵌套等。

5、你用过哪些下半部机制(softirq、tasklet、workqueue)?它们的执行上下文分别是什么?考察:对下半部实现的掌握。

6、如何测量中断处理函数的执行时间?如果发现执行时间过长,你会怎么优化?考察:性能分析与优化思路。

7、中断共享(shared interrupt)需要注意什么?如何在驱动中实现?考察:IRQF_SHARED的使用,中断号冲突排查。

8、在高负载系统中,中断风暴(interrupt storm)是什么?如何排查和解决?考察:实际问题分析与解决能力

最新文章

随机文章