中断处理函数地址(Interrupt Handler Address)

在 x86 架构中,中断发生时 CPU 需要跳转到对应的中断处理函数(Interrupt Service Routine, ISR)执行。那这个 ISR 的地址是如何定位的呢?我们可以从图中结构来一步一步说明这个查找过程。
首先我们从 中断描述符(Interrupt Descriptor) 说起。中断描述符是 IDT 表中的一个表项,它包含以下关键字段:
- 段选择子(segment selector):指明 ISR 所在代码段
- 偏移地址(offset):ISR 在该段内的偏移量
- 特权级(PL):该中断所需的权限等级
但仅有偏移地址还不够,因为偏移是相对于段基址base 而言的,我们还需要知道该段在内存中的起始位置,这就需要用到 段描述符(Segment Descriptor)。
段选择子会作为索引,查找 GDT(全局描述符表)或 LDT(局部描述符表)中对应的段描述符。段描述符中包含了该段的关键元数据,包括:
- 基址(base):代码段的起始地址
- 界限(limit):段的最大长度
- DPL(Descriptor Privilege Level):段的访问权限控制
我们通过 segment selector 找到对应的段描述符,从中提取出段的起始地址(base),再加上 offset 字段,最终就可以准确地定位到 ISR 的物理地址:
ISR Address = Segment Base + Offset \text{ISR Address} = \text{Segment Base} + \text{Offset} ISR Address=Segment Base+Offset
中断处理的堆栈结构(Stack of Interrupt Handler)
当中断或异常发生时,CPU 从当前正在执行的程序中"跳转"到对应的中断或异常处理函数。这种跳转,不像普通函数调用那样简单返回,还必须确保在处理完成后能够精确恢复中断发生前的上下文。而这一切的"还原数据",都被存储在中断处理专用的栈(stack)中。

