4. 中断处理函数编写:顶半部与底半部、快速中断与慢速中断、中断处理函数返回值

中断处理函数是驱动与硬件交互最频繁、对实时性要求最高的代码路径。Linux 内核将中断处理拆分为顶半部(Top Half)和底半部(Bottom Half),以平衡中断响应速度与处理复杂度。本章深入剖析中断处理函数的编写规范、快速/慢速中断的区分标准以及返回值语义,帮助开发者写出高效、稳定的中断服务例程(ISR)。

中断处理函数执行流程 硬件中断触发 顶半部(Top Half) 关中断 · 保存寄存器 · 调度底半部 软中断 / Tasklet 开中断 · 处理数据 · 唤醒等待队列 工作队列(Workqueue) 进程上下文 · 可睡眠 · 复杂操作 中断处理完成

4.1 顶半部与底半部

中断处理函数必须快速执行,否则会阻塞其他中断和系统调度。Linux 将中断处理拆分为两个阶段:

特性 顶半部(Top Half) 底半部(Bottom Half)
执行上下文 中断上下文(原子上下文) 软中断上下文 / 进程上下文
中断状态 关中断(当前 CPU) 开中断(可被其他中断抢占)
可调用函数 仅原子操作、寄存器读写 可睡眠、可加锁、可调度
典型操作 保存状态、清除中断标志、调度底半部 数据拷贝、协议解析、唤醒用户进程
执行时长 微秒级(< 100 µs) 毫秒级(可延迟执行)
核心原则:顶半部只做最少必要操作,将耗时工作推迟到底半部。顶半部中禁止调用 copy_from_user()、kmalloc(GFP_KERNEL)、mutex_lock() 等可能睡眠的函数。

4.1.1 底半部实现机制对比

机制 上下文 并发度 可睡眠 适用场景
软中断(Softirq) 中断上下文 高(同类型可多 CPU 并行) 否 网络收发、块设备 I/O
Tasklet 中断上下文 低(同类型串行) 否 一般设备驱动
工作队列(Workqueue) 进程上下文 中(由内核线程调度) 是 需要睡眠的复杂操作
线程化中断(Threaded IRQ) 进程上下文 中(内核线程) 是 替代传统底半部,简化设计
推荐做法:新驱动优先使用 线程化中断(request_threaded_irq()),将顶半部和底半部合并为一个线程函数,避免手动管理 Tasklet 或工作队列,降低编码复杂度。

4.1.2 顶半部与底半部代码示例

/* 传统方式:顶半部 + Tasklet 底半部 */
static void my_device_tasklet_handler(unsigned long data)
{
    struct my_dev *dev = (struct my_dev *)data;
    /* 底半部:开中断环境,可加锁、可调度 */
    spin_lock(&dev->lock);
    /* 读取 FIFO 数据、更新统计、唤醒等待队列 */
    spin_unlock(&dev->lock);
    wake_up_interruptible(&dev->wq);
}

static irqreturn_t my_device_isr(int irq, void *dev_id)
{
    struct my_dev *dev = (struct my_dev *)dev_id;
    /* 顶半部:保存状态,调度底半部 */
    dev->status = readl(dev->regs + REG_STATUS);
    /* 清除中断标志 */
    writel(INT_CLEAR, dev->regs + REG_INT_CLR);
    /* 调度 Tasklet */
    tasklet_schedule(&dev->tasklet);
    return IRQ_HANDLED;
}

/* 线程化中断方式(推荐) */
static irqreturn_t my_device_threaded_isr(int irq, void *dev_id)
{
    struct my_dev *dev = (struct my_dev *)dev_id;
    /* 此函数运行在进程上下文,可睡眠 */
    mutex_lock(&dev->mutex);
    /* 读取硬件状态、处理数据、与用户空间交互 */
    mutex_unlock(&dev->mutex);
    return IRQ_HANDLED;
}

/* 注册线程化中断 */
request_threaded_irq(irq, my_device_isr, my_device_threaded_isr,
                     IRQF_TRIGGER_RISING, "my_device", dev);

4.2 快速中断与慢速中断

Linux 内核在早期版本中区分“快速中断”和“慢速中断”,现代内核虽然不再强制区分,但理解其设计思想对中断性能调优仍然重要。

特性 快速中断(Fast IRQ) 慢速中断(Slow IRQ)
中断处理期间 关中断(当前 CPU) 开中断(允许嵌套)
注册标志 IRQF_DISABLED(旧版) 无特殊标志
执行时长 极短(< 50 µs) 可较长(> 100 µs)
典型场景 定时器、DMA 完成、高频率外设 按键、热插拔、错误处理
现代等价 顶半部 + 软中断 线程化中断
注意:从 Linux 2.6.39 开始,IRQF_DISABLED 标志已被废弃。现代内核中所有中断处理函数默认在关中断环境下执行顶半部,不再区分快速/慢速。开发者应关注的是顶半部执行时长,而非注册标志。
快速中断 vs 慢速中断时序对比 t 快速中断: 关中断 顶半部(50µs) 开中断 → 其他中断可响应 慢速中断: 关中断 顶半部(200µs) 开中断 → 中断延迟累积 性能影响 • 快速中断:关中断时间短,系统实时性高,适合高频外设 • 慢速中断:关中断时间长,可能导致其他中断丢失或调度延迟

