第9章:中断和异常(Interrupts and Exceptions)
一、导读
9.1 本章学习目标
第9章讲解Linux内核的中断和异常处理机制。中断和异常是操作系统响应异步事件和同步错误的核心机制——键盘按键、网卡收包、定时器到期、缺页异常、除零错误等,都通过中断和异常机制通知内核。理解中断处理的工作原理,是编写设备驱动程序、诊断系统问题、以及理解内核并发模型的基础。
本章的核心学习目标包括:
- 区分中断和异常:硬件中断、软件中断、异常的区别和联系
- 理解中断描述符表(IDT):中断向量如何映射到处理函数
- 掌握中断处理的完整流程:从硬件触发到处理函数执行再到返回
- 理解上半部和下半部的分离:为什么需要延迟处理,如何实现
- 掌握软中断、tasklet和工作队列:三种下半部机制的区别和适用场景
- 了解中断的并发控制:中断上下文中的锁和原子操作
9.2 为什么本章如此重要
中断是操作系统与硬件交互的主要方式。没有中断机制,CPU只能通过轮询(polling)检查设备状态,这将浪费大量CPU时间。中断允许设备在需要服务时主动通知CPU,使得CPU可以在等待I/O时执行其他任务。
异常处理则是系统安全和稳定性的保障。缺页异常实现了虚拟内存的按需分配,通用保护故障(GPF)阻止了非法的内存访问,系统调用通过异常(软中断)实现。理解异常处理机制,对于理解内存管理、进程保护和系统调用都至关重要。
对于设备驱动开发者而言,中断处理是最核心的技能之一。正确的中断处理需要平衡响应速度和处理时间,合理使用上半部/下半部分离、软中断和工作队列等机制。
9.3 核心问题引导
在阅读本章时,请带着以下问题思考:
当键盘按键时,从硬件到内核到用户程序,经历了哪些步骤?
中断处理和进程调度有什么关系?中断可以抢占当前运行的进程吗?
为什么中断处理函数要尽量短小?长时间的中断处理会导致什么问题?
软中断、tasklet和工作队列有什么区别?在什么场景下使用哪种?
为什么在中断处理函数中不能调用可能睡眠的函数?
多核系统中,中断如何在CPU之间分配?什么是中断亲和性?
二、核心概念详解
2.1 中断和异常的分类
Linux中的中断和异常可以分为以下几类:
中断和异常分类:
1. 中断(Interrupt)—— 异步事件
├── 硬件中断(Hardware Interrupt)
│ ├── 可屏蔽中断(Maskable Interrupt)
│ │ └── 通过 INTR 引脚,可被 IF 标志屏蔽
│ │ └── 由外部设备触发(键盘、网卡、定时器等)
│ └── 不可屏蔽中断(NMI)
│ └── 通过 NMI 引脚,不能被屏蔽
│ └── 用于硬件故障等紧急情况
│
└── 软件中断(Software Interrupt)
└── INT n 指令触发
└── 系统调用(INT 0x80 或 SYSCALL)
2. 异常(Exception)—— 同步事件
├── 故障(Fault)
│ └── 可恢复,指令重新执行
│ └── 缺页异常(Page Fault, #PF)
│ └── 除零异常(Division Error, #DE)
│
├── 陷阱(Trap)
│ └── 可恢复,执行下一条指令
│ └── 断点异常(Breakpoint, #BP)
│ └── 溢出异常(Overflow, #OF)
│
└── 终止(Abort)
└── 不可恢复,必须终止进程
└── 机器检查异常(Machine Check)/* x86 异常向量表(部分) */
/*
* 向量号 助记符 描述 类型
* 0 #DE 除零错误 故障
* 1 #DB 调试异常 故障/陷阱
* 2 --- NMI 中断
* 3 #BP 断点 陷阱
* 4 #OF 溢出 陷阱
* 5 #BR BOUND范围超出 故障
* 6 #UD 无效操作码 故障
* 7 #NM 设备不可用 故障
* 8 #DF 双重故障 终止
* 10 #TS 无效TSS 故障
* 11 #NP 段不存在 故障
* 12 #SS 栈段故障 故障
* 13 #GP 通用保护故障 故障
* 14 #PF 缺页异常 故障
* 16 #MF x87浮点异常 故障
* 17 #AC 对齐检查 故障
* 18 #MC 机器检查 终止
* 19 #XM SIMD浮点异常 故障
* 20 #VE 虚拟异常 故障
*/2.2 中断描述符表(IDT)
IDT(Interrupt Descriptor Table)存储了所有中断和异常处理函数的入口地址。CPU根据中断向量号索引IDT,找到对应的处理函数。
/* IDT 条目(门描述符) */
/* arch/x86/include/asm/desc.h */
struct gate_struct {
u16 offset_low; /* 入口地址低16位 */
u16 segment; /* 代码段选择符 */
unsigned ist : 3, /* 中断栈表索引 */
zero0 : 4,
zero1 : 1,
type : 5, /* 门类型 */
dpl : 2, /* 特权级 */
p : 1; /* 存在位 */
u16 offset_middle; /* 入口地址中16位 */
u32 offset_high; /* 入口地址高32位 */
u32 reserved;
} __attribute__((packed));
/* IDT 初始化(简化) */
/* arch/x86/kernel/idt.c */
void __init idt_setup_early_traps(void) {
/* 设置异常处理函数 */
set_intr_gate(X86_TRAP_DE, divide_error); /* 0: 除零 */
set_intr_gate(X86_TRAP_DB, debug); /* 1: 调试 */
set_intr_gate(X86_TRAP_NMI, nmi); /* 2: NMI */
set_intr_gate(X86_TRAP_BP, int3); /* 3: 断点 */
set_intr_gate(X86_TRAP_OF, overflow); /* 4: 溢出 */
set_intr_gate(X86_TRAP_UD, invalid_op); /* 6: 无效操作码 */
set_intr_gate(X86_TRAP_NM, device_not_available); /* 7: 设备不可用 */
set_intr_gate(X86_TRAP_DF, double_fault); /* 8: 双重故障 */
set_intr_gate(X86_TRAP_TS, invalid_TSS); /* 10: 无效TSS */
set_intr_gate(X86_TRAP_NP, segment_not_present); /* 11: 段不存在 */
set_intr_gate(X86_TRAP_SS, stack_segment); /* 12: 栈段故障 */
set_intr_gate(X86_TRAP_GP, general_protection); /* 13: 通用保护 */
set_intr_gate(X86_TRAP_PF, page_fault); /* 14: 缺页异常 */
set_intr_gate(X86_TRAP_MF, coprocessor_error); /* 16: 浮点异常 */
set_intr_gate(X86_TRAP_AC, alignment_check); /* 17: 对齐检查 */
set_intr_gate(X86_TRAP_MC, machine_check); /* 18: 机器检查 */
set_intr_gate(X86_TRAP_XM, simd_coprocessor_error);/* 19: SIMD异常 */
/* 加载 IDT */
load_idt(&idt_descr);
}
/* 设置硬件中断处理 */
void __init idt_setup_apic_and_irq_gates(void) {
/* 从向量 32 开始是硬件中断 */
for (int i = FIRST_EXTERNAL_VECTOR; i < NR_VECTORS; i++) {
set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]);
}
}2.3 中断处理的完整流程
当一个硬件中断到达时,CPU和内核协作完成以下处理流程:
硬件中断处理流程:
1. 设备发出中断信号
↓
2. 中断控制器(APIC)接收并路由到CPU
↓
3. CPU完成当前指令,检查中断
↓
4. CPU根据中断向量号查找IDT
↓
5. CPU保存当前上下文(EFLAGS, CS, RIP)到内核栈
↓
6. CPU跳转到中断处理函数
↓
7. 内核保存更多寄存器到栈上
↓
8. 调用设备的中断处理函数(上半部)
↓
9. 发送EOI(End of Interrupt)给中断控制器
↓
10. 恢复寄存器,执行 iret 指令返回
↓
11. CPU恢复之前的执行/* 中断处理函数的注册 */
/* include/linux/interrupt.h */
/* 中断处理函数类型 */
typedef irqreturn_t (*irq_handler_t)(int, void *);
/* 注册中断处理函数 */
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev_id);
/* 释放中断 */
void free_irq(unsigned int irq, void *dev_id);
/* 中断处理函数返回值 */
typedef enum {
IRQ_NONE = (0 << 0), /* 中断不是我的 */
IRQ_HANDLED = (1 << 0), /* 中断已处理 */
IRQ_WAKE_THREAD = (1 << 1), /* 唤醒中断线程 */
} irqreturn_t;
/* 中断标志 */
#define IRQF_SHARED (1 << 2) /* 允许共享中断线 */
#define IRQF_TIMER (1 << 5) /* 定时器中断 */
#define IRQF_PERCPU (1 << 6) /* 每CPU中断 */
/* 使用示例(设备驱动中) */
static irqreturn_t my_device_handler(int irq, void *dev_id) {
struct my_device *dev = (struct my_device *)dev_id;
/* 检查是否是我们的设备触发的中断 */
u32 status = readl(dev->base + STATUS_REG);
if (!(status & INTERRUPT_PENDING))
return IRQ_NONE; /* 不是我们的中断 */
/* 清除中断标志 */
writel(status, dev->base + STATUS_REG);
/* 上半部处理:只做最关键的操作 */
/* 例如:确认中断、保存硬件状态 */
/* 调度下半部处理(如果需要) */
schedule_work(&dev->work);
return IRQ_HANDLED;
}
static int my_device_probe(struct platform_device *pdev) {
struct my_device *dev;
int irq, ret;
irq = platform_get_irq(pdev, 0);
ret = request_irq(irq, my_device_handler,
IRQF_SHARED, "my_device", dev);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ %d\n", irq);
return ret;
}
return 0;
}2.4 上半部和下半部
中断处理分为上半部(Top Half)和下半部(Bottom Half):
上半部(Top Half):
- 在中断上下文中执行
- 关中断(或只关当前中断线)
- 必须快速完成
- 只做最关键的操作:确认中断、保存硬件状态
- 不能睡眠、不能做耗时操作
下半部(Bottom Half):
- 延迟执行
- 开中断(允许其他中断)
- 可以做更多工作
- 处理数据、调用协议栈、更新统计等
为什么要分离?
- 中断处理期间,CPU不能响应同优先级或低优先级的中断
- 如果中断处理时间太长,其他中断会被延迟
- 可能导致数据丢失(如网卡缓冲区溢出)
- 降低系统的响应性Linux提供了三种下半部机制:
/* 1. 软中断(Softirq) */
/* 静态分配,编译时确定 */
/* include/linux/interrupt.h */
enum {
HI_SOFTIRQ = 0, /* 高优先级软中断 */
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
};
/* 注册软中断处理函数 */
/* 只能在编译时注册,不能动态添加 */
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
/* 触发软中断 */
raise_softirq(NET_RX_SOFTIRQ);
/* 软中断的特点:
* - 可以在多个CPU上同时运行(同类型可以)
* - 不能睡眠
* - 执行时机:硬中断返回前、ksoftirqd线程
*/
/* 2. Tasklet */
/* 基于软中断实现,动态调度 */
struct tasklet_struct {
struct tasklet_struct *next;
unsigned long state; /* TASKLET_STATE_SCHED */
atomic_t count; /* 引用计数 */
void (*func)(unsigned long);
unsigned long data;
};
/* 定义 tasklet */
void my_tasklet_func(unsigned long data) {
struct my_device *dev = (struct my_device *)data;
/* 下半部处理... */
}
DECLARE_TASKLET(my_tasklet, my_tasklet_func, (unsigned long)dev);
/* 在中断处理函数中调度 tasklet */
static irqreturn_t my_handler(int irq, void *dev_id) {
/* 上半部处理... */
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
/* tasklet 的特点:
* - 同一 tasklet 不会在多个CPU上同时运行
* - 不能睡眠
* - 可以动态创建和调度
*/
/* 3. 工作队列(Workqueue) */
/* 在进程上下文执行,可以睡眠 */
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 处理函数 */
};
/* 定义工作 */
void my_work_func(struct work_struct *work) {
struct my_device *dev = container_of(work, struct my_device, work);
/* 下半部处理... */
/* 可以调用可能睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock 等) */
}
INIT_WORK(&dev->work, my_work_func);
/* 在中断处理函数中调度工作 */
static irqreturn_t my_handler(int irq, void *dev_id) {
/* 上半部处理... */
schedule_work(&dev->work);
return IRQ_HANDLED;
}
/* 工作队列的特点:
* - 在进程上下文执行
* - 可以睡眠
* - 可以重新调度
* - 有延迟,不如软中断及时
*/2.5 中断的并发控制
中断处理函数运行在中断上下文中,不能使用可能睡眠的操作(如mutex_lock、kmalloc(GFP_KERNEL))。内核提供了专门的机制来处理中断上下文中的并发问题。
/* 中断上下文中的并发控制 */
/* 1. 原子操作 */
atomic_t counter = ATOMIC_INIT(0);
atomic_inc(&counter); /* 原子递增 */
atomic_dec_and_test(&counter); /* 原子递减并测试 */
atomic_read(&counter); /* 原子读取 */
/* 2. 自旋锁 */
spinlock_t my_lock;
spin_lock_init(&my_lock);
/* 在中断处理函数中使用 */
spin_lock(&my_lock);
/* 临界区 */
spin_unlock(&my_lock);
/* 如果临界区也被进程上下文访问 */
unsigned long flags;
spin_lock_irqsave(&my_lock, flags); /* 关中断 + 加锁 */
/* 临界区 */
spin_unlock_irqrestore(&my_lock, flags); /* 解锁 + 恢复中断 */
/* 3. 每CPU变量 */
/* 如果数据只在单个CPU上使用 */
DEFINE_PER_CPU(int, my_counter);
/* 在中断处理函数中 */
int cpu = smp_processor_id();
per_cpu(my_counter, cpu)++;
/* 4. RCU(Read-Copy-Update) */
/* 适用于读多写少的场景 */
rcu_read_lock();
/* 读取共享数据 */
rcu_read_unlock();# 查看系统的中断分布
cat /proc/interrupts
# CPU0 CPU1 CPU2 CPU3
# 0: 23 0 0 0 IR-IO-APIC 2-edge timer
# 1: 3 0 0 0 IR-IO-APIC 1-edge i8042
# 8: 1 0 0 0 IR-IO-APIC 8-edge rtc0
# 9: 0 0 0 0 IR-IO-APIC 9-fasteoi acpi
# 12: 56 0 0 0 IR-IO-APIC 12-edge i8042
# 16: 1234 567 890 345 IR-IO-APIC 16-fasteoi ahci[0000:00:1f.2]
# 23: 56789 34567 23456 12345 IR-IO-APIC 23-fasteoi xhci_hcd
# NMI: 12 10 8 6 Non-maskable interrupts
# LOC: 123456 123456 123456 123456 Local timer interrupts
# 查看软中断统计
cat /proc/softirqs
# CPU0 CPU1 CPU2 CPU3
# HI: 0 0 0 0
# TIMER: 123456 123456 123456 123456
# NET_TX: 123 456 789 012
# NET_RX: 34567 23456 12345 6789
# BLOCK: 5678 4567 3456 2345
# IRQ_POLL: 0 0 0 0
# TASKLET: 1234 567 890 345
# SCHED: 56789 45678 34567 23456三、实战案例
3.1 编写简单的中断处理内核模块
/* irq_test.c - 简单的中断处理演示模块 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
MODULE_LICENSE("GPL");
static struct work_struct my_work;
/* 下半部处理函数(工作队列) */
static void bottom_half_handler(struct work_struct *work) {
printk(KERN_INFO "Bottom half: processing deferred work\n");
/* 这里可以做耗时的工作 */
/* 可以睡眠、可以调用可能阻塞的函数 */
}
/* 模拟中断处理(通过软件触发) */
static void simulate_interrupt(void) {
printk(KERN_INFO "Interrupt triggered!\n");
/* 上半部:快速处理 */
printk(KERN_INFO "Top half: quick response\n");
/* 调度下半部 */
schedule_work(&my_work);
}
static int __init irq_test_init(void) {
INIT_WORK(&my_work, bottom_half_handler);
printk(KERN_INFO "irq_test module loaded\n");
/* 模拟触发中断 */
simulate_interrupt();
return 0;
}
static void __exit irq_test_exit(void) {
flush_work(&my_work);
printk(KERN_INFO "irq_test module unloaded\n");
}
module_init(irq_test_init);
module_exit(irq_test_exit);3.2 分析中断分布
# 查看中断分布,判断是否有中断不平衡
cat /proc/interrupts
# 如果某个CPU的中断数量远多于其他CPU
# 说明中断分布不均衡,可能影响性能
# 设置中断亲和性(将中断绑定到特定CPU)
echo 1 > /proc/irq/16/smp_affinity # 绑定到 CPU 0
echo 2 > /proc/irq/16/smp_affinity # 绑定到 CPU 1
echo f > /proc/irq/16/smp_affinity # 绑定到所有 4 个 CPU
# 查看中断亲和性
cat /proc/irq/16/smp_affinity
# 0000000f
# 使用 irqbalance 守护进程自动平衡中断
systemctl status irqbalance3.3 观察软中断行为
# 持续监控软中断
watch -n 1 'cat /proc/softirqs | head -5'
# 在高网络负载下观察 NET_RX 软中断的增长
# 终端1:启动网络流量
iperf3 -c server -t 60
# 终端2:观察软中断
watch -n 1 'cat /proc/softirqs | grep -E "NET|TIMER"'
# 查看 ksoftirqd 线程的CPU使用率
top -H -p $(pgrep ksoftirqd)四、本章小结
本章深入讲解了Linux内核的中断和异常处理机制。中断是异步事件,由外部设备触发;异常是同步事件,由CPU在执行指令时检测到。IDT(中断描述符表)将中断向量号映射到处理函数,CPU根据向量号自动跳转到对应的处理入口。
中断处理分为上半部和下半部。上半部在中断上下文中快速执行,只做最关键的操作;下半部延迟执行,处理耗时的工作。软中断、tasklet和工作队列是三种下半部机制,各有适用场景:软中断性能最高但灵活性最低,工作队列可以在进程上下文执行但延迟较大。
中断处理是内核并发的重要来源。中断上下文不能使用可能睡眠的操作,需要使用原子操作、自旋锁等机制处理并发问题。理解中断处理机制,是编写设备驱动和诊断系统性能问题的基础。
五、思考与练习
思考题
如果中断处理函数执行时间过长,会导致什么问题?如何检测和解决?
为什么软中断可以在多个CPU上同时运行,而tasklet不能?
什么是"中断风暴"?如何防止设备产生过多的中断?
缺页异常处理函数为什么可能睡眠?这与普通中断处理有什么不同?
编程练习
编写一个内核模块,注册一个虚拟的中断处理函数(使用软件中断),实现上半部和下半部的分离。
编写一个程序,持续监控/proc/interrupts和/proc/softirqs,统计各中断和软中断的频率。
编写一个内核模块,使用工作队列处理"中断"的延迟工作,验证工作队列在进程上下文中执行。