3. 中断处理流程:从硬件触发到CPU响应、中断上下文与进程上下文、上半部与下半部机制

3.1 中断处理全流程:从硬件信号到CPU响应

中断是硬件与软件交互的核心异步事件。当外设需要CPU关注时,通过中断控制器向CPU发送电信号,CPU在指令边界检查中断线,保存现场后跳转到中断服务程序(ISR)执行。整个流程可分为硬件阶段与软件阶段。

关键概念: 中断延迟 = 硬件延迟 + 中断控制器仲裁延迟 + CPU响应延迟 + 上下文切换开销。在多核系统中,中断亲和性设置直接影响延迟分布。
中断处理全流程:硬件触发 → CPU响应 → 上下半部 外设触发中断 中断控制器仲裁 CPU中断线检测 保存CPU上下文 查找中断向量表 执行ISR(上半部) 上半部(Top Half):快速、原子、禁止本地中断 下半部(Bottom Half):延迟、可中断、可睡眠 软中断 (SoftIRQ) Tasklet 工作队列 (Workqueue) 线程化IRQ 说明: 中断上下文不能睡眠、不能调度、不能访问用户空间;进程上下文可以睡眠、可以调度、可以访问用户空间。

3.2 中断上下文 vs 进程上下文

Linux内核根据执行环境严格区分两种上下文。中断上下文(Interrupt Context)是CPU响应中断时进入的特权模式,进程上下文(Process Context)是普通内核线程或系统调用执行时的环境。两者在可调用函数、锁机制、调度能力上存在本质差异。

对比维度 中断上下文 进程上下文
进入方式 硬件中断触发,CPU自动保存部分寄存器 系统调用、异常、内核线程主动执行
是否可睡眠 ❌ 不可睡眠 ✅ 可以睡眠(调用 schedule())
是否可被抢占 默认不可抢占(local_irq_disable 时) 可被抢占(PREEMPT 内核)
持有锁类型 只能使用 spinlock(不可睡眠锁) 可使用 mutex、semaphore(可睡眠锁)
访问用户空间 ❌ 不可访问(无进程 mm 结构) ✅ 可以访问(copy_from/to_user)
current 宏有效性 无效(指向被中断进程,但不可信赖) 有效(指向当前进程 task_struct)
典型执行时间 微秒级(必须极短) 毫秒级或更长
⚠️ 常见错误: 在中断上下文中调用 msleep()、mutex_lock() 或 copy_from_user() 会导致内核崩溃或死锁。驱动开发者必须通过 in_interrupt() 或 in_irq() 宏检查当前上下文。

3.3 上半部(Top Half)机制

上半部是中断处理程序本身,由 request_irq() 注册。其核心约束是:快进快出。上半部在关中断或屏蔽当前中断线的环境下执行,必须完成最紧急的硬件操作(如清除中断标志、读取硬件状态寄存器),然后将耗时工作推迟到下半部。

// 上半部典型结构示例
static irqreturn_t my_interrupt_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 + ACK_REG);

    // 3. 将数据保存到设备结构体(供下半部使用)
    dev->last_status = status;

    // 4. 调度下半部执行
    tasklet_schedule(&dev->tasklet);
    // 或:irq_work_queue(&dev->work);  // 线程化IRQ

    return IRQ_HANDLED;
}
性能要点: 上半部执行时间应控制在 10~50 微秒以内。若超过 100 微秒,应考虑使用 IRQF_TIMER 或线程化中断。上半部中禁止调用 printk()(除非调试),因为其可能触发控制台锁。

3.4 下半部(Bottom Half)机制

下半部是中断处理中可延迟执行的部分。Linux 提供四种主要下半部机制,各有不同的调度延迟、可睡眠性和使用场景。

