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 中断处理流程全景图

下图展示了从硬件中断触发到上下半部协同完成的完整流程:

Linux 中断处理流程:上半部与下半部协同 硬件中断触发 上半部(Top Half) 中断上下文 · 不可睡眠 · 快进快出 清除标志 · 拷贝数据 · 调度下半部 软中断 / Tasklet 软中断上下文 不可睡眠 · 串行执行 工作队列 进程上下文 可睡眠 · 内核线程 线程化中断 进程上下文 专用内核线程 中断处理完成 · 恢复被中断的进程 ⚠ 调试关键点 ftrace 可跟踪 irq_handler_entry/exit(上半部)和 softirq_entry/exit(下半部)

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 下半部调度时机

下半部并非在上半部返回后立即执行。内核在以下时机检查并执行待处理的下半部:

  1. 中断返回路径: 所有中断处理完成后,在返回用户空间或内核空间之前。
  2. 显式调用: 通过 local_bh_enable() 重新使能下半部时。
  3. 调度器调用: 在 schedule() 函数中。
  4. ksoftirqd 内核线程: 当软中断负载过高时,内核会唤醒 ksoftirqd 线程在进程上下文中处理。

理解这些调度时机对于使用 ftrace 分析中断延迟至关重要——下半部可能因为被其他中断或高优先级任务抢占而延迟执行。