4. 中断处理函数编写:顶半部与底半部、快速中断与慢速中断、中断处理函数返回值
中断处理函数是驱动与硬件交互最频繁、对实时性要求最高的代码路径。Linux 内核将中断处理拆分为顶半部(Top Half)和底半部(Bottom Half),以平衡中断响应速度与处理复杂度。本章深入剖析中断处理函数的编写规范、快速/慢速中断的区分标准以及返回值语义,帮助开发者写出高效、稳定的中断服务例程(ISR)。
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 标志已被废弃。现代内核中所有中断处理函数默认在关中断环境下执行顶半部,不再区分快速/慢速。开发者应关注的是顶半部执行时长,而非注册标志。
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()确保卸载时底半部已停止。