4. Linux内核中断机制概述:中断向量、中断控制器(GIC)、中断号、中断上下文与进程上下文

中断机制是嵌入式Linux系统响应硬件事件的基石。对于I2C外设驱动开发而言,理解中断从硬件触发到内核软件处理的完整路径,是正确实现中断处理程序(ISR)和保证数据一致性的前提。本节将系统梳理中断向量表、通用中断控制器(GIC)、中断号映射以及中断上下文与进程上下文的本质区别。

4.1 中断向量与中断向量表

中断向量是处理器为每个中断源分配的固定入口地址。在ARM Cortex-A系列处理器中,中断向量表通常位于地址0x00000000或0xFFFF0000(由系统控制寄存器决定)。当CPU检测到中断信号时,硬件自动跳转到对应的向量地址执行预定义的跳转指令。

关键概念: 中断向量不是中断号,而是内存地址。每个向量对应一个异常或中断类型(复位、未定义指令、软中断、预取中止、数据中止、IRQ、FIQ)。
嵌入式要点: 在Linux启动过程中,内核会将中断向量表从ROM复制到RAM,并设置MMU映射,使向量表位于内核虚拟地址空间的高端(如0xFFFF0000)。这是为了支持动态修改异常处理函数。
ARM Cortex-A 中断向量表结构 向量地址 异常类型 处理模式 优先级 0x00000000 复位 SVC模式 1(最高) 0x00000004 未定义指令 UND模式 6 0x00000008 软中断(SWI) SVC模式 6 0x0000000C 预取中止 ABT模式 5 0x00000010 数据中止 ABT模式 2 0x00000018 IRQ (外设中断) IRQ模式 4 I2C外设中断由此进入

4.2 中断控制器:GIC(Generic Interrupt Controller)

在ARM多核处理器系统中,GIC是管理中断路由、优先级、使能和分发的核心硬件单元。GIC负责将来自外设(如I2C控制器、GPIO、定时器)的中断信号,经过优先级仲裁后,分发到指定的CPU核心。

GIC架构分为两个主要逻辑单元:

单元 全称 功能 寄存器接口
GICD Distributor 全局中断使能、优先级配置、中断分组、SPI/PPI路由 GICD_* 寄存器组
GICC CPU Interface 每个CPU核心独立的中断屏蔽、优先级掩码、中断确认(EOI) GICC_* 寄存器组
驱动开发陷阱: 在编写I2C中断驱动时,必须确保GICD中对应的中断使能位已置位,且GICC的优先级掩码寄存器允许该中断通过。否则,即使外设产生了中断信号,CPU也不会收到IRQ。
GIC中断分发流程 I2C控制器 (外设中断源) GPIO控制器 (外设中断源) 定时器 (外设中断源) GICD Distributor • 中断使能/禁能 • 优先级配置 • 中断分组(Group0/1) • SPI路由选择CPU • 中断状态跟踪 (Pending/Active) 已仲裁中断 GICC CPU Interface • 优先级掩码 • 中断抢占逻辑 • IRQ/FIQ信号生成 • 中断确认(IAR) • 结束中断(EOI) IRQ CPU Core 0 执行中断处理程序

4.3 中断号:从硬件中断线到Linux IRQ号

中断号是驱动开发者最常接触的抽象概念。在嵌入式Linux中,中断号经历了从硬件中断线到软件IRQ号的映射过程。

层级 名称 说明 示例(I2C控制器)
硬件层 HW IRQ ID GIC为每个中断源分配的硬件ID,在GICD寄存器中索引 SPI 62(GIC硬件ID)
DT层 中断描述符 设备树中interrupts = <GIC_SPI 62 IRQ_TYPE_LEVEL_HIGH> 指定中断类型和触发方式
内核层 Linux IRQ号 内核通过irq_of_parse_and_map()将DT描述符映射为虚拟IRQ号 返回IRQ号(如62或动态分配值)
驱动层 IRQ号 驱动调用request_irq(irq, handler, flags, name, dev)注册 dev->irq 存储的IRQ号
核心API: 在I2C控制器驱动中,通常通过以下方式获取中断号:
struct resource *res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
或 int irq = irq_of_parse_and_map(pdev->dev.of_node, 0);

