2. 中断处理基础回顾:Linux中断上下文、上半部与下半部机制
在深入使用 ftrace 与 printk 调试驱动中断处理流程之前,必须牢固掌握 Linux 内核的中断处理模型。中断处理是驱动开发中最关键、也最容易出错的环节,其核心在于将处理工作拆分为 上半部(Top Half) 与 下半部(Bottom Half),并严格区分 中断上下文 与 进程上下文。
2.1 中断上下文 vs 进程上下文
Linux 内核根据执行环境将代码分为两大类上下文。中断上下文是驱动中断处理函数(ISR)运行的“舞台”,它与普通进程上下文有本质区别。
| 特性 | 中断上下文 | 进程上下文 |
|---|---|---|
| 触发方式 | 硬件中断信号(IRQ) | 系统调用、异常、内核线程 |
| 可睡眠 | 绝对不允许(调度器被冻结) | 允许(可调用 schedule()) |
| 可被抢占 | 通常不可抢占(除非中断嵌套) | 可被抢占(PREEMPT 内核) |
| current 指针 | 指向被中断的进程(不可信赖) | 指向当前进程(可信赖) |
| 持有锁 | 只能使用 spinlock(不可用 mutex) | 可使用 mutex、spinlock 等 |
| 典型函数 | request_irq() 注册的处理函数 | 系统调用处理、文件操作回调 |
调试陷阱: 在中断上下文中调用可能导致睡眠的函数(如
kmalloc(GFP_KERNEL)、copy_from_user()、mutex_lock())会触发内核 BUG,导致系统死锁或崩溃。使用 ftrace 跟踪时,若发现 schedule 函数在 ISR 中被调用,应立即排查。
2.2 上半部(Top Half)—— 快速响应
上半部即注册到 request_irq() 的中断处理函数。它的设计原则是:快进快出,只做最必要、最紧急的操作。
- 职责: 确认中断来源、清除硬件中断标志、拷贝硬件寄存器数据到软件缓冲区、唤醒下半部。
- 限制: 运行在中断上下文,不可睡眠,执行时间应控制在微秒级。
- 标志:
IRQF_DISABLED(旧内核)或默认不禁止其他中断(新内核支持中断嵌套)。
// 典型的上半部处理函数原型
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
// 1. 读取硬件状态寄存器,确认中断源
uint32_t status = ioread32(dev->regs + STATUS_REG);
// 2. 清除硬件中断标志(防止重复触发)
iowrite32(status, dev->regs + CLEAR_REG);
// 3. 将关键数据拷贝到软件缓冲区
dev->rx_data = ioread32(dev->regs + DATA_REG);
// 4. 调度下半部(例如 tasklet 或 workqueue)
tasklet_schedule(&dev->tasklet);
return IRQ_HANDLED;
}
2.3 下半部(Bottom Half)—— 延迟处理
下半部负责执行上半部中来不及处理的、可以稍后完成的工作。它运行在 软中断上下文 或 进程上下文(取决于机制),允许被中断,但依然不能睡眠(workqueue 除外)。
Linux 内核提供了多种下半部机制,各有优劣:
| 机制 | 运行上下文 | 可睡眠 | 并发性 | 典型使用场景 |
|---|---|---|---|---|
| 软中断(Softirq) | 软中断上下文 | 否 | 同类型可多 CPU 并行 | 网络收发包、块设备层(内核内部使用) |
| Tasklet | 软中断上下文 | 否 | 同类型串行(同一 CPU 上) | 驱动下半部首选(简单、安全) |
| 工作队列(Workqueue) | 进程上下文 | 是 | 由内核线程调度 | 需要睡眠的操作(如 I2C/SPI 传输) |
| 线程化中断(Threaded IRQ) | 进程上下文 | 是 | 专用内核线程 | 现代驱动推荐,简化设计 |
调试建议: 使用 ftrace 的
irq_handler_entry 和 irq_handler_exit 事件可以精确测量上半部的执行时间。若发现上半部耗时超过 100 微秒,应考虑将更多工作移至下半部。
2.4 中断处理流程全景图
下图展示了从硬件中断触发到上下半部协同完成的完整流程:
2.5 中断嵌套与并发注意事项
现代 Linux 内核(2.6.35+)默认允许中断嵌套,即高优先级中断可以打断低优先级中断的上半部。这给调试带来了复杂性:
- 中断优先级: 硬件中断控制器(如 GIC)决定优先级,内核软件无法改变。
- 重入风险: 同一中断处理函数可能在多核 CPU 上同时执行(如果中断线是共享的)。
- 锁策略: 上半部必须使用
spin_lock_irqsave()来保护共享数据,防止与下半部或进程上下文并发访问。
ftrace 实战提示: 使用
trace-cmd record -e irq -e softirq 可以捕获完整的中断嵌套场景。在 trace 日志中,如果看到 irq_handler_entry 事件在另一个 irq_handler_exit 之前出现,就说明发生了中断嵌套。
2.6 下半部调度时机
下半部并非在上半部返回后立即执行。内核在以下时机检查并执行待处理的下半部:
- 中断返回路径: 所有中断处理完成后,在返回用户空间或内核空间之前。
- 显式调用: 通过
local_bh_enable()重新使能下半部时。 - 调度器调用: 在
schedule()函数中。 - ksoftirqd 内核线程: 当软中断负载过高时,内核会唤醒
ksoftirqd线程在进程上下文中处理。
理解这些调度时机对于使用 ftrace 分析中断延迟至关重要——下半部可能因为被其他中断或高优先级任务抢占而延迟执行。