当前位置:首页>Linux>小白也能看懂的 Linux 内核中断机制解析

小白也能看懂的 Linux 内核中断机制解析

  • 2026-10-11 06:49:55
小白也能看懂的 Linux 内核中断机制解析

小白也能看懂的 Linux 内核中断机制解析

学 Linux 驱动时,中断机制是一个绕不开的坎。很多初学者会卡在几个问题上:外设为什么能“打断”CPU?中断号从哪里来?中断处理函数什么时候执行?为什么中断里不能随便睡眠?上半部、下半部、线程化中断又是什么?

这篇文章不从复杂源码开始,而是先建立一条清晰主线:**外设发出中断信号,中断控制器汇总并送给 CPU,CPU 进入内核中断入口,内核找到对应驱动的处理函数,驱动快速处理关键现场,再把耗时工作延后。**

理解这条链路,后面再看 request_irq()、/proc/interrupts、设备树里的 interrupts 属性,思路就会顺很多。

一、中断解决的核心问题:CPU 不用一直傻等

先从一个最朴素的问题开始:假设按键被按下,CPU 怎么知道?

最简单的方法是轮询。CPU 每隔一段时间去读一次 GPIO 状态,看看按键有没有变化。这个方法容易理解,但有明显缺点:

  • 读得太慢,响应会有延迟。
  • 读得太快,CPU 时间被浪费。
  • 外设很多时,轮询逻辑会越来越臃肿。

中断的思路相反:CPU 平时正常执行任务,外设有事件时主动发信号通知 CPU。CPU 暂停当前执行流,跳到内核中断处理路径,处理完再回到原来的位置继续运行。

这就像你不用一直盯着门口看快递到了没有,而是让门铃响了再去开门。

在 Linux 系统里,一次典型中断路径可以拆成几步:

  • 外设产生事件,例如按键按下、网卡收到包、串口收到数据。
  • 外设通过 IRQ 线或内部中断信号通知中断控制器。
  • 中断控制器判断优先级、屏蔽状态,再把中断送给 CPU。
  • CPU 进入异常入口,切换到内核中断上下文。
  • 内核根据中断号找到驱动注册的处理函数。
  • 驱动处理关键状态,必要时唤醒线程或调度延后任务。

这里有一个关键点:**中断不是某个驱动自己凭空触发的,而是硬件事件、控制器、CPU 和内核共同完成的一套机制。**

二、硬件入口:IRQ 信号先到中断控制器

在嵌入式 SoC 里,外设通常不会直接把中断送到 Linux 驱动。它们会先连接到中断控制器,例如 ARM 平台常见的 GIC。GPIO 控制器、I2C 控制器、SPI 控制器、网卡、USB 控制器、定时器等外设,都可能有自己的中断输出。

中断控制器负责几件事:

  • 接收多个外设中断信号。
  • 判断哪个中断当前允许上报。
  • 处理中断优先级。
  • 把中断路由到某个 CPU 核。
  • 向 CPU 报告当前需要处理的中断 ID。

驱动里看到的 IRQ number,并不一定等于硬件手册里的原始中断线编号。Linux 会把硬件中断号映射成内核使用的虚拟 IRQ 编号,驱动通常只需要使用内核分配好的 IRQ。

在设备树平台里,中断信息常常长这样:

●●●// 示例:设备通过设备树描述自己的中断资源
example_device: sensor@40 {
    compatible = "demo,irq-sensor";
    reg = <0x40>;
    interrupt-parent = <&gpio1>;
    interrupts = <12 IRQ_TYPE_EDGE_FALLING>;
};

这段描述表达的是:设备的中断接在 gpio1 控制器的第 12 路,触发方式是下降沿。驱动并不直接硬编码这个数字,而是从设备树或平台资源中获取 IRQ。

触发方式也很重要:

  • 边沿触发:信号从低到高或从高到低的瞬间触发。
  • 电平触发:信号保持在某个电平时持续有效。

