在 Linux 系统中,CPU 并非始终只做一件事。它会随时暂停当前正在运行的程序,去处理一些更紧急或必须立即响应的事件,这些事件就是中断(interrupt)和异常(exception)。理解它们的运作方式,不仅能帮助你读懂 /proc/interrupts 等系统信息,还是分析系统性能、排查延迟问题的基础。
1. 中断与异常:两股“打断”CPU 的力量
中断通常由外部硬件产生,告诉 CPU “我有事需要处理”。比如:
- 键盘按下了一个键(键盘控制器发出中断);
- 网卡收到了一个数据包;
- 硬盘完成了一次 DMA 数据传输;
- 定时器到达一个节拍(时钟中断)。
中断是异步的,它们随时可能发生,与 CPU 当前执行的代码没有任何直接关系。
异常则是由当前正在执行的指令直接引发的同步事件,通常是因为指令执行出现了“特殊情况”。比如:
- 访问了无效内存地址(缺页异常、段错误);
- 试图执行一条特权指令,但 CPU 当前不在特权级下;
- 遇到了除零错误;
- 执行了
int 0x80或syscall指令(主动触发“系统调用”异常)。
异常是同步的,因为它是当前代码运行的结果,CPU 必须在这条指令执行的同时做出反应。
尽管来源不同,中断和异常在硬件层面使用了相似的机制:中断向量表和中断描述符表(IDT)。内核在初始化时会在这张表中为每一个中断向量注册相应的处理函数。当事件发生时,硬件根据向量号自动查表,跳转到对应的内核函数执行。
2. 中断处理流程:快慢分家
当一个硬件中断到达 CPU 后,一个精简的处理流程大致如下:
- 硬件保存现场:CPU 自动将当前被中断程序的某些关键寄存器(如指令指针)压入内核栈,并根据 IDT 找到对应的中断处理函数入口。
- 内核“接管”:中断处理程序(Interrupt Service Routine, ISR)开始运行。它首先要做的就是尽快搞清楚发生了什么,然后处理最紧急的部分,比如从网卡缓冲区读出数据包、清除中断标志。
- 关键原则:快进快出,少做重活
中断处理程序运行在中断上下文中,不能被其他同一个 CPU 上的中断抢占(通常还会屏蔽同级或所有中断),也不能做任何可能导致睡眠的操作(如等待信号量、分配可能阻塞的内存)。因此,真正耗时的、非紧急的工作必须推迟到中断处理程序之外去完成。这诞生了 Linux 中断子系统中经典的“上半部/下半部”模型:
- 上半部(top-half):硬件触发时立即执行,完成最紧急的应答,通常只用几十到几百微秒。
- 下半部(bottom-half):将剩余工作推迟到更安全的时机执行,比如稍后在开中断的环境中处理网络数据包的协议栈打包、唤醒等待数据的用户进程等。下半部可以通过 tasklet、工作队列(workqueue) 或 软中断(softirq) 等机制实现。
你可以在 /proc/interrupts 文件中看到每个 CPU 上各种中断发生的次数,并在中断风暴或 CPU 被大量中断淹没时据此定位问题。
3. 系统调用:软件主动触发的“异常”
应用程序不能直接操作硬件或访问内核资源,必须通过系统调用请求内核的服务,比如打开文件、发送网络数据、创建进程等。系统调用实际上就是一次软件主动触发的异常。在 x86-64 架构下,用户程序通常会执行 syscall 指令(32 位系统使用 int 0x80)。
一次完整的系统调用流程大致为:
- 参数准备:用户程序按照调用约定,将系统调用号和参数放入指定寄存器(例如
rax存放调用号,rdi、rsi、rdx等存放参数)。 - 触发异常:执行
syscall指令。CPU 硬件识别出这是一个系统调用异常,自动切换到内核栈,并将当前用户态的指令指针、栈指针等保存起来,然后根据特殊的 MSR(Model Specific Register)里配置的处理函数入口,跳转到entry_SYSCALL_64等汇编入口代码。 - 内核处理:入口代码构建完整的现场保存结构(将所有寄存器压栈,形成
pt_regs结构),然后调用 C 语言的系统调用处理函数。内核会根据rax中的系统调用号,在系统调用表(sys_call_table)中找到对应的内核函数(如sys_write),并执行它。 - 返回用户态:内核函数完成后,恢复保存的寄存器,执行
sysret指令(或iret),CPU 自动切换回用户态较低的权限级别,并跳转到用户程序被中断的下一条指令继续执行。
在整个过程中,一次系统调用会经历两次上下文切换(用户→内核、内核→用户),虽然现代 CPU 的 syscall/sysret 指令已经极度优化,但仍会消耗数百个 CPU 周期,这也是高性能服务(如 Redis)尽可能采用用户态批量操作而非频繁系统调用的原因。
4. 实用视角
- 观察中断活动:使用
cat /proc/interrupts查看各中断在各 CPU 上的分布;使用mpstat -P ALL 1可以观察与中断相关的%irq和%soft负载。 - 查看系统调用耗时:通过
strace -c可以统计一个程序各系统调用的次数和耗时,快速找到潜在的性能瓶颈。 - 调试系统调用失败:
perf trace会实时显示系统调用的事件流,类似于动态的strace,且开销更低。 - 内核模块编写:如果开发驱动,必须意识到不能在中断上下文中调用可能睡眠的函数,否则会导致内核 panic。
中断与异常是连接用户代码与硬件、用户空间与内核空间的两条关键通道。理解了它们的处理流程,你不仅知道为什么 top 中会出现 %hi(硬中断)和 %si(软中断),也能够在设计高性能系统或排查延迟焦虑时做出更准确的判断。