3. Linux中断子系统框架:中断注册API与处理流程

中断子系统是Linux内核响应外部硬件事件的枢纽。在Android BSP开发中,触摸屏、传感器、相机、Wi-Fi等外设均依赖中断机制向CPU报告事件。本章深入剖析中断注册API(request_irq / request_threaded_irq)的语义差异、参数陷阱,以及从硬件中断触发到驱动回调完成的完整处理流程。

3.1 中断注册API:request_irq 与 request_threaded_irq

Linux内核提供两套中断注册接口,分别对应纯顶半部(Top Half)和线程化中断(Threaded IRQ)两种处理模型。BSP工程师必须根据设备实时性要求和上下文限制选择合适的API。

3.1.1 request_irq:传统顶半部注册

request_irq 是最基础的中断注册函数,注册的处理函数在中断上下文(硬中断)中执行,必须快速、不可睡眠。

int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev_id);
参数 说明 常见取值 / 注意事项
irq 中断号 来自设备树(DT)或平台数据,需与硬件IRQ线对应
handler 顶半部回调函数 返回 IRQ_NONE(未处理)或 IRQ_HANDLED(已处理)
flags 中断触发类型与属性 IRQF_TRIGGER_RISING、IRQF_SHARED、IRQF_NO_SUSPEND 等
name 设备名称(/proc/interrupts 显示) 建议使用驱动名+实例,如 "msm_touch"
dev_id 设备标识指针 共享中断时必须唯一;释放中断时用于匹配
⚠ 顶半部约束: 处理函数运行在关中断(或屏蔽当前IRQ线)的上下文中,禁止调用 msleep、mutex_lock、copy_to_user 等可能睡眠的函数。典型操作:读状态寄存器、清中断标志、唤醒底半部(tasklet/workqueue)。

3.1.2 request_threaded_irq:线程化中断注册

request_threaded_irq 允许将中断处理拆分为顶半部(handler)和线程化底半部(thread_fn)。顶半部执行极简的硬件确认,线程化底半部在可睡眠的内核线程中运行,适合需要I2C/SPI通信、内存分配等耗时操作的设备。

int request_threaded_irq(unsigned int irq,
                         irq_handler_t handler,
                         irq_handler_t thread_fn,
                         unsigned long flags,
                         const char *name,
                         void *dev_id);
参数 说明 典型场景
handler 顶半部(可为 NULL) 若为 NULL,内核自动调用 irq_default_primary_handler,直接唤醒线程
thread_fn 线程化底半部 可睡眠,执行协议解析、数据拷贝、上报输入事件等
💡 最佳实践: 对于Android触摸屏驱动,顶半部仅读取中断状态寄存器并清中断,线程化底半部执行I2C读取触摸坐标、上报 input_report_abs。这避免了在硬中断中阻塞I2C总线。

3.1.3 两种API的对比与选择

对比维度 request_irq request_threaded_irq
执行上下文 硬中断上下文(关中断) 顶半部:硬中断;底半部:内核线程(可睡眠)
可睡眠 ❌ 禁止 ✅ thread_fn 中允许
实时性 高(微秒级响应) 中(受内核调度影响,毫秒级)
适用设备 GPIO按键、DMA完成、定时器 触摸屏、传感器、MMC/SD卡、USB
代码复杂度 低 中(需拆分顶半部与线程函数)

3.2 中断处理流程:从硬件触发到驱动回调

理解中断处理的全流程是优化驱动响应延迟的基础。下图展示了从外设拉高IRQ线到驱动顶半部/底半部执行完毕的完整路径。

Linux中断处理全流程 Linux中断处理全流程(硬件→驱动) 外设触发中断 中断控制器(GIC)仲裁与分发 CPU跳转异常向量表 内核通用中断层(irq_enter / handle_IRQ) 驱动顶半部(handler) 返回 IRQ_HANDLED 唤醒 tasklet / workqueue 唤醒 irq_thread 说明:顶半部执行完毕后,根据注册方式选择立即返回、调度底半部或唤醒线程化处理函数

3.2.1 流程关键阶段解析

  1. 外设触发中断: 外设(如触摸控制器)拉高IRQ线,向中断控制器(GIC)发送中断信号。
  2. GIC仲裁与分发: GIC根据优先级、亲和性(affinity)将中断分发给目标CPU核心。Android big.LITTLE架构中,通常将外设中断绑定到little核以节省功耗。
  3. CPU异常向量表: CPU保存当前上下文(PC、CPSR等),跳转到异常向量表中的中断入口(vectors 或 VBAR_EL1)。
  4. 内核通用中断层: 调用 irq_enter() 标记进入中断上下文,然后通过 handle_IRQ() 或 generic_handle_irq() 查找已注册的 irq_desc 描述符。
  5. 驱动顶半部: 执行驱动注册的 handler。典型操作:读取硬件状态寄存器、清除中断挂起位、记录时间戳。
  6. 底半部调度: 根据注册方式,顶半部返回后内核自动调度底半部(tasklet/workqueue)或唤醒 irq_thread 执行 thread_fn。

