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线到驱动顶半部/底半部执行完毕的完整路径。
3.2.1 流程关键阶段解析
- 外设触发中断: 外设(如触摸控制器)拉高IRQ线,向中断控制器(GIC)发送中断信号。
- GIC仲裁与分发: GIC根据优先级、亲和性(affinity)将中断分发给目标CPU核心。Android big.LITTLE架构中,通常将外设中断绑定到little核以节省功耗。
- CPU异常向量表: CPU保存当前上下文(PC、CPSR等),跳转到异常向量表中的中断入口(
vectors或VBAR_EL1)。 - 内核通用中断层: 调用
irq_enter()标记进入中断上下文,然后通过handle_IRQ()或generic_handle_irq()查找已注册的irq_desc描述符。 - 驱动顶半部: 执行驱动注册的
handler。典型操作:读取硬件状态寄存器、清除中断挂起位、记录时间戳。 - 底半部调度: 根据注册方式,顶半部返回后内核自动调度底半部(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驱动中断处理的黄金法则。