机制 调度延迟 可睡眠 并发模型 推荐场景
软中断 (SoftIRQ) 极低(下一个 tick 前) ❌ 不可睡眠 同一类型可在多核并行 网络收发包、块设备层
Tasklet 低(软中断之后) ❌ 不可睡眠 同一 Tasklet 串行执行 简单设备驱动、GPIO 中断
工作队列 (Workqueue) 中等(内核线程调度) ✅ 可以睡眠 由 worker 线程执行 需要睡眠、访问文件系统、大量数据处理
线程化IRQ (Threaded IRQ) 较高(内核线程调度) ✅ 可以睡眠 每个 IRQ 独立内核线程 复杂设备、需要 RT 优先级控制
下半部机制选择决策树 下半部需要睡眠? 是 → 需要睡眠 工作队列 (Workqueue) 或 线程化IRQ 否 → 不可睡眠 需要多核并行? 是 → 软中断 否 → Tasklet 选择建议: • 网络/块设备 → 软中断(性能最高) • 简单驱动 → Tasklet(开发简单) • 复杂逻辑/需要睡眠 → 工作队列

3.5 软中断与 Tasklet 实现细节

软中断是 Linux 下半部最底层的机制,静态分配(编译时定义),目前内核使用 HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、BLOCK_SOFTIRQ、IRQ_POLL_SOFTIRQ、TASKLET_SOFTIRQ、SCHED_SOFTIRQ、HRTIMER_SOFTIRQ、RCU_SOFTIRQ 共 10 种。Tasklet 基于软中断实现,但提供更简单的接口。

// 软中断注册示例(内核内部使用,驱动一般不直接注册)
// 在 kernel/softirq.c 中定义
enum
{
    HI_SOFTIRQ = 0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
    NR_SOFTIRQS
};

// Tasklet 使用示例(驱动常用)
// 1. 定义并初始化 tasklet
struct tasklet_struct my_tasklet;
tasklet_init(&my_tasklet, my_tasklet_handler, (unsigned long)dev);

// 2. 实现 tasklet 处理函数(下半部)
static void my_tasklet_handler(unsigned long data)
{
    struct my_device *dev = (struct my_device *)data;

    // 这里可以执行较复杂的处理,但仍不可睡眠
    // 例如:将数据拷贝到环形缓冲区、唤醒等待队列
    dev->rx_count++;
    wake_up_interruptible(&dev->wq);
}

// 3. 在上半部中调度
tasklet_schedule(&my_tasklet);

3.6 工作队列与线程化IRQ

工作队列将下半部任务交给内核 worker 线程执行,因此可以睡眠、可以持有 mutex、可以执行文件系统操作。线程化IRQ(request_threaded_irq())则直接为每个中断创建一个内核线程,适合需要实时优先级管理的场景。

// 工作队列示例
static struct work_struct my_work;

// 工作队列处理函数(可以睡眠)
static void my_work_handler(struct work_struct *work)
{
    struct my_device *dev = container_of(work, struct my_device, work);

    // 可以安全地调用 msleep、mutex_lock、copy_from_user 等
    mutex_lock(&dev->lock);
    dev->data_ready = true;
    mutex_unlock(&dev->lock);

    // 通知用户空间
    kill_fasync(&dev->fasync, SIGIO, POLL_IN);
}

// 在上半部中调度
schedule_work(&my_work);

// 线程化IRQ注册示例
// 上半部只做最小硬件操作,下半部由内核线程执行
request_threaded_irq(irq, my_hardirq_handler, my_thread_fn,
                     IRQF_TRIGGER_RISING, "my_device", dev);

// 如果上半部为 NULL,内核自动使用 irq_default_primary_handler
// 适合完全不需要上半部的场景
request_threaded_irq(irq, NULL, my_thread_fn,
                     IRQF_TRIGGER_RISING | IRQF_ONESHOT, "my_device", dev);
💡 性能优化提示: 对于高吞吐设备(如万兆网卡),推荐使用 软中断 + NAPI 组合,避免每个包都触发完整中断流程。对于低延迟要求的中断,使用 线程化IRQ + SCHED_FIFO 实时优先级 可保证确定性响应。

