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 以内。
中断处理时间线:快速路径 vs 慢速路径 中断触发 保存上下文 ISR 执行 调度下半部 恢复上下文 中断返回 说明:ISR 执行阶段(红色)应尽可能短,仅完成清除中断标志、读取最小数据、调度下半部。 整个中断处理路径(从触发到返回)建议控制在 10 µs 以内,超过 100 µs 必须使用 threaded IRQ。

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;
}
可重入中断场景:SMP 与嵌套中断 CPU 0 CPU 1 ISR 执行中 访问共享数据 ISR 执行中 访问共享数据 数据竞争!需要自旋锁保护 说明:在 SMP 系统中,两个 CPU 可能同时执行同一个 ISR。如果 ISR 访问共享数据结构,必须使用 spin_lock_irqsave() 保护。 嵌套中断场景类似:高优先级中断打断低优先级 ISR,同样需要可重入设计。

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. 步骤 1:读取硬件状态寄存器,判断中断是否来自本设备。
  2. 步骤 2:如果不是本设备中断 → 返回 IRQ_NONE。
  3. 步骤 3:如果是本设备中断:
    • 如果所有工作都在 ISR 中完成 → 返回 IRQ_HANDLED。
    • 如果还需要 threaded handler 处理 → 返回 IRQ_WAKE_THREAD。
/* 返回值使用示例:共享中断线 */
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 睡眠。