栈中保存了什么?
从图中可以看到,在中断发生时,CPU 会将如下信息依次压入当前栈中:
- 旧的 EIP:也就是中断发生前指令的地址
- 旧的 CS:代码段寄存器的内容
- 旧的 EFLAGS:中断发生时的标志寄存器
- 错误码(error code):仅当特定异常发生时才会存在,例如页面错误或非法访问
- 如果发生了特权级切换(比如从用户态切到内核态),还会额外保存:
- 旧的栈段选择子(SS)
- 旧的栈顶指针(ESP)
两种情况对比:
🧩 无特权级转换(w/o privilege transition)
如果中断是在相同的权限级别(如内核态中断打断内核程序)发生的,则无需更换栈。此时 CPU 会在当前使用的栈上直接压入 EIP、CS、EFLAGS 和可能的错误码。原 ESP 与 SS 不变。
这类中断的处理和普通函数跳转更为接近,不涉及权限转换,因此开销较小。
🛡️ 有特权级转换(w/ privilege transition)
当中断由低权限代码(如用户态)触发内核态中断处理函数时,为了保证内核栈的安全性和独立性,CPU 会自动完成如下操作:
- 根据 TSS(任务状态段)中记录的栈地址切换栈指针(NEW SS ESP from TSS)
- 在新的内核栈中压入旧 SS 和 ESP,记录用户栈的上下文
- 然后再依次压入 EIP、CS、EFLAGS 和 error code
这种机制确保中断处理程序运行在可靠的栈上,避免用户程序伪造或破坏中断上下文。
栈结构的重要性
这个中断栈结构并不仅仅是恢复程序用的,它也是调试和异常诊断的重要依据。例如当页面错误发生时,error code 可以指明是哪种访问违规;EIP 可以定位到出错的指令,EFLAGS 还能揭示中断前的中断使能状态。
这也是为什么操作系统通常在进入中断处理函数后会先解析这些值,再决定是修复问题、终止程序还是重启系统。
中断请求的处理流程(Handling an Interrupt Request)
当外设或处理器本身触发了中断请求(IRQ)后,CPU 会执行一系列固定流程,最终将控制权交给内核态的中断处理函数。这个过程涉及权限判断、堆栈切换以及上下文保存等多个关键步骤。
🧭 处理流程如下:
-
检查当前特权级(Current Privilege Level, CPL)
x86 架构支持四个权限等级( 0 ∼ 3 0\sim3 0∼3),内核态为 0 0 0,用户态为 3 3 3。CPU 会首先判断中断触发时所在代码段的权限与目标中断处理函数是否一致。
-
如需特权级转换(例如用户态切换到内核态)
- 根据 TSS(任务状态段)获取目标权限等级的栈地址,更换堆栈
- 将旧的 SS栈段选择)和 ESP栈顶指针压入新栈中保存
- 将中断发生前的寄存器值依次保存到新栈:
- EFLAGS(标志寄存器)
- CS(代码段选择子)
- EIP(指令指针)
-
如果是异常中断(如页错误、非法访问)
某些异常中断还会自动压入一个错误码(error code),用于指示异常原因。
-
开始执行内核态的中断处理程序
此时,系统已完成权限切换和上下文保存,可以安全地进入内核空间处理硬件事件或异常。
从中断处理函数中返回(Returning from an Interrupt Handler)
完成中断处理后,系统需要恢复之前被打断的执行流程。这一过程也由硬件和特殊指令协助完成,确保恢复现场无误。
⏮️ 使用 IRET 指令返回(x86)
x86 架构提供了专门的 IRET
(Interrupt Return)指令,它类似于普通函数返回的 RET
,但处理更加复杂:
-
IRET
会从堆栈中弹出多个寄存器,包括:- EIP(指令指针)
- CS(代码段)
- EFLAGS(标志寄存器)
-
如果在中断进入时发生了特权级转换,它还会额外恢复:
- ESP(栈顶指针)
- SS(栈段)
-
同时,
IRET
会比RET
多处理 四字节的 EFLAGS 内容 ,这就是为什么IRET
比RET
多移动一次 ESP 的原因。
🎯 具体返回过程如下:
-
弹出错误码(若存在异常)
-
执行 IRET 指令
-
CPU 从栈中依次恢复寄存器状态:
- EIP ← 栈顶指针处的值
- CS ← 下一项
- EFLAGS ← 再下一项
-
如有权限恢复需求:
- ESP ← 用户态原始 ESP
- SS ← 用户态原始 SS
- 同时 CPL 恢复至原先状态,切换至用户态
这套机制保证了中断处理是对当前执行流无损的操作,中断退出后程序可以像从未被打断一样继续运行。
Linux 中的中断处理机制(Interrupt Handling in Linux)
在 Linux 内核中,中断处理被划分为三个阶段,分别是:关键阶段(critical) 、即时阶段(immediate) 和 延迟阶段(deferred) 。这种分层设计的目的,是为了在中断处理过程中兼顾响应速度与处理效率,尽可能减少对正常进程调度的干扰。
🔹 第一阶段:关键阶段(Critical Phase)
这一阶段的任务是尽快完成中断响应的最低需求,属于中断处理的最上半部分(top half):
- 内核会运行一个通用的中断处理入口函数(generic interrupt handler)
- 它会解析出当前中断号(IRQ number)、确定对应的中断控制器以及注册的中断处理函数
- 如果中断控制器要求立即回应(如发送中断应答信号),此时也会立即完成确认操作
在整个关键阶段中,本地 CPU 的中断会被关闭,以避免新的中断打断当前中断上下文的处理。
🔸 第二阶段:即时阶段(Immediate Phase)
接下来是实际执行与中断关联的设备驱动函数:
-
内核会遍历所有与当前中断号关联的设备驱动处理函数,并依次执行它们
-
如果当前中断是共享中断(shared interrupt) ,那么多个设备驱动可能绑定同一个中断号。这种情况下,驱动本身需要主动判断中断是否来自于它所管理的设备
通常会通过读取设备状态寄存器、标志位等方式来判断。例如网卡驱动收到中断后,会先查看网卡是否有数据到达。
-
所有处理函数执行完毕后,会通知中断控制器(如 APIC)调用
end_of_interrupt()
接口,允许其重新使能此中断线路
此阶段仍处于中断上下文,但直到末尾才重新开启本地 CPU 的中断
🔻 第三阶段:延迟阶段(Deferred Phase)
这部分通常被称为中断的下半部分(bottom half),与前面两个阶段相比,它的特点是:
- 延迟执行,不在最初的中断上下文中完成
- 本质上属于调度机制中一个"软中断"层,运行在软中断或任务队列(tasklet)上下文中
- 处理一些无需立即完成但又属于中断后续逻辑的任务,比如网络收包、缓冲区同步、调度唤醒等
此时中断已经完全重新开启,处理是在允许抢占的常规环境下完成,避免阻塞关键路径。
嵌套中断与异常处理(Nested Interrupts and Exceptions)

