在 Linux 设备驱动开发中,中断处理是一个必须掌握的核心知识点。相比于轮询方式持续查询硬件状态,中断机制让驱动程序能够"按需响应",即只有当硬件真正有事件需要处理时,CPU 才会被通知并执行相应的处理代码,从而大幅提升系统的整体响应效率。
本文将聚焦中断驱动开发中最核心、最必备的知识点,帮助你快速建立起完整的中断驱动知识框架。

1.中断基础知识
1.什么是中断?
中断,简单来说就是硬件设备向 CPU 发出的"我有事情要处理"的信号。当外部设备(如按键、网卡、定时器等)有数据需要 CPU 处理时,会通过中断控制器向 CPU 发送一个中断请求。CPU 接收到信号后,会暂停当前正在执行的任务,转而执行与该中断对应的中断处理函数,处理完毕后再恢复之前的任务。
2.Linux驱动里中断基本流程:
在驱动里处理中断,一般分成这几步:
- 获取中断号 irq
- 注册中断处理函数 request_irq()
- 硬件触发中断
- 内核调用你的中断处理函数
- 驱动在退出时释放中断 free_irq()
这就是最基本的中断开发流程。
3.中断的上下半部(中断上下文)
Linux 内核对中断处理有一条"铁律":中断处理函数执行得越快越好。
为什么?因为中断会打断正在运行的进程,如果中断处理函数执行时间过长,不仅会阻塞其他中断的响应,还会导致系统整体的实时性下降。因此,内核将中断处理分为两部分:
-
上半部(Top Half):中断处理函数本身,运行在中断上下文中。它负责处理最紧急、最核心的硬件操作,如清除中断标志、读取硬件寄存器、将数据从硬件拷贝到内存缓冲区等。上半部运行期间,当前 CPU 线上的中断是被屏蔽的,因此必须迅速完成。
-
下半部(Bottom Half):将那些不那么紧急、耗时较长的处理逻辑推迟执行。下半部可以被新的中断打断,从而不会影响系统的中断响应能力。
一个典型的场景是网卡驱动:当网卡收到数据包后,中断上半部只做最基础的数据搬运和状态清理,然后将数据的协议栈处理交给下半部来完成。
上下半部的使用原则:
-
任务对时间非常敏感 → 放在上半部
-
任务与硬件直接相关 → 放在上半部
-
任务不能被其他中断打断 → 放在上半部
-
其他所有任务 → 放在下半部
2.中断硬件层
我们可以将其拆分为三个核心组件:中断源 、中断控制器(GIC) 、以及 CPU 接口。
1. 中断源 (Interrupt Source)
硬件设备通过特定的引脚(Pin)或总线协议发送信号。
- 物理中断引脚: 外设(如按键、传感器、电容屏)通过 GPIO 引脚直接连接到 SoC。信号可以是:
- 电平触发 (Level Triggered):持续的高/低电平。
- 边沿触发 (Edge Triggered):电平跳变的瞬间(上升沿或下降沿)。
- MSI/MSI-X (Message Signaled Interrupts):在 PCIe 或高版本 USB 设备中常用。它不通过物理引脚,而是通过向特定的内存地址写入一个"消息数据"来触发中断。
- **内部中断:**来自 SoC 内部的模块,如 NPU 计算完成信号、定时器(Timer)溢出或 DMA 传输结束。
2. 中断控制器 (GIC - Generic Interrupt Controller)
这是硬件层的"中枢调度员"。ARM 架构标准中使用的是 GIC(RK3588 使用的是 GICv3)。
GIC 的内部主要分为两部分:
- 分发器 (Distributor)
- 集中管理:收集来自系统所有外设的中断信号。
- 优先级裁决:如果同时来了两个中断,分发器根据寄存器里的优先级配置决定先处理谁。
- 路由决策:决定将中断发给哪一个 CPU 核心(Core 0~7)。
- 再分发器 (Redistributor) & ITS
- 在 GICv3 中,再分发器负责将中断准确传达到目标 CPU。
- ITS (Interrupt Translation Service):专门用于处理前文提到的 MSI/MSI-X 消息中断,将其翻译成具体的硬件中断号。
3. 中断类型划分 (Hardware View)
硬件层对中断有严格的分类,每类处理方式不同:
| 类型 | 全称 | 说明 |
|---|---|---|
| SPI | Shared Peripheral Interrupt | 共享外设中断。外部硬件(如 UART, I2C)发出的中断,可以路由到任何核心。 |
| PPI | Private Peripheral Interrupt | 私有外设中断。每个 CPU 核心独有的,如每个核自己的 Generic Timer 中断。 |
| SGI | Software Generated Interrupt | 软件触发中断 。用于多核通信 (IPI),一个核心可以通过写寄存器让另一个核心"被中断"。 |
| LPI | Locality-specific Peripheral Interrupt | 特定局部外设中断。主要配合 MSI 使用,支持海量的中断数量(用于高速 PCIe 设备)。 |
4. 硬件交互流程 (Hardware Handshake)
-
Assert (触发):外设拉高电平。
-
Pending (悬起):GIC 收到信号并记录在寄存器中,等待 CPU 空闲。
-
Active (激活):GIC 向 CPU 发送中断信号(nIRQ/nFIQ 引脚),CPU 停止当前指令。
-
Acknowledge (应答) :CPU 读取 GIC 的
IAR寄存器,告诉 GIC:"我开始处理这个中断号了"。 -
EOI (End of Interrupt) :处理完成后,CPU 向 GIC 写入
EOI寄存器,硬件状态位清零,允许下一次中断进入。
3.驱动中断基本操作
3.1中断注册函数
驱动中注册中断使用 request_irq 函数,其原型为:
cpp
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev_id);
参数含义:
**irq :**中断号。
**handler :**中断处理函数指针,当中断发生时被调用
**flags :**中断标志,用于设置中断的触发方式和共享属性
常见标志:
- IRQF_TRIGGER_RISING:上升沿触发
- IRQF_TRIGGER_FALLING:下降沿触发
- IRQF_TRIGGER_HIGH:高电平触发
- IRQF_TRIGGER_LOW:低电平触发
- IRQF_SHARED:共享中断
**name :**中断名称,会显示在 /proc/interrupts 里。
**dev :**设备私有数据,通常传设备结构体指针。
**返回值:**0 表示注册成功,负值表示失败
所以中断号怎样获得是一个常见的问题,中断号 irq 一般有几种来源:
1)平台固定给定
某些硬件文档里会明确写死某个中断号。
2)设备树获取
在 ARM/Linux 驱动开发里,更常见的是从设备树获取:
cpp
irq = platform_get_irq(pdev, 0);
或者:
cpp
irq = irq_of_parse_and_map(np, 0);
3)GPIO 转中断
如果设备通过 GPIO 产生中断,比如按键中断,常常这样获取:
cpp
gpio = of_get_named_gpio(node, "irq-gpio", 0);
irq = gpio_to_irq(gpio);
我们只要记住:
驱动并不是随便写一个中断号,而是要从硬件描述里获取。
3.2中断处理函数
中断处理函数的原型为:
cpp
irqreturn_t irq_handler(int irq, void *dev_id);
**irq:**发生中断的中断号
**dev_id:**注册时传入的设备标识符
中断处理函数应当尽快返回,返回值一般为 IRQ_HANDLED(表示中断已被正确处理)或 IRQ_NONE(表示中断不属于当前设备)
注:中断处理程序(上半部)中绝对不能执行休眠操作((如调用 msleep、mutex_lock、wait_event 等)。
3.3free_irq 释放中断
驱动卸载时需要释放已申请的中断资源:
cpp
void free_irq(unsigned int irq, void *dev_id);
对于非共享中断,free_irq 会删除中断处理函数并禁止该中断;对于共享中断,只有当释放最后一个设备时才会真正禁用该中断线
3.4最基础中断实验示例
cpp
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
int irq;
irqreturn_t interrupt_func(int irq,void* argvs)
{
printk("this is interrupt_func......\n");
return IRQ_RETVAL(IRQ_HANDLED);
}
static int interrupt_test_init(void)
{
//1.获取中断号 2.注册中断处理函数
int ret;
irq = gpio_to_irq(16);
ret = request_irq(irq,interrupt_func,IRQF_TRIGGER_RISING,"test",NULL);
if(ret < 0)
{
printk("request_irq failed");
return -1;
}
return 0;
}
static void interrupt_test_exit(void)
{
free_irq(irq,NULL);
}
module_init(interrupt_test_init);
module_exit(interrupt_test_exit);
MODULE_LICENSE("GPL");
4.中断下半部实现机制
首先我们要明白,为什么需要分上下文,前面也差不多依旧提到了,我们中断处理函数里要尽量做到:
- 快
- 短
- 不阻塞
所以中断处理函数(上半部函数)适合做的事情:
- 读状态寄存器
- 清中断标志
- 记录事件
- 唤醒等待队列
- 调度下半部处理
故而我们要把耗时的事情放到中断下半部,在现代 Linux 内核中,实现中断下文的方法主要有以下四种:
1. 软中断 (Softirq)
这是内核中最底层的下文处理机制,性能最高,但限制也最多。
- **特点:**运行在中断上下文,不可休眠;允许多个 CPU 同时运行同一个软中断的副本(必须是重入安全的)。
- **使用场景:**只有性能要求极高的关键任务(如网络收发 NET_RX、内核调度 HI_SOFTIRQ)才会使用。
- 开发建议: 普通驱动开发者基本不需要 也不建议直接添加新的软中断。
2. Tasklet (微任务) (逐渐废弃)
Tasklet 是基于软中断实现的,是以前字符设备驱动中最常用的方法。
- 特点: 运行在中断上下文,不可休眠;同一个 Tasklet 不会在多个 CPU 上并行,开发者不需要担心重入问题,编写简单。
- 使用场景: 快速的、不需要休眠的小型任务。
- **现状:**虽然还在用,但内核社区目前倾向于用工作队列或 threaded IRQs 替代它。
3. 工作队列 (Workqueue) ------ 最常用
这是目前驱动开发中最推荐的通用方法。
- 特点: 运行在进程上下文。这意味着它可以休眠(例如调用 msleep()、等待信号量或操作复杂的 I/O)。
- 使用场景: 处理时间较长、需要访问磁盘/文件、或者需要获取可能导致阻塞的互斥锁的任务。
4. 线程化中断 (Threaded IRQs) ------ 现代首选
这是较新且非常优雅的机制,通过 request_threaded_irq() 注册。
- 原理: 内核会为该中断专门创建一个内核线程。
- 优点: 结构清晰:你提供两个函数,一个是 hardirq_handler(上半部),一个是 thread_fn(下半部)。
- 优先级可控: 因为下半部变成了线程,你可以像设置普通进程一样设置它的优先级(这对实时系统非常重要)。
线程化中断函数接口api:
注册中断不再使用 request_irq,而是使用 request_threaded_irq,只比request_irq多了一个参数:
thread_fn(下半部线程):
-
在进程上下文运行。
-
可以休眠(可以使用 Mutex,可以调用耗时函数)。
-
由内核调度器管理。
cpp
int request_threaded_irq(unsigned int irq,
irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long irqflags,
const char *devname,
void *dev_id);
下面我们展示线程化中断的实现代码:
cpp
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
int irq;
irqreturn_t bottom_func(int irq,void* argvs)
{
printk("this is bottom_func\n");
IRQ_RETVAL(IRQ_HANDLED);
}
irqreturn_t interrupt_func(int irq,void* argvs)
{
printk("this is interrupt_func......\n");
//return IRQ_RETVAL(IRQ_HANDLED);
//唤醒下半部中断函数 down_func;
return IRQ_WAKE_THREAD;
}
static int interrupt_test_init(void)
{
//1.获取中断号 2.注册中断处理函数
int ret;
irq = gpio_to_irq(16);
//ret = request_irq(irq,interrupt_func,IRQF_TRIGGER_RISING,"test",NULL);
ret = request_threaded_irq(irq,interrupt_func,bottom_func,IRQF_TRIGGER_RISING,"thread_test",NULL);
if(ret < 0)
{
printk("request_irq failed");
return -1;
}
return 0;
}
static void interrupt_test_exit(void)
{
free_irq(irq,NULL);
}
module_init(interrupt_test_init);
module_exit(interrupt_test_exit);
MODULE_LICENSE("GPL");