3. Linux中断子系统:中断注册API、中断处理函数与中断上下文

中断处理,说白了就是CPU被硬件“打断”后怎么响应。很多初学者觉得中断就是写个函数挂上去,其实里面的门道不少。我刚开始做驱动时,就因为没搞懂上半部和下半部的区别,导致系统卡死,最后查了三天才找到原因。

这一节,我们重点讲三个东西:怎么注册中断、中断处理函数怎么写、以及中断上下文里能干什么不能干什么。

3.1 中断注册API:request_irq 与 devm_request_irq

注册中断,最常用的就是 request_irq。它的原型长这样:

int request_irq(unsigned int irq, 
                irq_handler_t handler,
                unsigned long flags,
                const char *name, 
                void *dev);

参数不多,但每个都很关键:

参数 说明 注意事项
irq 中断号 通常从设备树或平台数据获取
handler 中断处理函数指针 上半部,在中断上下文中执行
flags 中断标志 如 IRQF_TRIGGER_RISING、IRQF_SHARED
name 设备名称 用于 /proc/interrupts 显示
dev 设备标识 共享中断时用于区分
我的习惯:能用 devm_request_irq 就别用 request_irq。前者是资源管理版本,驱动卸载或设备移除时会自动释放中断,省得你忘了调用 free_irq。我在一个项目里就因为忘记释放中断,导致第二次加载驱动时直接崩溃。

devm_request_irq 的用法几乎一样:

int devm_request_irq(struct device *dev, 
                     unsigned int irq,
                     irq_handler_t handler,
                     unsigned long flags,
                     const char *name, 
                     void *dev_id);

唯一的区别是多了一个 struct device *dev 参数。内核通过这个设备指针来管理中断的生命周期。

3.2 中断处理函数:上半部与下半部

中断处理函数分两部分,这是Linux中断子系统的核心设计思想。为什么要分?因为中断上下文里不能睡眠,不能做耗时操作。你想想看,如果中断处理函数里做个复杂的计算或者等待I/O,整个系统都会被拖死。

上半部(Top Half):在中断上下文中立即执行,要求快速完成。通常只做最紧急的事:

  • 清除中断标志
  • 保存硬件状态
  • 调度下半部

下半部(Bottom Half):稍后执行,可以做耗时操作。Linux提供了三种下半部机制:

机制 特点 适用场景
软中断(Softirq) 性能最高,但使用复杂 网络、块设备等高性能场景
Tasklet 基于软中断,使用简单 大多数驱动场景
工作队列(Workqueue) 在进程上下文执行,可以睡眠 需要睡眠或大量计算的场景
核心原则:上半部越快越好,下半部能放就放。我见过一个驱动工程师把整个数据处理都放在上半部,结果系统响应延迟从微秒级变成了毫秒级。

来看一个典型的例子:

/* 上半部:中断处理函数 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = (struct my_device *)dev_id;
    
    /* 1. 清除中断标志 */
    writel(0x01, dev->regs + INT_CLEAR);
    
    /* 2. 保存关键状态 */
    dev->status = readl(dev->regs + INT_STATUS);
    
    /* 3. 调度下半部 */
    tasklet_schedule(&dev->tasklet);
    
    return IRQ_HANDLED;
}

/* 下半部:Tasklet处理函数 */
static void my_tasklet_handler(unsigned long data)
{
    struct my_device *dev = (struct my_device *)data;
    
    /* 这里可以做耗时操作 */
    process_data(dev);
    wake_up_interruptible(&dev->wait_queue);
}

3.3 中断上下文:能做什么,不能做什么

中断上下文,说白了就是CPU正在执行中断处理函数时的状态。这个状态下,内核有很多限制。我刚开始做驱动时,在中断处理函数里调用了 msleep,结果系统直接死锁。

中断上下文中的“禁区”:

  • 不能睡眠:不能调用 msleep、wait_event、mutex_lock 等可能导致睡眠的函数
  • 不能访问用户空间:不能使用 copy_from_user、copy_to_user
  • 不能持有互斥锁:只能使用自旋锁(spinlock)
  • 不能做复杂的计算:时间太长会影响系统响应
我曾经踩过的坑:在中断处理函数里调用了 printk 打印调试信息。你以为只是打印几个字符?实际上 printk 在控制台繁忙时可能会睡眠,直接导致内核崩溃。调试时可以用 printk,但正式代码里一定要去掉。

中断上下文中“可以”做的事:

  • 读写硬件寄存器
  • 操作自旋锁保护的共享数据
  • 调度下半部(tasklet、工作队列)
  • 唤醒等待队列(wake_up)

3.4 知识结构总览

下面这张图把中断子系统的核心脉络画出来了,建议你保存下来对照学习:

Linux中断子系统核心结构 硬件中断触发 request_irq / devm_request_irq 上半部:中断处理函数 软中断 (Softirq) Tasklet 工作队列 (Workqueue) 中断上下文限制:不能睡眠、不能访问用户空间、只能使用自旋锁 上半部负责快速响应硬件,下半部处理耗时逻辑。两者配合才能写出稳定的驱动

3.5 实战建议

最后,分享几个我在项目中积累的经验:

  1. 注册中断前先确认中断号:用 cat /proc/interrupts 查看系统中已注册的中断,避免冲突。
  2. 共享中断要小心:使用 IRQF_SHARED 标志时,dev_id 参数不能为 NULL,否则无法区分是哪个设备触发了中断。
  3. 上半部越短越好:我一般要求上半部不超过50条指令,超过的就扔到下半部去。
  4. 调试时用 tracepoint:别在中断处理函数里加 printk,用 trace_irq_handler_entry 和 trace_irq_handler_exit 更安全。
一个小技巧:如果你不确定某个函数能否在中断上下文中调用,可以查内核文档或者看函数注释。大部分内核API都会明确标注“This function may sleep”或“This function must be called with interrupts disabled”。