如果触发方式配置错了,可能出现两类问题:

  • 完全没有中断。
  • 中断一直触发,形成中断风暴。

这也是调试中断问题时必须检查硬件原理图、设备树和芯片手册的原因。

三、内核处理:上半部要短,下半部做重活

中断来了以后,CPU 会进入内核中断上下文。这个上下文很特殊:它不是普通进程上下文,不能像普通线程那样随便睡眠、等待锁、做长时间阻塞操作。

因此 Linux 驱动设计里有一个基本原则:**中断处理函数要尽量短。**

传统说法里,中断处理分为上半部和下半部:

  • 上半部:真正的硬中断处理函数,响应要快。
  • 下半部:把耗时或可延后的工作放到稍后执行。

上半部通常做这些事:

  • 确认中断来源。
  • 读取硬件状态寄存器。
  • 清除或应答中断。
  • 保存必要数据。
  • 唤醒等待队列或调度后续任务。

它不适合做这些事:

  • 长时间循环处理大量数据。
  • 等待 mutex。
  • 访问可能睡眠的接口。
  • 做复杂业务逻辑。
  • 在中断里打印大量日志。

一个简化的中断处理函数大概是这样:

●●●// 示例:表达中断上半部的典型处理逻辑
static irqreturn_t demo_irq_handler(int irq, void *dev_id)
{
struct demo_dev *ddev = dev_id;
    u32 status;

// 读取状态寄存器,确认是否是本设备产生的中断
    status = readl(ddev->base + DEMO_INT_STATUS);
if (!status)
return IRQ_NONE;

// 清除中断,避免同一个中断反复进入
    writel(status, ddev->base + DEMO_INT_CLEAR);

// 保存轻量状态,唤醒后续处理
    ddev->last_status = status;
    wake_up_interruptible(&ddev->waitq);

return IRQ_HANDLED;
}

这段代码的重点不是寄存器名字,而是处理策略:先确认来源,再清中断,再保存必要状态,最后唤醒后续逻辑。复杂工作不要堆在这里。

Linux 里常见的下半部机制包括:

  • softirq:内核内部高性能延后处理机制,网络收包路径常用。
  • tasklet:基于 softirq 的较老机制,新代码里使用要谨慎。
  • workqueue:在进程上下文执行,可以睡眠,适合耗时工作。
  • threaded IRQ:把中断处理线程化,适合很多驱动场景。

对于初学驱动的人,重点先记住一句话:**硬中断里只做必须马上做的事,能晚点做的事放到下半部。**

四、驱动怎么注册中断

驱动想处理某个中断,需要先向内核注册处理函数。常见接口是 request_irq() 或 devm_request_irq()。

devm_request_irq() 更适合现代驱动,因为它和设备生命周期绑定,设备卸载时资源会自动释放,减少忘记 free_irq() 的风险。

简化示例如下:

●●●// 示例:驱动 probe 阶段申请中断
staticint demo_probe(struct platform_device *pdev)
{
struct demo_dev *ddev;
int irq;
int ret;

// 从设备树或平台资源中获取 IRQ 编号
    irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;

    ddev = devm_kzalloc(&pdev->dev, sizeof(*ddev), GFP_KERNEL);
if (!ddev)
return -ENOMEM;

    init_waitqueue_head(&ddev->waitq);

// 注册中断处理函数
    ret = devm_request_irq(&pdev->dev, irq, demo_irq_handler,
                           IRQF_TRIGGER_FALLING,
"demo-irq-device", ddev);
if (ret)
return ret;

    platform_set_drvdata(pdev, ddev);
return0;
}

这里有几个参数值得理解:

  • irq
    :内核分配给这个设备的 IRQ 编号。
  • demo_irq_handler
    :中断触发时执行的处理函数。
  • IRQF_TRIGGER_FALLING
    :触发方式,实际项目要和硬件一致。
  • "demo-irq-device"
    :显示在 /proc/interrupts 里的名字。
  • ddev
    :传给中断处理函数的私有数据。

