3. 中断处理流程:从硬件触发到CPU响应、中断上下文与进程上下文、上半部与下半部机制
3.1 中断处理全流程:从硬件信号到CPU响应
中断是硬件与软件交互的核心异步事件。当外设需要CPU关注时,通过中断控制器向CPU发送电信号,CPU在指令边界检查中断线,保存现场后跳转到中断服务程序(ISR)执行。整个流程可分为硬件阶段与软件阶段。
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;
}
IRQF_TIMER 或线程化中断。上半部中禁止调用 printk()(除非调试),因为其可能触发控制台锁。
3.4 下半部(Bottom Half)机制
下半部是中断处理中可延迟执行的部分。Linux 提供四种主要下半部机制,各有不同的调度延迟、可睡眠性和使用场景。
| 机制 | 调度延迟 | 可睡眠 | 并发模型 | 推荐场景 |
|---|---|---|---|---|
| 软中断 (SoftIRQ) | 极低(下一个 tick 前) | ❌ 不可睡眠 | 同一类型可在多核并行 | 网络收发包、块设备层 |
| Tasklet | 低(软中断之后) | ❌ 不可睡眠 | 同一 Tasklet 串行执行 | 简单设备驱动、GPIO 中断 |
| 工作队列 (Workqueue) | 中等(内核线程调度) | ✅ 可以睡眠 | 由 worker 线程执行 | 需要睡眠、访问文件系统、大量数据处理 |
| 线程化IRQ (Threaded IRQ) | 较高(内核线程调度) | ✅ 可以睡眠 | 每个 IRQ 独立内核线程 | 复杂设备、需要 RT 优先级控制 |
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);
3.7 中断上下文与进程上下文的切换代价
中断上下文与进程上下文之间的切换涉及 TLB 刷新、寄存器保存恢复、栈切换等操作。频繁切换会显著影响性能。下表总结了不同切换路径的典型开销:
| 切换路径 | 典型开销(cycles) | 主要代价来源 |
|---|---|---|
| 用户态 → 内核态(系统调用) | ~100-300 | syscall 指令、栈切换 |
| 进程上下文 → 中断上下文 | ~200-500 | 寄存器保存、中断栈切换 |
| 中断上下文 → 下半部(软中断) | ~50-150 | 软中断向量查找、状态恢复 |
| 中断上下文 → 工作队列线程 | ~1000-3000 | 内核线程调度、上下文切换 |
| 进程上下文切换(同核) | ~500-1500 | TLB 刷新、CR3 切换、cache 污染 |
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);
}
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 |