Linux内核与驱动:9.驱动中的中断机制

在 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)

  1. Assert (触发):外设拉高电平。

  2. Pending (悬起):GIC 收到信号并记录在寄存器中,等待 CPU 空闲。

  3. Active (激活):GIC 向 CPU 发送中断信号(nIRQ/nFIQ 引脚),CPU 停止当前指令。

  4. Acknowledge (应答) :CPU 读取 GIC 的 IAR 寄存器,告诉 GIC:"我开始处理这个中断号了"。

  5. 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");
相关推荐
格林威2 小时前
Windows 实时性补丁(RTX / WSL2)
linux·运维·人工智能·windows·数码相机·计算机视觉·工业相机
xuxie992 小时前
N22 key驱动
linux·运维·服务器
c++逐梦人2 小时前
Linux多线程
linux·服务器
开心码农1号2 小时前
RabbitMQ 生产运维命令大全
linux·开发语言·ruby
IMPYLH2 小时前
Linux 的 nl 命令
linux·运维·服务器·bash
咖喱o2 小时前
路由策略
linux·服务器·网络
南境十里·墨染春水2 小时前
linux学习进展 主函数的参数
linux·运维·学习
淮北4942 小时前
obsidian管理自己的计划
linux·学习·kanban·obsidian
YYYing.2 小时前
【Linux/C++网络篇(一) 】网络编程入门:一文搞懂 TCP/UDP 编程模型与 Socket 网络编程
linux·网络·c++·tcp/ip·ubuntu·udp