4.4 中断上下文与进程上下文

这是嵌入式驱动开发中最容易出错的概念之一。中断上下文和进程上下文在可调用函数、可访问内存、调度行为等方面存在根本差异。

中断上下文 vs 进程上下文 中断上下文 (Atomic Context) ❌ 不可睡眠 (不能调用 schedule()) ❌ 不可获取信号量 (mutex) ✅ 可使用自旋锁 (spinlock) ✅ 使用 in_interrupt() 检测 进程上下文 (Process Context) ✅ 可以睡眠 (schedule_timeout()) ✅ 可以使用信号量 (mutex) ✅ 可访问用户空间内存 ✅ 可执行系统调用 不可 混用

4.5 中断处理程序的分层设计:Top Half与Bottom Half

由于中断上下文不允许睡眠,且必须尽快完成(通常要求在微秒级),Linux内核将中断处理分为两个阶段:

  • Top Half(上半部): 在中断上下文中立即执行,完成最紧急的硬件操作(如读取I2C状态寄存器、清除中断标志、拷贝FIFO数据到内存缓冲区)。必须快速返回。
  • Bottom Half(下半部): 在稍后的安全时间执行,运行在软中断或进程上下文。完成耗时的数据处理(如解析I2C数据帧、唤醒等待队列、通知用户空间)。
I2C驱动实践: 在I2C控制器中断处理中,Top Half通常只做:
1. 读取中断状态寄存器,判断中断类型(TX_EMPTY / RX_FULL / ERROR)。
2. 如果FIFO深度小,直接搬运数据到内存缓冲区。
3. 返回IRQ_HANDLED或IRQ_WAKE_THREAD(如果使用线程化中断)。

耗时的I2C消息解析、DMA描述符管理、错误恢复逻辑应放在Bottom Half或中断线程中。

4.6 中断上下文检测与调试

在驱动开发中,判断当前是否处于中断上下文至关重要。内核提供了以下检测机制:

/* 检测当前是否在中断上下文中 */
#include <linux/preempt.h>
#include <linux/hardirq.h>

if (in_interrupt()) {
    /* 当前在中断上下文或软中断上下文 */
    pr_debug("Cannot sleep, using spinlock instead\n");
    spin_lock(&dev->lock);
} else {
    /* 当前在进程上下文 */
    pr_debug("Can sleep, using mutex\n");
    mutex_lock(&dev->mutex);
}

/* 更精细的检测 */
if (in_irq())       /* 仅在硬件中断上下文 */
if (in_softirq())   /* 仅在软中断上下文 */
if (in_atomic())    /* 在原子上下文(中断+持有自旋锁) */
死锁风险: 如果在中断上下文中调用了mutex_lock()或kmalloc(GFP_KERNEL)(可能触发内存回收而睡眠),内核会触发BUG: scheduling while atomic错误,导致系统挂死或Oops。I2C驱动中常见的错误是在中断处理函数中直接调用i2c_transfer()——该函数可能睡眠等待硬件完成。

4.7 本章知识结构总结

概念 本质 对I2C驱动的影响
中断向量 CPU硬件跳转地址 决定IRQ异常入口,驱动无需直接操作
GIC 中断路由与优先级控制器 需在设备树中正确配置中断类型和触发方式
中断号 从硬件ID到软件IRQ号的映射 驱动通过platform_get_resource获取
中断上下文 原子执行环境,不可睡眠 Top Half中只能使用自旋锁,不能调用i2c_transfer
进程上下文 可睡眠、可调度的执行环境 Bottom Half或中断线程中完成I2C消息处理