4. 中断处理函数编写规范:快速处理、避免睡眠、可重入性、返回值含义
中断处理函数(Interrupt Handler,也称为 ISR)是 Linux 内核中响应硬件事件的“第一响应人”。其编写质量直接决定系统的实时性、吞吐量和稳定性。本节从四个核心维度——快速处理、避免睡眠、可重入性、返回值含义——展开规范说明,并给出可落地的检查清单。
核心原则:中断上下文是原子上下文,不可阻塞、不可调度、不可访问用户空间。违反任何一条都会导致内核 panic、死锁或数据损坏。
4.1 快速处理:中断路径必须“短平快”
中断处理函数在关闭当前 CPU 中断(或至少是当前中断线)的情况下执行。执行时间越长,系统响应其他中断的延迟越大,甚至导致中断丢失。
4.1.1 处理时间基准
| 场景 | 建议最大执行时间 | 说明 |
|---|---|---|
| 顶级中断(IRQ) | < 100 µs | 超过此值应使用 threaded IRQ 或 tasklet |
| 共享中断 | < 50 µs | 多个设备共享一条 IRQ 线,必须快速判断归属 |
| 嵌套中断(若架构支持) | < 30 µs | 高优先级中断可打断低优先级 ISR |
4.1.2 快速处理清单
- 只做必要操作:读取硬件状态寄存器、清除中断标志、将数据放入缓冲区、唤醒等待队列或调度 tasklet/workqueue。
- 推迟繁重工作:协议解析、数据拷贝、I/O 操作、内存分配(GFP_KERNEL)必须放到下半部(tasklet、workqueue、threaded IRQ)。
- 避免循环等待:不要在 ISR 中轮询硬件状态;使用硬件 FIFO 或 DMA 完成中断。
- 使用
local_irq_save()保护共享数据:如果 ISR 与进程上下文共享数据,必须使用 spin_lock_irqsave()。
/* 正确示例:ISR 只做最小工作 */
static irqreturn_t my_isr(int irq, void *dev_id)
{
struct my_dev *dev = (struct my_dev *)dev_id;
uint32_t status;
status = ioread32(dev->regs + STATUS_REG);
if (!(status & IRQ_PENDING))
return IRQ_NONE; /* 非本设备中断 */
iowrite32(IRQ_CLEAR, dev->regs + CLEAR_REG); /* 清除中断标志 */
/* 将数据放入缓冲区,唤醒消费者 */
spin_lock(&dev->lock);
fifo_push(&dev->rx_fifo, ioread32(dev->regs + DATA_REG));
spin_unlock(&dev->lock);
tasklet_schedule(&dev->tasklet); /* 调度下半部处理 */
return IRQ_HANDLED;
}
性能指标:在 ARM Cortex-A72 @1.5GHz 上,一个空的 ISR 入口到返回耗时约 0.8 µs。每增加一次寄存器访问增加 0.1–0.3 µs。建议将 ISR 总耗时控制在 10 µs 以内。
4.2 避免睡眠:中断上下文不可阻塞
中断处理函数运行在中断上下文中,不允许调用任何可能导致睡眠的函数。睡眠意味着进程调度,而中断上下文没有进程结构(current 宏指向的是被中断的进程,而非 ISR 本身)。
4.2.1 禁止调用的函数列表
| 函数类别 | 具体函数示例 | 禁止原因 |
|---|---|---|
| 内存分配(可能阻塞) | kmalloc(size, GFP_KERNEL) |
GFP_KERNEL 可能触发页面回收/交换,导致睡眠 |
| 信号量/互斥锁 | down()、mutex_lock() |
这些锁允许调用者睡眠等待 |
| 用户空间访问 | copy_to_user()、copy_from_user() |
可能触发缺页异常,导致睡眠 |
| 调度相关 | schedule()、wait_event() |
显式让出 CPU,中断上下文不允许 |
| I/O 操作 | printk()(控制台输出) |
串口输出可能阻塞,生产环境应使用 trace_printk |
4.2.2 安全替代方案
- 内存分配:使用
kmalloc(size, GFP_ATOMIC)或预分配缓冲区。 - 锁机制:使用
spin_lock()/spin_lock_irqsave(),不可使用 mutex。 - 数据传递:使用无锁环形缓冲区(kfifo)或原子变量。
- 调试输出:使用
trace_printk()或 ftrace,避免printk()。
/* 错误示例:在 ISR 中调用可能导致睡眠的函数 */
static irqreturn_t bad_isr(int irq, void *dev_id)
{
char *buf;
struct my_data data;
buf = kmalloc(1024, GFP_KERNEL); /* 错误!可能睡眠 */
mutex_lock(&dev->mutex); /* 错误!mutex 允许睡眠 */
copy_from_user(&data, buf, sizeof(data)); /* 错误!缺页异常 */
kfree(buf);
mutex_unlock(&dev->mutex);
return IRQ_HANDLED;
}
调试技巧:启用内核配置
CONFIG_DEBUG_ATOMIC_SLEEP。当在原子上下文中检测到睡眠调用时,内核会打印 BUG: sleeping function called from invalid context 并输出调用栈。开发阶段务必开启此选项。
4.3 可重入性:中断处理函数必须可重入
可重入性指一个函数可以在执行过程中被再次安全调用。对于中断处理函数,可重入性主要面临两个场景:同一中断嵌套(高优先级中断打断低优先级 ISR)和 SMP 并发(多个 CPU 同时处理同一中断线)。
4.3.1 可重入性设计原则
| 原则 | 实现方式 | 说明 |
|---|---|---|
| 无全局/静态可变数据 | 使用 dev_id 参数传递设备私有结构 |
每个中断实例操作自己的数据结构 |
| 原子操作共享变量 | atomic_t、set_bit()、test_and_set_bit() |
避免非原子读-改-写序列 |
| 自旋锁保护临界区 | spin_lock_irqsave() / spin_unlock_irqrestore() |
防止本 CPU 中断和 SMP 竞态 |
| 避免使用 per-CPU 变量不当 | 使用 get_cpu_var() / put_cpu_var() 配对 |
确保在 ISR 中不会发生 CPU 迁移 |
4.3.2 可重入性检查清单
- 检查点 1:ISR 中是否访问了模块全局变量?如果是,必须加锁或改为 per-CPU。
- 检查点 2:ISR 中是否调用了其他可能不可重入的函数?(如某些硬件操作函数)
- 检查点 3:如果使用
request_threaded_irq(),主 handler 和 threaded handler 之间是否存在共享数据竞争? - 检查点 4:在 SMP 系统上,两个 CPU 同时进入同一个 ISR 是否安全?
/* 可重入 ISR 示例:使用 dev_id 和自旋锁 */
struct my_dev {
spinlock_t lock;
struct kfifo rx_fifo;
atomic_t irq_count;
};
static irqreturn_t reentrant_isr(int irq, void *dev_id)
{
struct my_dev *dev = (struct my_dev *)dev_id;
unsigned long flags;
uint32_t data;
atomic_inc(&dev->irq_count); /* 原子操作,无需锁 */
spin_lock_irqsave(&dev->lock, flags);
data = ioread32(dev->regs + DATA_REG);
kfifo_in(&dev->rx_fifo, &data, sizeof(data));
spin_unlock_irqrestore(&dev->lock, flags);
return IRQ_HANDLED;
}
4.4 返回值含义:正确通知内核中断处理状态
中断处理函数必须返回 irqreturn_t 类型值,告知内核该中断是否被本驱动处理。错误返回值会导致中断风暴或设备无响应。
4.4.1 返回值枚举
| 返回值 | 整数值 | 含义 | 使用场景 |
|---|---|---|---|
IRQ_NONE |
0 | 中断不是本设备产生的 | 共享中断线,检查状态寄存器后发现非本设备中断 |
IRQ_HANDLED |
1 | 中断已被本设备处理 | 正常处理完成,已清除中断标志 |
IRQ_WAKE_THREAD |
2 | 需要唤醒 threaded handler 继续处理 | 使用 request_threaded_irq() 注册了主 handler 和 threaded handler |
4.4.2 返回值选择决策树
- 步骤 1:读取硬件状态寄存器,判断中断是否来自本设备。
- 步骤 2:如果不是本设备中断 → 返回
IRQ_NONE。 - 步骤 3:如果是本设备中断:
- 如果所有工作都在 ISR 中完成 → 返回
IRQ_HANDLED。 - 如果还需要 threaded handler 处理 → 返回
IRQ_WAKE_THREAD。
- 如果所有工作都在 ISR 中完成 → 返回
/* 返回值使用示例:共享中断线 */
static irqreturn_t shared_isr(int irq, void *dev_id)
{
struct my_dev *dev = (struct my_dev *)dev_id;
uint32_t status;
status = ioread32(dev->regs + STATUS_REG);
if (!(status & MY_DEV_IRQ_MASK))
return IRQ_NONE; /* 不是本设备中断 */
/* 处理中断 */
iowrite32(IRQ_CLEAR, dev->regs + CLEAR_REG);
if (dev->use_threaded) {
/* 需要 threaded handler 处理数据 */
return IRQ_WAKE_THREAD;
}
/* 在 ISR 中完成所有处理 */
spin_lock(&dev->lock);
fifo_push(&dev->rx_fifo, ioread32(dev->regs + DATA_REG));
spin_unlock(&dev->lock);
return IRQ_HANDLED;
}
内核调试:通过
/proc/interrupts 查看每个中断的统计信息。如果某个中断的 IRQ_NONE 返回次数异常高,说明可能存在中断误触发或共享中断线设备驱动未正确判断归属。
4.5 综合规范检查表
| 检查项 | 规范要求 | 验证方法 |
|---|---|---|
| 执行时间 | < 100 µs,建议 < 10 µs | ftrace 测量 ISR 持续时间 |
| 睡眠禁止 | 无 GFP_KERNEL、mutex、copy_to/from_user | 开启 CONFIG_DEBUG_ATOMIC_SLEEP |
| 可重入性 | 使用 dev_id、自旋锁、原子操作 | SMP 压力测试 + lockdep 验证 |
| 返回值正确性 | IRQ_NONE / IRQ_HANDLED / IRQ_WAKE_THREAD | 检查 /proc/interrupts 统计 |
| 共享数据保护 | spin_lock_irqsave() 保护 ISR 与进程上下文共享数据 | lockdep 检测潜在死锁 |
常见陷阱:
- 在 ISR 中调用
printk()导致控制台输出阻塞,进而引发中断超时。 - 忘记在 ISR 中清除中断标志,导致中断反复触发,形成中断风暴。
- 在共享中断线上始终返回
IRQ_HANDLED,掩盖了其他设备的中断问题。 - 使用
GFP_KERNEL分配内存,在内存压力下导致 ISR 睡眠。