1. 中断处理基础
1.1 中断的核心概念
中断(Interrupt)是硬件或软件向处理器发送的一种异步信号,用于通知处理器发生了需要立即关注的事件。中断机制是现代操作系统实现多任务、实时响应和外设管理的基石。
中断的本质:中断是一种硬件级别的“紧急呼叫”,它允许CPU暂停当前正在执行的程序流,转而执行一个专门的中断服务程序(ISR, Interrupt Service Routine),处理完后再恢复原来的执行流。
中断的主要分类如下:
| 分类 | 触发源 | 特点 | 典型示例 |
|---|---|---|---|
| 硬件中断 | 外部硬件设备 | 异步、随机发生、与CPU时钟无关 | 网卡收到数据包、键盘按键、磁盘I/O完成 |
| 软件中断 | CPU指令执行 | 同步、由程序主动触发 | 系统调用(int 0x80)、异常(除零错误) |
| 异常 | CPU内部错误或特殊条件 | 同步、通常与当前指令相关 | 缺页异常、保护错误、调试断点 |
在Linux内核中,中断处理的核心数据结构是irq_desc,每个中断号对应一个描述符,管理着该中断的触发方式、处理函数链表和状态信息。
1.2 中断上下文
中断上下文(Interrupt Context)是Linux内核中一个极为重要的概念。当CPU执行中断服务程序时,它运行在中断上下文中,与普通的进程上下文有本质区别。
| 对比维度 | 进程上下文 | 中断上下文 |
|---|---|---|
| 可睡眠 | 可以(通过调度器) | 绝对不可以 |
| 可被抢占 | 可以 | 通常不可以(取决于内核配置) |
| 持有锁 | 可以使用互斥锁(mutex) | 只能使用自旋锁(spinlock) |
| 访问用户空间 | 可以 | 绝对不可以 |
| current宏有效 | 有效,指向当前进程 | 无效,指向被中断的进程 |
| 调度时机 | 主动调用schedule()或时间片耗尽 | 中断返回时检查need_resched标志 |
⚠️ 关键约束:中断上下文中不能调用任何可能导致睡眠的函数,包括
kmalloc(GFP_KERNEL)、copy_from_user()、mutex_lock()等。违反此规则会导致内核死锁或系统崩溃。
内核通过in_interrupt()和in_irq()等宏来检测当前是否处于中断上下文:
/* 判断当前是否处于中断上下文 */
if (in_interrupt()) {
pr_info("当前运行在中断上下文中,不能睡眠!\n");
/* 只能使用自旋锁等原子操作 */
spin_lock(&my_lock);
/* ... 处理中断相关任务 ... */
spin_unlock(&my_lock);
} else {
pr_info("当前运行在进程上下文中,可以安全睡眠\n");
/* 可以使用互斥锁 */
mutex_lock(&my_mutex);
/* ... 处理常规任务 ... */
mutex_unlock(&my_mutex);
}
1.3 上半部与下半部机制
中断服务程序必须快速执行,因为它在执行期间会屏蔽同级别或更低级别的中断。然而,很多设备的中断处理工作非常复杂,例如网卡收到一个数据包后,需要将数据从硬件FIFO拷贝到内核缓冲区、解析协议头、将数据包交给协议栈、唤醒等待的进程等。如果所有这些工作都在ISR中完成,系统将长时间无法响应其他中断,导致严重的实时性问题。
为了解决这个问题,Linux内核将中断处理拆分为两个部分:
- 上半部(Top Half):即中断服务程序本身,执行时间敏感、必须原子完成的操作,如清除硬件中断标志、拷贝关键数据、记录中断状态。上半部执行期间,当前中断线被屏蔽。
- 下半部(Bottom Half):在中断使能的环境下执行剩余的非紧急处理工作,如协议栈处理、数据分发、唤醒进程。下半部可以被其他中断抢占。
1.4 Linux下半部机制的演进
Linux内核在发展过程中,下半部机制经历了多次演进,从最初的BH(Bottom Half)机制,到软中断(Softirq)、Tasklet,再到Workqueue,每种机制都有其适用的场景和性能特征。
| 下半部机制 | 引入版本 | 执行上下文 | 可睡眠 | 并发度 | 使用场景 |
|---|---|---|---|---|---|
| BH (旧) | Linux 1.x | 中断上下文 | 否 | 单CPU串行 | 已废弃 |
| Softirq | Linux 2.3 | 中断上下文 | 否 | 多CPU并行 | 网络、块设备等高性能路径 |
| Tasklet | Linux 2.3 | 中断上下文 | 否 | 同类型串行 | 驱动中通用下半部 |
| Workqueue | Linux 2.5 | 进程上下文 | 是 | 多CPU并行 | 需要睡眠的复杂任务 |
💡 选择指南:
- 如果下半部工作非常短且不需要睡眠 → 使用 Tasklet(基于Softirq的简化封装)
- 如果下半部工作涉及大量计算、I/O操作或需要睡眠 → 使用 Workqueue
- 如果对性能有极致要求且熟悉内核内部机制 → 直接使用 Softirq
1.5 中断处理的关键数据结构
在Linux内核中,中断处理涉及多个关键数据结构,理解它们之间的关系是掌握中断机制的基础:
/* 中断描述符结构体(简化版) */
struct irq_desc {
struct irq_data irq_data; /* 中断号、触发方式等硬件信息 */
struct irqaction *action; /* 中断处理函数链表 */
unsigned int status_use_accessors; /* 中断状态标志 */
struct irq_chip *chip; /* 中断控制器操作函数集 */
struct irq_domain *domain; /* 中断映射域 */
unsigned int depth; /* 中断使能深度计数 */
wait_queue_head_t wait_for_threads; /* 等待队列 */
};
/* 中断处理动作结构体 */
struct irqaction {
irq_handler_t handler; /* 上半部处理函数指针 */
unsigned long flags; /* 中断标志(触发方式等) */
const char *name; /* 设备名称 */
void *dev_id; /* 设备ID,用于共享中断识别 */
struct irqaction *next; /* 链表下一个节点(共享中断) */
int irq; /* 中断号 */
struct tasklet_struct tasklet; /* 关联的tasklet(可选) */
};
中断处理函数注册使用request_irq()接口,释放使用free_irq():
/* 注册中断处理函数 */
int request_irq(unsigned int irq, /* 中断号 */
irq_handler_t handler, /* 上半部处理函数 */
unsigned long flags, /* 中断标志 */
const char *name, /* 设备名称 */
void *dev_id); /* 设备ID */
/* 典型的中断处理函数原型 */
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
/* 上半部:快速处理,保存必要数据 */
struct my_device *dev = (struct my_device *)dev_id;
/* 读取硬件状态寄存器 */
u32 status = readl(dev->regs + STATUS_REG);
/* 清除中断标志 */
writel(status, dev->regs + CLEAR_REG);
/* 调度下半部处理(例如使用tasklet) */
tasklet_schedule(&dev->my_tasklet);
return IRQ_HANDLED;
}
📌 核心要点回顾:
- 中断是异步事件,中断上下文不能睡眠、不能访问用户空间、只能使用自旋锁。
- 上半部执行时间极短(微秒级),负责硬件交互和关键数据保存。
- 下半部执行时间可长可短,负责剩余处理工作,根据需求选择Tasklet或Workqueue。
- Linux提供了多种下半部机制,从Softirq到Workqueue,覆盖从高性能到高灵活性的不同需求。