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 内核在启动时会初始化向量表,并将硬件中断映射到内核的通用中断处理框架。
1.3 中断处理流程
从硬件触发中断到 Linux 内核完成处理,整个过程分为硬件自动阶段和软件处理阶段。以下是标准的中断处理流程:
- 硬件阶段:外设拉高中断请求线 → CPU 完成当前指令 → 保存 CPSR 和返回地址 → 切换到中断模式 → 从向量表读取 ISR 地址 → 跳转执行。
- 内核入口:进入
__irq_svc或__irq_usr汇编入口 → 保存所有寄存器到栈 → 调用handle_arch_irq。 - 中断控制器查询:通过 GIC(通用中断控制器)读取中断号 → 查找 irq_desc 描述符 → 调用
generic_handle_irq。 - 执行 ISR:调用驱动注册的
request_irq回调函数 → 执行顶半部处理(快速、不可阻塞)→ 返回。 - 恢复现场:恢复寄存器 → 执行
eret指令返回被中断的上下文。
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 处理数据。顶半部必须极快返回,将数据解析、协议处理、用户空间通知等耗时操作全部交给底半部。这正是本课程后续章节的核心设计模式。
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 处理。这样既保证了顶半部的快速响应,又避免了在中断上下文中执行耗时操作。