1. 中断基础概念

中断是嵌入式 Linux 驱动开发中最核心的异步事件处理机制。它允许硬件在发生特定事件时暂停 CPU 当前执行流,跳转到预先注册的处理函数,完成后再恢复原执行流。理解中断的定义、向量表、处理流程和上下文是掌握中断底半部与 DMA 协同设计的前提。

1.1 中断定义与分类

中断(Interrupt)是一种由硬件或软件触发的信号,用于通知处理器发生了需要立即关注的事件。根据触发源和特性,中断可分为以下几类:

分类 触发源 特点 典型示例
硬件中断 外设(GPIO、定时器、DMA、UART 等) 异步、可屏蔽、优先级可配置 按键按下触发 GPIO 中断
软件中断 CPU 执行指令(INT n、SWI) 同步、用于系统调用或异常处理 ARM 的 SWI 指令触发系统调用
异常 CPU 内部错误或特殊状态 同步、不可屏蔽、优先级最高 除零错误、缺页异常
NMI(不可屏蔽中断) 硬件紧急事件 不可屏蔽、优先级最高 看门狗超时、电源故障
驱动开发要点:在 Linux 驱动中,硬件中断是最常打交道的类型。DMA 传输完成、FIFO 半满、传输错误等事件都会通过硬件中断通知 CPU。理解硬件中断的触发条件和响应时序,是设计高效底半部的基础。

1.2 中断向量表

中断向量表(Interrupt Vector Table, IVT)是存储在固定内存地址的一张表,表中每一项指向一个中断服务程序(ISR)的入口地址。当 CPU 检测到中断信号时,硬件自动根据中断号从向量表中取出对应的 ISR 地址并跳转执行。

关键概念:中断向量号与 ISR 地址的映射关系由体系结构决定。ARM Cortex-A 系列使用向量表基址寄存器(VBAR)定位向量表;x86 架构则使用 IDT(中断描述符表)。Linux 内核在启动时会初始化向量表,并将硬件中断映射到内核的通用中断处理框架。
中断向量表结构示意图 中断向量表 向量 0:复位 向量 1:未定义指令 向量 2:SWI 向量 3:IRQ 向量 4:FIQ 中断服务程序 (ISR) 复位 ISR 未定义指令 ISR SWI ISR IRQ ISR (通用处理) FIQ ISR 说明 • 向量表基址由 VBAR 寄存器指定 • 每个向量占用 4 字节(ARM) • IRQ 向量指向内核通用中断入口 • 硬件自动保存/恢复关键寄存器 • 中断号与向量号通过 GIC 映射 • DMA 完成中断映射到 IRQ 向量

1.3 中断处理流程

从硬件触发中断到 Linux 内核完成处理,整个过程分为硬件自动阶段和软件处理阶段。以下是标准的中断处理流程:

  1. 硬件阶段:外设拉高中断请求线 → CPU 完成当前指令 → 保存 CPSR 和返回地址 → 切换到中断模式 → 从向量表读取 ISR 地址 → 跳转执行。
  2. 内核入口:进入 __irq_svc 或 __irq_usr 汇编入口 → 保存所有寄存器到栈 → 调用 handle_arch_irq。
  3. 中断控制器查询:通过 GIC(通用中断控制器)读取中断号 → 查找 irq_desc 描述符 → 调用 generic_handle_irq。
  4. 执行 ISR:调用驱动注册的 request_irq 回调函数 → 执行顶半部处理(快速、不可阻塞)→ 返回。
  5. 恢复现场:恢复寄存器 → 执行 eret 指令返回被中断的上下文。
Linux 中断处理完整流程 ① 硬件触发中断 ② CPU 硬件响应 ③ 保存现场 (栈) ④ GIC 查询中断号 ⑤ 执行驱动 ISR ⑥ 恢复现场 → 返回 流程说明 • 硬件阶段:CPU 自动完成 • 保存 SPSR_irq 和返回地址 • 切换到 IRQ 模式,关中断 • 内核入口保存 r0-r14 • GIC 读取 IAR 寄存器获取中断号 • 调用 handle_arch_irq • 查找 irq_desc → 执行 action • ISR 必须快速返回 • 顶半部只做最紧急操作 • 耗时操作委托给底半部 • DMA 完成中断适合用 tasklet • eret 指令恢复 CPSR 和 PC