3.3 中断描述符与irq_chip:内核视角的数据结构

每个中断号在内核中对应一个 irq_desc 结构体,其中包含中断处理函数链表、中断芯片(irq_chip)操作、触发类型、亲和性掩码等关键信息。

struct irq_desc {
    struct irq_common_data  irq_common_data;
    struct irq_data         irq_data;
    unsigned int __percpu   *kstat_irqs;
    irq_flow_handler_t      handle_irq;      // 高层级处理函数(如 handle_edge_irq)
    struct irqaction        *action;         // 驱动注册的 action 链表
    unsigned int            status_use_accessors;
    ...
};

struct irq_chip {
    const char      *name;
    void (*irq_mask)(struct irq_data *data);     // 屏蔽中断
    void (*irq_unmask)(struct irq_data *data);   // 取消屏蔽
    void (*irq_ack)(struct irq_data *data);      // 应答中断
    void (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
    ...
};
🔍 调试技巧: 通过 /proc/interrupts 查看每个中断号的触发次数和CPU亲和性;通过 /proc/irq/{irq}/smp_affinity 手动绑定中断到指定CPU核心,用于隔离关键中断的延迟抖动。

3.4 中断处理延迟关键指标

指标 定义 Android典型要求 优化方向
中断延迟(Interrupt Latency) 从硬件拉高IRQ到驱动顶半部开始执行的时间 < 10 µs(硬实时外设) 减少关中断区间、使用 IRQF_NO_SUSPEND
顶半部执行时间 handler 函数的执行耗时 < 50 µs 仅做最小硬件操作,将耗时工作移到底半部
底半部调度延迟 从顶半部返回到底半部开始执行的时间 < 1 ms(线程化中断) 提高线程优先级(sched_setscheduler)
总处理时间 从触发到驱动完成数据上报的总耗时 < 5 ms(触摸事件) 合理拆分顶半部/底半部,避免不必要的数据拷贝

3.5 实战案例:触摸屏中断注册与处理

以下代码展示了一个Android触摸屏驱动使用 request_threaded_irq 注册中断的典型模式。

static irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
    struct touch_device *tdev = dev_id;

    // 顶半部:仅读取中断状态并清中断
    tdev->int_status = readl(tdev->regs + INT_STATUS);
    writel(tdev->int_status, tdev->regs + INT_CLEAR);

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

static irqreturn_t touch_irq_thread_fn(int irq, void *dev_id)
{
    struct touch_device *tdev = dev_id;
    struct touch_data td;

    // 线程化底半部:通过I2C读取触摸坐标(可睡眠)
    i2c_read_block(tdev->client, REG_TOUCH_DATA, (u8 *)&td, sizeof(td));

    // 上报输入事件
    input_report_abs(tdev->input, ABS_MT_POSITION_X, td.x);
    input_report_abs(tdev->input, ABS_MT_POSITION_Y, td.y);
    input_mt_sync(tdev->input);
    input_sync(tdev->input);

    return IRQ_HANDLED;
}

static int touch_probe(struct platform_device *pdev)
{
    struct touch_device *tdev;
    int irq, ret;

    // ... 分配设备结构体、映射寄存器、注册输入设备 ...

    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    ret = request_threaded_irq(irq,
                               touch_irq_handler,
                               touch_irq_thread_fn,
                               IRQF_TRIGGER_RISING | IRQF_ONESHOT,
                               "msm_touch",
                               tdev);
    if (ret) {
        dev_err(&pdev->dev, "failed to request threaded irq\n");
        return ret;
    }

    return 0;
}
💡 IRQF_ONESHOT 标志: 在线程化中断中,IRQF_ONESHOT 保证在顶半部返回后、线程化底半部执行完毕前,中断线保持屏蔽状态,防止同一中断嵌套触发。对于电平触发的中断,此标志尤为重要。

3.6 本章小结

  • request_irq 适用于快速、不可睡眠的顶半部处理;request_threaded_irq 将耗时操作移入内核线程,是Android复杂外设驱动的首选。
  • 中断处理流程从外设触发→GIC分发→CPU异常向量→内核通用层→驱动顶半部→底半部,每个阶段都可能成为延迟瓶颈。
  • 通过 /proc/interrupts 和 smp_affinity 可以监控和优化中断分布,降低关键中断的响应抖动。
  • 实战中,顶半部应保持极简(读状态+清中断),线程化底半部负责协议解析和数据上报,这是Android BSP驱动中断处理的黄金法则。