4.3 中断处理函数返回值

中断处理函数必须返回 irqreturn_t 类型值,内核根据返回值判断中断是否被正确处理。错误返回值会导致内核误判中断状态,引发系统不稳定。

返回值 宏定义 含义 使用场景
IRQ_NONE 0 中断不是本设备产生的 共享中断线时,检测到非本设备中断
IRQ_HANDLED 1 中断已被正确处理 正常处理完成
IRQ_WAKE_THREAD 2 唤醒线程化中断处理函数 线程化中断的顶半部返回此值
关键规则:
  • 如果 ISR 检测到中断不是本设备产生,必须返回 IRQ_NONE,否则内核会误认为设备持续产生中断,导致 irq storm(中断风暴)。
  • 使用 request_threaded_irq() 时,顶半部返回 IRQ_WAKE_THREAD 来唤醒底半部线程。
  • 不要返回 IRQ_HANDLED 当实际未处理任何操作,这会破坏内核的中断统计和调试机制。

4.3.1 共享中断的正确处理

static irqreturn_t my_device_isr(int irq, void *dev_id)
{
    struct my_dev *dev = (struct my_dev *)dev_id;
    unsigned int status;

    /* 读取中断状态寄存器 */
    status = readl(dev->regs + REG_INT_STATUS);

    /* 检查是否为本设备中断 */
    if (!(status & INT_PENDING)) {
        /* 不是本设备产生,返回 IRQ_NONE */
        return IRQ_NONE;
    }

    /* 清除中断标志 */
    writel(INT_CLEAR, dev->regs + REG_INT_CLR);

    /* 处理中断(顶半部) */
    dev->event_count++;

    /* 调度底半部 */
    tasklet_schedule(&dev->tasklet);

    /* 返回 IRQ_HANDLED 表示已处理 */
    return IRQ_HANDLED;
}

4.3.2 线程化中断返回值示例

/* 顶半部:仅检查中断源,唤醒线程 */
static irqreturn_t my_device_top_isr(int irq, void *dev_id)
{
    struct my_dev *dev = (struct my_dev *)dev_id;

    if (!(readl(dev->regs + REG_INT_STATUS) & INT_PENDING))
        return IRQ_NONE;

    /* 清除中断标志(必须在顶半部完成) */
    writel(INT_CLEAR, dev->regs + REG_INT_CLR);

    /* 唤醒线程化底半部 */
    return IRQ_WAKE_THREAD;
}

/* 底半部线程:运行在进程上下文,可睡眠 */
static irqreturn_t my_device_bottom_thread(int irq, void *dev_id)
{
    struct my_dev *dev = (struct my_dev *)dev_id;

    /* 可调用睡眠函数 */
    mutex_lock(&dev->mutex);
    /* 复杂数据处理、与用户空间通信 */
    mutex_unlock(&dev->mutex);

    return IRQ_HANDLED;
}

/* 注册线程化中断 */
request_threaded_irq(irq, my_device_top_isr, my_device_bottom_thread,
                     IRQF_SHARED | IRQF_TRIGGER_FALLING,
                     "my_device", dev);
调试技巧:通过 /proc/interrupts 查看中断统计,如果某个中断号的 IRQ_NONE 计数持续增长,说明共享中断线中存在设备未正确识别自己的中断,需要检查 IRQ_NONE 返回逻辑。

4.4 中断处理函数编写最佳实践

实践 说明 反例
顶半部最小化 只做寄存器保存、中断标志清除、底半部调度 在顶半部中调用 printk() 或 memcpy()
使用线程化中断 简化设计,避免手动管理 Tasklet/工作队列 在顶半部中处理复杂协议解析
正确返回值 非本设备中断返回 IRQ_NONE 所有中断都返回 IRQ_HANDLED
避免死锁 顶半部使用 spin_lock(),底半部使用 mutex_lock() 在顶半部中使用 mutex_lock()
测量执行时间 使用 ftrace 或 tracepoints 监控 ISR 时长 不关注 ISR 执行时间,导致中断延迟
常见陷阱:
  • 中断标志未清除:导致同一中断反复触发,系统卡死。
  • 顶半部调用睡眠函数:内核崩溃(BUG: scheduling while atomic)。
  • 共享中断未返回 IRQ_NONE:触发中断风暴,系统响应变慢。
  • 底半部访问已释放资源:使用 cancel_work_sync() 或 tasklet_kill() 确保卸载时底半部已停止。
中断处理函数编写决策树 编写中断处理函数 是否需要复杂处理(可睡眠)? 是 → 使用线程化中断 否 → 使用顶半部+Tasklet 顶半部:检查中断源 清除标志 → IRQ_WAKE_THREAD 底半部线程:可睡眠处理 顶半部:保存状态 清除标志 → tasklet_schedule() Tasklet:原子上下文处理 返回 IRQ_HANDLED 或 IRQ_NONE