一、中断的本质与硬件基础
中断(Interrupt)是计算机系统中处理器响应外部或内部事件的机制,它打破了程序的顺序执行流,使CPU能够及时处理硬件设备的请求。在Linux内核中,中断处理是系统响应性和实时性的核心保障。
1.1 中断的分类
从硬件层面,中断可分为两大类:
- 外部中断(硬件中断):由外部硬件设备通过中断控制器(如8259A、APIC、GIC)向CPU引脚发送信号触发。例如键盘按键、网卡收到数据包、磁盘IO完成等。
- 内部中断(异常/软中断):由CPU内部执行指令时产生。包括故障(Page Fault)、陷阱(System Call)、_ABORT(硬件错误)等。
1.2 x86 APIC 中断控制器架构
现代x86系统使用高级可编程中断控制器(APIC),包含Local APIC和I/O APIC两个核心组件:
- Local APIC:集成在每个CPU核心中,接收来自I/O APIC的中断消息和处理器间中断(IPI),维护本地定时器和中断向量表。
- I/O APIC:通常集成在南桥芯片中,负责接收来自外部设备的中断请求(IRQ线),并根据重定向表将中断路由到指定CPU的Local APIC。
1.3 ARM GIC 中断控制器架构
ARM架构使用通用中断控制器(GIC),当前主流版本为GICv3/GICv4:
- SPI(Shared Peripheral Interrupt):共享外设中断,可被分组CPU访问,编号32到1019。
- PPI(Private Peripheral Interrupt):私有外设中断,编号为16到31,每个CPU核心有独立的PPI信号。
- LPI(Locality-specific Peripheral Interrupt):特定位置外设中断,基于消息的中断,GICv3引入,支持MSI/MSI-X。
- SGI(Software Generated Interrupt):软件生成中断,用于处理器间通信,编号0到15。
二、中断处理的上下文与基本流程
当硬件中断发生时,处理器执行以下原子操作序列:
2.1 硬件自动操作序列
- 保存当前执行上下文:将EFLAGS、CS、EIP(或ARM的PSTATE/ELR)压入当前特权级的栈中。
- 确定中断向量号:从中断控制器获取中断索引号(IRQ number)。
- 切换到内核态:从TSS(x86)或VBAR(ARM)加载中断栈指针,提升特权级别。
- 跳转到中断服务例程(ISR):根据向量号在IDT(中断描述符表)中索引并跳转到对应处理函数。
2.2 内核ISR入口(以x86为例)
内核使用汇编实现入口点common_interrupt,快速保存所有通用寄存器到pt_regs结构体,然后调用C语言处理函数:
// arch/x86/kernel/entry_64.S 示意
ENTRY(common_interrupt)
ASM_CLAC
addq $-0x80, %rsp // 对齐栈指针
CFI_ADJUST_CFA_OFFSET 0x80
movq %rdi, %rsp // 保存pt_regs指针
call do_IRQ // 调用C语言中断处理
jmp ret_from_intr // 返回处理路径
END(common_interrupt)
2.3 IRQ Descriptor 与处理链
内核使用struct irq_desc描述每个中断线的状态和关联操作:
// include/linux/irqdesc.h
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq; // 电平/边沿流处理
struct irqaction *action; // 设备驱动注册的action链表
unsigned int status_use_accessors;
unsigned int depth; // 嵌套深度
unsigned int wake_depth; // 唤醒嵌套计数
unsigned int irq_count; // 防中断风暴的计数器
unsigned long last_unhandled;
unsigned int irqs_unhandled;
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
const struct cpumask *percpu_affinity;
cpumask_var_t affinity_hint;
struct rcu_head rcu;
struct kobject kobj;
struct mutex request_mutex;
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;
关键点:*action指针指向一个irqaction链表,每个注册该IRQ的驱动都会挂载一个节点——这就是共享IRQ(IRQ sharing)的实现基础。
三、Top Half 与 Bottom Half:中断分割的艺术
在处理硬件中断时,系统处于中断上下文(interrupt context),这意味着:
- 中断处理执行时,本地CPU对应中断线被屏蔽(可能所有中断都被屏蔽)
- 不能睡眠或阻塞(没有进程上下文背供调度)
- 持有自旋锁期间执行时间过长会导致系统无响应
因此Linux将中断处理分为两个阶段:
- Top Half(上半部):立即执行的硬件响应部分,关中断运行,仅做最关键的确认和数据搬运(如读取设备状态寄存器、清除中断标志)。
- Bottom Half(下半部):延后执行的数据处理部分,开中断运行,可以执行较复杂的逻辑。
3.1 三大下半部机制对比
| 特性 | softirq | tasklet | workqueue |
|---|---|---|---|
| 执行上下文 | 软中断上下文(原子态) | softirq上下文(基于softirq) | 进程上下文(内核线程) |
| 是否可睡眠 | 否(不能睡眠) | 否(不能睡眠) | 是(可睡眠/阻塞) |
| 并发性 | 同类型可在不同CPU同时执行 | 同类型串行(不同类型可并发) | 全局工作队列可并发 |
| 注册方式 | 静态定义(open_softirq) | 动态初始化(tasklet_init) | 动态创建(INIT_WORK) |
| 适用场景 | 高频小任务(NET_RX, BLOCK) | 基于softirq的中断下半部 | 需要睡眠的复杂操作 |
| API复杂度 | 低(受限) | 中 | 高(功能丰富) |
四、softirq:底层软中断引擎
4.1 softirq的类型与优先级
softirq在内核中静态定义,枚举值代表优先级(数字越小优先级越高):
// include/linux/interrupt.h
enum {
HI_SOFTIRQ = 0, // 高优先级tasklet
TIMER_SOFTIRQ, // 定时器
NET_TX_SOFTIRQ, // 网络发送
NET_RX_SOFTIRQ, // 网络接收(最高负载)
BLOCK_SOFTIRQ, // 块设备
IRQ_POLL_SOFTIRQ, // 中断轮询
TASKLET_SOFTIRQ, // 普通tasklet
SCHED_SOFTIRQ, // 调度器
HRTIMER_SOFTIRQ, // 高精度定时器
RCU_SOFTIRQ, // RCU回调
NR_SOFTIRQS // 总数限制为10
};
4.2 softirq的执行时机
softirq并非立即执行,而是在以下时刻被检查和触发:
- 硬件中断处理返回前(
irq_exit中调用invoke_softirq) - 显式调用
local_bh_enable使能下半部时 - 软中断守护进程
ksoftirqd被唤醒(当软中断过载时)
4.3 编写自定义softirq
注册softirq需要编译时静态声明,不推荐驱动开发者直接使用——大多数情况下应使用tasklet或workqueue。但了解其实现有助于理解内核设计:
// 1. 在枚举中插入自定义softirq(需要修改内核头文件)
// 2. 注册处理函数
open_softirq(MY_CUSTOM_SOFTIRQ, my_softirq_handler);
// 3. 在驱动中触发
raise_softirq(MY_CUSTOM_SOFTIRQ);
五、tasklet:softirq之上的串行化封装
tasklet基于softirq(HI_SOFTIRQ和TASKLET_SOFTIRQ)构建,提供更简单的API和串行化保证。
5.1 tasklet核心结构
// kernel/softirq.c
struct tasklet_struct {
struct tasklet_struct *next;
unsigned long state; // TASKLET_STATE_SCHED / TASKLET_STATE_RUN
atomic_t count; // 引用计数(禁用计数器)
void (*func)(unsigned long); // 处理函数
unsigned long data; // 传入参数
};
// tasklet状态枚举
enum {
TASKLET_STATE_SCHED, // 已被调度,待执行
TASKLET_STATE_RUN // 正在某个CPU执行
};
5.2 tasklet API与使用示例
// 在驱动中声明并初始化tasklet
static void my_tasklet_handler(unsigned long data);
static DECLARE_TASKLET(my_tasklet, my_tasklet_handler, (unsigned long)dev);
// 或直接动态初始化
struct tasklet_struct *tl = kmalloc(sizeof(*tl), GFP_KERNEL);
tasklet_init(tl, my_handler, (unsigned long)dev);
// 在中断Top Half中调度tasklet
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
// Top Half: 读取设备FIFO,写ACK清除中断标志
u32 status = readl(dev->regs REG_STATUS);
writel(status

发表评论 取消回复