如果多个设备共享一根中断线,还可能使用 IRQF_SHARED。共享中断时,中断处理函数必须检查硬件状态,确认中断是否真的来自本设备。如果不是,就返回 IRQ_NONE。

五、调试中断,先看三个地方

中断问题不要一上来就钻源码。更高效的顺序是先确认硬件信号、内核计数和驱动日志。

先看 /proc/interrupts:

●●●# 查看系统中断计数和中断名称
cat /proc/interrupts

重点看几件事:

  • 设备对应的中断项是否存在。
  • 触发外设后计数是否增长。
  • 中断是否集中在某个 CPU 上。
  • 名称是否和驱动注册时一致。
  • 是否有异常快速增长的中断计数。

再看内核日志:

●●●# 观察驱动 probe、中断注册、异常日志
dmesg -w

如果驱动注册失败,常见原因包括:

  • 设备树中断属性写错。
  • interrupt-parent
     指向不对。
  • 触发方式和硬件不一致。
  • 中断已经被其他驱动占用。
  • GPIO 没有配置成中断输入。

再结合硬件确认:

  • 用示波器或逻辑分析仪看 IRQ 引脚是否真的跳变。
  • 确认上拉、下拉、电平转换是否正确。
  • 确认外设中断状态寄存器是否置位。
  • 确认驱动是否正确清除中断。

中断风暴是初学者很容易遇到的问题。它通常表现为 CPU 占用升高,/proc/interrupts 计数疯狂增长,系统响应变慢。常见原因是中断状态没有清除、电平触发信号一直有效,或者触发方式配置错。

可以临时用下面的方式观察中断计数变化:

●●●# 每秒刷新一次中断计数
watch -n 1 cat /proc/interrupts

如果某一行计数在没有真实事件时也持续快速增长,就要优先检查清中断逻辑和触发方式。

六、小白应该怎么建立整体模型

学习 Linux 中断机制,不建议一开始就陷入架构相关汇编入口。更有效的方法是先建立四层模型:

  • 硬件层:外设是否真的产生 IRQ 信号。
  • 控制器层:中断控制器是否接收、屏蔽、路由该中断。
  • 内核层:Linux 是否把 IRQ 映射好,并调用对应 handler。
  • 驱动层:驱动是否正确注册、确认来源、清除中断并唤醒后续逻辑。

以后遇到中断不触发,可以按这个顺序排查:

1. 先确认硬件信号有没有变化。 2. 再确认设备树或平台资源是否描述正确。 3. 再看 /proc/interrupts 计数是否增长。 4. 再看中断处理函数是否进入。 5. 最后检查是否清中断、是否唤醒等待队列、是否调度下半部。

这样排查比盲目加日志更稳定。

七、总结

Linux 中断机制看起来复杂,本质上是在解决一个问题:外设发生事件时,如何让 CPU 高效、及时、可控地响应。

它的主线并不难:

  • 外设产生事件。
  • IRQ 信号进入中断控制器。
  • CPU 跳入内核中断入口。
  • 内核调用驱动注册的处理函数。
  • 上半部快速确认和清理现场。
  • 下半部或线程继续处理复杂工作。

对驱动开发来说,真正重要的不是背概念,而是形成工程判断:中断处理要短,触发方式要对,状态要清干净,调试时要看 /proc/interrupts、dmesg 和硬件信号。

掌握这条链路之后,再去读具体平台的 GIC、GPIO 中断、request_irq()、线程化中断源码,理解难度会明显下降。

【往期推荐】

一个 USB 口可以通过 Hub 接多少个设备?

一文讲透 Debian、Buildroot、Yocto 的实战运用和区别

具身智能到底是什么?为什么机器人突然“活”起来了

最新文章

随机文章