可编程中断控制器(Programmable Interrupt Controller,PIC)
支持中断(interrupt)的设备通常会有一个专门用于发出中断请求Interrupt ReQuest,IRQ的输出引脚(IRQ pin)。这些IRQ引脚连接到一个称为可编程中断控制器Programmable Interrupt Controller,简称PIC的设备上,而PIC再连接到CPU的INTR
引脚上,用于向CPU发送中断请求信号。
一般来说,PIC拥有一组端口(ports),用来与CPU交换信息。当连接到PIC某条IRQ线上的设备需要CPU关注时,过程如下:
-
设备通过对应的IRQn引脚发起中断请求(IRQ)。
解释:IRQn 表示设备所使用的第 n n n 条中断线(如IRQ0、IRQ1等)。
-
PIC将该IRQ请求转换为一个向量号(vector number),并将这个向量号写入特定的端口,供CPU读取。
解释:向量号是一个数字,代表具体的中断类型或来源。CPU通过读取这个向量号确定具体的中断处理程序地址。
-
PIC通过CPU的
INTR
引脚向CPU发出中断信号。 -
在收到CPU对当前中断请求的确认(acknowledge,简称ACK)之前,PIC不会发出另一个中断请求。
-
CPU确认(ACK)中断后,即开始处理中断请求。
具体CPU如何处理中断请求,稍后我们会详细介绍。需要注意的是,按照设计,PIC必须等待CPU确认当前中断后,才可以继续发出新的中断请求。
注意(Note)
一旦CPU确认(ACK)了某个中断请求后,PIC就可以再次发出新的中断请求。这种行为与CPU是否完成了前一个中断的处理无关。因此,根据操作系统(OS)对CPU的控制策略不同,有可能会出现嵌套中断nested interrupts的情况。
解释:嵌套中断指的是CPU在处理中断时又收到新的中断请求,此时CPU可能暂停当前中断处理,转而处理更高优先级的中断。
PIC允许单独地禁止(disable)每条IRQ线。这种设计可以简化系统实现,确保中断处理程序(interrupt handlers)始终是串行执行的(即每次只处理一个中断)。
对称多处理系统中的中断控制器(Interrupt Controllers in SMP Systems)
在对称多处理(Symmetric Multi-Processing,简称SMP)系统中,通常不仅存在多个CPU核心,还可能配置了多个中断控制器。这是因为不同类型的中断需要被合理地分发与管理,以保证系统的响应效率和可扩展性。
以 x86 架构 为例,每个处理器核心都配有一个本地高级可编程中断控制器(Local APIC)。这个 Local APIC 主要负责处理来自该核心本地的中断信号,比如:
- 定时器中断(Timer Interrupt):核心自身定期触发的时钟信号,用于操作系统的时间片轮转调度。
- 热感应中断(Thermal Sensor Interrupt):当核心温度过高时触发,用于过热保护或调频处理。
这些中断是与单个核心紧密绑定的,Local APIC 提供了低延迟和高效率的中断响应机制。
除此之外,系统中还会配置一个或多个I/O APIC(Input/Output Advanced Programmable Interrupt Controller)。I/O APIC 用于接收来自外部设备的中断请求,例如来自:
- 网络接口卡
- USB 外设
- 磁盘控制器
I/O APIC 的作用是将这些设备发出的中断信号动态地分发到各个 CPU 核心,避免所有中断都集中到一个处理器上,从而提高系统整体的负载均衡性和响应能力。
扩展说明:
在早期系统中,所有中断请求都发送到主核心(通常是CPU0),这会导致瓶颈。而 I/O APIC 的引入,使得系统可以根据配置或操作系统策略,将中断轮流分发(轮询方式)、或根据设备负载情况定向分发到不同核心,大大提升了中断处理的并发性。
此外,现代操作系统(如Linux)也支持中断亲和性Interrupt Affinity的配置,即可以指定某个中断只能在某几个特定的核心上处理,这在 NUMA 架构或性能调优时尤为重要。
中断控制(Interrupt Control)
在操作系统中,为了安全地访问共享数据,我们常常需要协调中断处理函数interrupt handler与其他可能并发执行的操作(例如驱动初始化或驱动数据处理)之间的交互。因为中断可能在任何时刻打断当前任务并执行中断服务例程,如果不加控制就访问共享资源,极有可能导致数据竞争或系统异常。
因此,一个关键措施是:在关键区域临时关闭中断,待操作完成后再重新开启。这样的中断控制机制有助于保证关键操作的原子性(atomicity)。
中断的使能和屏蔽(即开启与关闭),可以在以下几个层面上进行:
1. 设备层(Device Level)
设备本身通常具有控制寄存器(control registers),其中包含用于开启或屏蔽中断的位(bit)。例如:
- 某些网卡设备可以通过设置寄存器关闭“接收完成中断”,等系统准备好再重新开启。
- 嵌入式系统中的定时器,也允许通过寄存器来关闭或开启定时中断。
解释:这种方式最贴近硬件,控制粒度最细,但通常由设备驱动程序负责,程序员较少手动操作。
2. 中断控制器层(PIC Level)
可编程中断控制器(PIC)允许我们通过编程方式单独屏蔽某一条IRQ线路,从而禁止特定设备发起中断请求。
- 例如,在传统的8259A PIC中,可以设置中断屏蔽寄存器(IMR)来关闭某条IRQ线。
- 在APIC架构中,也可以通过修改重定向表项的“屏蔽位”来实现相同效果。
这种方式通常用于控制多个外设之间的中断优先级,或者在某些场景临时屏蔽不重要的中断。
3. CPU层(CPU Level)
在CPU指令级别,也可以直接通过指令控制中断总开关。在x86架构中有两个经典的汇编指令:
cli
:CLear Interrupt flag,清除中断标志,即关闭全局中断。sti
:SeT Interrupt flag,设置中断标志,即重新打开中断。
解释:
CPU中有一个标志位 I F IF IF(Interrupt Flag),决定是否允许处理可屏蔽中断(maskable interrupt)。执行
cli
会将 I F = 0 IF=0 IF=0,即CPU不再响应中断;而sti
则将 I F = 1 IF=1 IF=1,允许中断再次触发。
这种方式通常用于内核或驱动程序中实现短时间的临界区保护,例如在访问共享变量、链表或队列时临时关闭中断。
中断优先级(Interrupt Priorities)
在现代计算机架构中,大多数系统都支持中断优先级机制(Interrupt Priority)。这个机制的核心思想是:并非所有中断都是平等的。某些中断比其他中断更紧急、更关键,因此当系统正在处理中断时,仍然允许优先级更高的中断打断当前的处理流程。
启用中断优先级后,系统会为每条IRQ中断线分配一个优先级(通常是一个数值,数值越小表示优先级越高)。这样一来,中断嵌套Nested Interrupt的行为就可以根据优先级做出有条件的允许:
- 当系统正在处理一个中断时,只允许优先级更高的中断插入打断当前流程。
- 优先级相同或更低的中断将被标记为 pending(待处理),等当前中断处理完后再继续处理它们。
🔧 示例解析
嵌套中断处理的实际例子:
- 系统最初运行在 进程上下文(Process Context),此时没有中断发生。
- 突然,IRQ10 中断到来,系统立即切换去执行
irq10 handler
。 - 在
irq10 handler
执行期间,IRQ20 发生了,但其优先级较低,因此它被标记为 pending。 - 然而,这时又来了一个更高优先级的 IRQ5,它立即打断当前的
irq10 handler
,开始执行irq5 handler
。 - 当
irq5 handler
完成后,系统会恢复执行irq10 handler
。 - 等到
irq10 handler
也结束后,才会回过头来处理刚才被挂起的 IRQ20,也就是irq20 handler
。
🔍 延伸理解:
中断优先级机制带来的优势有:
- 确保响应关键事件:像系统时钟、温度过热报警、电源掉电等关键中断可以设为高优先级,确保系统快速响应。
- 避免资源饥饿:高频率但优先级较低的中断不会长时间阻塞更关键的任务。
- 支持实时性要求:在嵌入式系统或实时系统中,中断优先级是调度策略的核心基础。
注意事项:
设计中断优先级时要非常小心,避免优先级反转(低优先级任务持有资源,阻塞高优先级中断)以及嵌套过深导致栈溢出stack overflow的问题。
x86 架构下的中断处理机制(Interrupt Handling on the x86 Architecture)
中断作为操作系统与硬件之间沟通的重要机制,其实现过程在底层架构中体现得尤为精细。x86 处理器使用一种称为 中断描述符表(Interrupt Descriptor Table,IDT) 的结构来管理中断处理流程。
🧩 中断描述符表(Interrupt Descriptor Table, IDT)
IDT 的核心作用是将每一个可能的中断或异常(exception)向量编号(vector number),与对应的处理程序(也称为中断/异常处理函数)建立起映射关系。可以把 IDT 理解为一个跳转表(jump table):当某个中断向量被触发时,CPU 会自动跳转到 IDT 中对应的处理程序入口。
🧱 IDT 的基本特性如下:
-
跳转表机制
当一个中断或异常被触发时,CPU 会根据该事件的向量编号(vector number),查找 IDT 中对应的入口,从而跳转到中断或异常处理函数的入口地址执行。
解释:向量编号是一个 0 ∼ 255 0\sim255 0∼255 的无符号整数,代表不同的中断或异常。例如:
- 0 0 0:除0错误(Divide-by-zero)
- 13 13 13:通用保护错误(General Protection Fault)
- 32 ∼ 255 32\sim255 32∼255:通常为用户定义的硬件中断
-
结构大小
IDT 是一个包含 256 256 256 个入口的数组,每个入口占用 8 8 8 个字节,因此整张表大小为 256 × 8 = 2048 256 × 8 = 2048 256×8=2048 字节(2KB)。
-
存储位置灵活
IDT 可以被加载到任意物理内存地址,并不固定在某个区域。
-
通过 IDTR 寄存器定位
处理器通过一个特殊的寄存器 IDTR(Interrupt Descriptor Table Register) 来定位 IDT 的起始地址。该寄存器由操作系统在系统初始化时设置,其内容包含:
- 基地址:IDT 所在内存的物理地址
- 界限值(limit):IDT 的长度(通常是 2047 2047 2047)
CPU 在处理中断时,始终以 IDTR 提供的信息为依据,查找 IDT 并确定跳转位置。
✨ 延伸理解
IDT 的每个表项(Entry)包含了中断处理函数的地址信息、段选择子(Segment Selector)、权限等级(Privilege Level)等内容。这使得中断机制可以兼顾灵活性与安全性:
- 不同的中断可以指向不同的处理函数
- 可以控制哪些中断允许从用户态进入,哪些只能在内核态触发
- 通过 IDT 的重新配置,系统可以热插拔或动态加载新的设备驱动
Linux 中断向量布局详解(IRQ Vector Layout in Linux)
在 Linux 操作系统中,针对 x86 架构的中断机制有一套清晰的向量编号体系。所谓中断向量(Interrupt Vector),本质上是一个 0 ∼ 255 0\sim255 0∼255 的编号表,每一个编号都对应着一个中断/异常类型,在触发事件后引导 CPU 跳转至对应的中断处理程序。
下面是 Linux 在 arch/x86/include/asm/irq_vectors.h
中定义的中断向量表布局,如图所示:
📐 向量编号划分结构如下:
🧱 0 ∼ 31 0 \sim 31 0∼31:系统陷阱和异常(System Traps and Exceptions)
这一部分是由处理器保留的向量范围,用于表示 CPU 运行过程中出现的异常情况,例如:
- 0 0 0:除以0异常(Divide-by-zero)
- 6 6 6:非法指令(Invalid Opcode)
- 13 13 13:通用保护错误(General Protection Fault)
- 14 14 14:缺页异常(Page Fault)
这些异常通常意味着程序执行过程中出现了严重问题,系统必须立即中断当前任务并做出响应。
⚙️ 32 ∼ 127 32 \sim 127 32∼127:设备中断(Device Interrupts)
这是给外设保留的向量区间,也就是平时我们说的硬件中断。例如:
- 键盘输入
- 网络数据包接收
- 磁盘 I/O 完成通知
这些中断通常来源于外部硬件,由 I/O APIC 或 中断控制器(PIC) 分发给对应的 CPU 核心。
注意:由于前 0 ∼ 31 0\sim31 0∼31 已被保留,因此设备中断向量需要从 32 32 32 起跳,这也是为什么许多中断号从 IRQ32 开始的原因。
🔧 向量 128 128 128:系统调用接口(int80 syscall interface)
向量号 128 128 128 是历史上在 x86 架构下被用于系统调用System Call的专用中断号:
- 用户态程序使用
int 0x80
指令陷入内核,从而调用系统提供的服务,如read
,write
,open
等。 - 虽然现代 Linux 已逐步改用更高效的
sysenter
或syscall
指令,但int 0x80
仍被保留以兼容旧程序。
🧩 129 ∼ 255 129 \sim 255 129∼255:其他中断(Other Interrupts)
这部分空间通常由系统内部、虚拟化框架或定制机制使用。例如:
- 高级电源管理(如 APIC Timer)
- 虚拟机中 Hypervisor 与 Guest OS 通信的中断
- 特殊调试或性能监控机制
Linux 内核可能会在这一区间分配特定功能的中断向量,也允许驱动程序根据需求分配动态向量。
x86 架构中 IDT 表项结构详解(Interrupt Descriptor Table Entry in x86)
在 x86 架构中,中断描述符表IDT的每一个表项被称为一个“门(Gate)”,每个门占用 8 8 8 字节的空间,存储了当中断或异常发生时,CPU 应该跳转到哪个处理函数的详细信息。
🌀 三种类型的中断门(Gate Types)
-
中断门(Interrupt Gate)
- 存储中断或异常处理函数的地址。
- 跳转到该处理函数时,CPU 会自动关闭可屏蔽中断(maskable interrupts),也就是说清除中断标志位 I F = 0 IF=0 IF=0,防止处理中断过程中再次被打断。
- 是 Linux 中最常见的门类型。
-
陷阱门(Trap Gate)
- 与中断门非常类似,但不会关闭中断标志位,允许嵌套中断发生。
- 适用于一些非关键路径的异常,比如调试断点或系统调用。
-
任务门(Task Gate)
- 跳转目标不是一个普通的函数地址,而是一个任务状态段(TSS),用于进行任务切换。
- Linux 中并不使用此机制,因为现代操作系统使用了不同的调度方式来管理多任务。
🧱 一个 IDT 表项的字段解析(如图所示)
图中的结构展示了一个完整的 IDT 表项的位字段分布,从低位到高位如下:
🔸 offset (0…15) 与 offset (16…31)
- 这两个字段组合在一起,构成了中断处理函数的地址(偏移量)。
- 处理函数通常位于某个内核代码段的特定偏移位置。
🔸 segment selector(段选择子)
- 用于从 全局描述符表(GDT) 或 局部描述符表(LDT) 中选出哪一个段包含中断处理函数。
- 本质上是告诉 CPU:跳转地址属于哪个段。
🔸 T(Type)
- 表示这是哪一种门(中断门、陷阱门、任务门),占 3 位。
- 在 Linux 中通常为 0 x E 0xE 0xE(中断门)或 0 x F 0xF 0xF(陷阱门)。
🔸 DPL(Descriptor Privilege Level)
- 段的权限等级,范围是 0 ∼ 3 0\sim3 0∼3,数字越小权限越高。
- 决定了用户程序是否能触发该中断,比如系统调用向量必须设置合适的 DPL。
🔸 P(Present)
- 表示该表项是否有效,若 P = 0 P=0 P=0,说明这项是未启用的。