早期的 Linux 内核曾经支持中断嵌套,也就是在处理中断A的过程中,允许另一个中断B抢占执行。但后来这种机制被移除,原因是它可能引发一系列复杂的问题,比如内核栈溢出、状态难以管理、多层嵌套导致调试困难等。
因此,现代 Linux 采用一种更保守的策略:只允许有限制地嵌套,并且有明确的规则。
🔒 嵌套行为的限定规则如下:
-
异常(Exception)不能抢占中断处理
例如:页错误(page fault)或系统调用(syscall)等异常,不能在中断上下文中打断其处理过程。如果发生这种情况,将被视为内核中的严重 Bug。
-
中断可以打断异常处理
比如用户态系统调用进入内核后,还未处理完异常,就可以被一个设备中断抢占执行。这是合法的嵌套场景。
-
中断不能再抢占另一个中断 (过去曾支持)
当前内核版本中,为了简化栈管理与上下文切换,中断处理程序之间互不抢占。也就是说,中断处理是串行执行的,一个中断处理完毕才能响应下一个。
⛓️ 图示说明(如下图)
下方的流程图展示了一个典型的嵌套场景:

用户态(User Mode)
↓
系统调用或页错误等异常(Syscall / Exception)
↓
进入内核态(Kernel Mode)
↓
处理异常过程中发生 IRQi(中断 i)
↓
IRQi 处理中再触发 IRQj(不允许,现代内核中不会发生)
中断上下文与可延迟操作(Interrupt Context and Deferrable Actions)
中断上下文(Interrupt Context)
在 Linux 内核中,当 CPU 跳转至中断处理函数开始执行,到最终通过 IRET
指令返回主流程为止,这一段期间被称为中断上下文(interrupt context)。
运行在中断上下文中的代码有以下几个显著特征:
- 它是由中断请求IRQ触发的,而**不是异常(exception)**产生的控制流
- 该代码不属于任何用户进程上下文,因此没有合法的当前进程(current)可用,不能访问用户空间内存
- 禁止触发上下文切换 ,因此不能调用
sleep()
、schedule()
等可能导致调度器运行的操作,也不能执行阻塞行为
这就意味着,中断上下文中的函数应当执行得短小、精悍、快速返回,否则容易影响系统响应和实时性。
可延迟操作(Deferrable Actions)
可延迟操作是 Linux 内核中用于将部分工作延后处理的机制,通常用于中断处理函数中。中断处理函数负责快速响应硬件事件,而实际的"重活儿"可以通过 deferrable 机制放到稍后合适的时机再处理。
根据执行环境的不同,可延迟操作可以分为两类:
- 在中断上下文中执行的可延迟操作(如 softirq、tasklet)
- 在进程上下文中执行的可延迟操作(如 workqueue)
目的很明确:尽量减少中断处理函数中的工作负担,因为在中断上下文期间,CPU 是关闭中断的。如果处理时间太久,会阻塞其他中断响应,带来如网络丢包、设备数据堆积、系统卡顿等一系列问题。
比如:网卡中断响应后,只需迅速记录"有数据到达"这个事件,真正的数据接收、协议解析等动作可以放到 deferrable 操作中再慢慢处理。
可延迟操作的 API 支持:
内核提供了一套标准接口,用于设备驱动和内核模块使用这些机制:
- 初始化操作对象
- 激活或调度操作:表示延迟任务应当被执行
- 屏蔽与解除屏蔽:用于同步,避免与其他上下文同时操作共享数据
驱动程序通常会在设备初始化阶段设置好相关结构,在中断处理函数中调用"调度"接口,触发延迟处理。
Soft IRQ(软中断)
软中断(softirq) 是 Linux 中实现"延迟工作"机制的底层方式之一。它仍然运行在中断上下文中,但不是立即响应硬件中断,而是稍后执行特定任务的回调函数。
软中断的相关接口如下:
- 初始化:
open_softirq()
- 激活:
raise_softirq()
- 屏蔽 / 解屏蔽:
local_bh_disable()
和local_bh_enable()
激活后的 softirq 可以在以下时机运行:
- 紧随某个硬件中断处理函数执行之后(中断返回前)
- 或由内核线程
ksoftirqd
在合适时机调度执行
软中断机制设计灵活,但也有副作用。如果 softirq 持续重调度自身,或者中断频繁触发 softirq,会导致系统长时间无法执行普通进程,出现"进程饿死"的情况。为此,内核设置了两项限制:
MAX_SOFTIRQ_TIME
:限制单次 softirq 执行的最大时间MAX_SOFTIRQ_RESTART
:限制连续重调度的次数
一旦超过限制,内核会唤醒 ksoftirqd
线程,将剩余的 softirq 工作转移到该线程中,以进程上下文方式执行,释放中断上下文,保障系统调度的公平性。