3.7 中断上下文与进程上下文的切换代价

中断上下文与进程上下文之间的切换涉及 TLB 刷新、寄存器保存恢复、栈切换等操作。频繁切换会显著影响性能。下表总结了不同切换路径的典型开销:

切换路径 典型开销(cycles) 主要代价来源
用户态 → 内核态(系统调用) ~100-300 syscall 指令、栈切换
进程上下文 → 中断上下文 ~200-500 寄存器保存、中断栈切换
中断上下文 → 下半部(软中断) ~50-150 软中断向量查找、状态恢复
中断上下文 → 工作队列线程 ~1000-3000 内核线程调度、上下文切换
进程上下文切换(同核) ~500-1500 TLB 刷新、CR3 切换、cache 污染
优化策略: 在中断密集场景(如 10Gbps 网络),应尽量将工作保留在软中断上下文中完成,避免触发工作队列调度。使用 local_bh_disable()/local_bh_enable() 控制下半部执行时机,减少不必要的上下文切换。

3.8 中断处理中的锁与并发

中断上下文中的并发问题比进程上下文更复杂。同一中断线可能在不同 CPU 上同时触发(如果设置了 IRQF_PERCPU),下半部可能与上半部并发执行。以下是中断处理中常用的锁机制:

  • spinlock_irqsave() / spinlock_irqrestore():在中断上下文中保护共享数据,同时禁用本地中断防止死锁。
  • raw_spinlock_t:用于实时内核(PREEMPT_RT)中,避免 spinlock 被转换为可睡眠锁。
  • atomic_t / atomic_long_t:用于简单的计数器或标志位,避免锁开销。
  • local_t:用于单 CPU 统计计数,无需锁。
// 中断上下文中的锁使用规范
static irqreturn_t my_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    unsigned long flags;

    // 必须使用 irqsave 变体,保存当前中断状态
    spin_lock_irqsave(&dev->lock, flags);

    // 操作共享数据
    dev->rx_packets++;
    dev->rx_bytes += ioread32(dev->regs + RX_LEN_REG);

    spin_unlock_irqrestore(&dev->lock, flags);

    // 调度下半部
    tasklet_schedule(&dev->tasklet);

    return IRQ_HANDLED;
}

// 下半部中同样需要锁保护
static void my_tasklet_fn(unsigned long data)
{
    struct my_device *dev = (struct my_device *)data;
    unsigned long flags;

    // Tasklet 中可以使用 spinlock_bh 变体
    spin_lock_bh(&dev->lock);
    // ... 处理数据 ...
    spin_unlock_bh(&dev->lock);
}
⚠️ 死锁风险: 如果上半部持有 spinlock,而同一个 CPU 上的下半部也尝试获取同一个 spinlock,将导致自旋死锁。解决方案:上半部使用 spin_lock_irqsave(禁用本地中断),下半部使用 spin_lock_bh(禁用下半部抢占)。

3.9 中断处理流程总结

理解中断处理流程是驱动性能优化的基础。从硬件触发到 CPU 响应,再到上下半部分离,每个环节都影响系统的实时性和吞吐量。在多核系统中,中断亲和性设置(/proc/irq/<IRQ>/smp_affinity)决定了中断在哪个 CPU 上处理,直接影响 cache 局部性和负载均衡。

阶段 执行环境 可调用函数 典型耗时 优化目标
硬件触发 硬件逻辑 N/A ~ns 减少总线延迟
中断控制器仲裁 硬件逻辑 N/A ~10-100ns 合理配置优先级
CPU 响应(上半部) 中断上下文 spinlock、ioread/write、tasklet_schedule ~1-50μs 最小化执行时间
下半部调度 软中断 / 内核线程 取决于机制 ~10-500μs 选择合适机制
下半部执行 软中断 / 进程上下文 完整内核 API ~100μs-10ms 避免长时间占用 CPU