1.4 中断上下文

中断上下文(Interrupt Context)是指 CPU 在执行 ISR 时所处的特殊运行环境。与进程上下文相比,中断上下文有严格的约束条件:

对比项 进程上下文 中断上下文
可睡眠 是(通过 schedule()) 否(不可调用可能睡眠的函数)
可被抢占 是(内核抢占开启时) 否(中断上下文不可被抢占)
持有锁 可以使用 mutex 只能使用 spinlock
访问用户空间 可以(copy_from_user) 否(无进程关联)
current 宏 指向当前进程 指向被中断的进程(不可依赖)
执行时间 无严格限制 必须极短(微秒级)
典型用途 系统调用、文件操作 硬件响应、数据搬运、唤醒等待队列
常见陷阱:在中断上下文中调用 kmalloc(GFP_KERNEL) 会导致内核崩溃,因为 GFP_KERNEL 可能触发内存回收而睡眠。必须使用 GFP_ATOMIC 标志。同样,mutex_lock()、down_interruptible() 等可能导致睡眠的函数在中断上下文中绝对禁止使用。

Linux 内核提供 in_interrupt() 和 in_irq() 宏来检测当前是否处于中断上下文:

#include <linux/preempt.h>

/* 检查是否处于任何中断上下文(硬中断 + 软中断) */
if (in_interrupt()) {
    pr_info("当前在中断上下文中执行\n");
}

/* 检查是否处于硬中断上下文 */
if (in_irq()) {
    pr_info("当前在硬中断(顶半部)中执行\n");
}

/* 检查是否处于软中断上下文 */
if (in_softirq()) {
    pr_info("当前在软中断(底半部)中执行\n");
}
与 DMA 协同设计的关系:DMA 传输完成中断在顶半部中只做两件事:① 读取 DMA 状态寄存器确认传输完成;② 调度 tasklet 或 workqueue 处理数据。顶半部必须极快返回,将数据解析、协议处理、用户空间通知等耗时操作全部交给底半部。这正是本课程后续章节的核心设计模式。
中断上下文 vs 进程上下文 进程上下文 ✅ 可以睡眠 (schedule, wait_event) ✅ 可以使用 mutex / semaphore ✅ 可以访问用户空间内存 ✅ 可以调用 kmalloc(GFP_KERNEL) 中断上下文 ❌ 禁止睡眠 (内核会 panic) ❌ 只能使用 spinlock ❌ 不能访问用户空间 ❌ 必须使用 GFP_ATOMIC

1.5 中断上下文中的编程约束

基于中断上下文的特性,驱动开发者在编写 ISR 时必须遵守以下硬性规则:

  • 禁止调用任何可能睡眠的函数:包括但不限于 kmalloc(GFP_KERNEL)、mutex_lock()、down_interruptible()、copy_from_user()、msleep()、wait_event()。
  • 使用原子操作或自旋锁保护共享数据:中断上下文与进程上下文共享的数据必须使用 spin_lock_irqsave() 保护,防止死锁。
  • ISR 执行时间必须极短:通常控制在几十微秒以内。超过 100μs 的 ISR 会导致系统响应延迟增加,甚至触发看门狗复位。
  • 避免浮点运算:中断上下文不保存 FPU 状态,执行浮点指令会导致不可预知的结果。
  • 使用 GFP_ATOMIC 分配内存:如果必须在 ISR 中分配内存,必须使用 kmalloc(size, GFP_ATOMIC)。
设计建议:对于 DMA 传输完成中断,顶半部 ISR 的标准做法是:读取 DMA 状态寄存器确认传输完成 → 清除中断标志 → 调用 tasklet_schedule() 调度底半部 → 立即返回。所有数据搬运、协议解析、缓冲区管理全部交给 tasklet 处理。这样既保证了顶半部的快速响应,又避免了在中断上下文中执行耗时操作。