在上一篇文章中,我们已经了解了中断和异常的一些概念,对于中断和异常也有了大概的理解。那么,系统中硬件到底是如何处理中断和异常的呢?本文我们就以常见的X86架构为例,看看中断和异常的硬件工作原理。
之前,我们主要考虑的单处理器系统,如果是多处理器系统,主PIC控制器的INTR管脚是如何接到CPU上的?我们接下来讨论这个话题。
我们知道,多核处理系统的价值在于 并行处理。所以,如何把中断分配到每一个CPU上就至关重要了。基于这个原因,Intel从奔腾III开始,引入一个新的高级可编程中断控制器(I/O-APIC
)。这个控制器是8259A中断控制器的加强版。为了兼容旧版本的操作系统,有些主板包含这两种芯片。x86架构中,每个处理器包含自己的APIC,每个APIC具有32位的寄存器,内部时钟,内部定时器以及2个额外的IRQ线,LINT0和LINT1,用作APIC的中断。所有私有的APIC都连接到I/O-APIC
,组成一个多APIC系统。
图4-1展示了一个多APIC系统的原理图。I/O-APIC
通过APIC总线和各个APIC连接在一起。I/O-APIC
相等于一个中继的角色。
图4-1 多APIC系统
I/O-APIC
由24条中断线,中断重定向表,可编程寄存器和一个通过APIC总线收发数据的消息单元组成。与8259A中断控制器不同,管脚编号不再具有优先级:重定向表中的每一项都可以被独立设置中断向量和优先级,目的处理器以及处理器如何处理该中断。也就是说,中断重定向表就是外部IRQ到私有APIC的映射关系。
中断请求被分配到CPU上的方式有两种:
除了CPU与外设之间的中断,多APIC系统还允许CPU产生CPU之间的中断。当一个CPU想给另一个CPU发送中断时,它就会把目标CPU的私有APIC的标识符和中断号存储到自己APIC的中断命令寄存器(ICR)中。然后通过APIC总线发送给目标APIC,该APIC就会给自己的CPU发送一个相应的中断。
CPU间的中断(简称IPI)是多核系统一个重要组成部分。Linux有效地利用它们,在CPU之间传递消息。
目前,大部分的单核系统也都包含一个I/O-APIC芯片,可以使用两种不同的方式配置它:
x86架构大约有20种不同的异常。内核必须为每种异常提供专用的处理函数。对于某些异常,CPU控制单元也会产生硬件错误码,并将其压入内核态栈,然后再启动异常处理函数。
下表是异常列表,列出了异常号,名称,类型等等。更多信息请参考Intel技术手册。
# | 异常 | 类型 | 异常处理函数 | 信号 |
---|---|---|---|---|
0 | 除法错误 | fault | divide_error() | SIGFPE |
1 | Debug | trap/fault | debug( ) | SIGTRAP |
2 | NMI | - | nmi( ) | - |
3 | 断点 | trap | int3( ) | SIGTRAP |
4 | 溢出 | trap | overflow( ) | SIGSEGV |
5 | 边界检查 | fault | bounds( ) | SIGSEGV |
6 | 非法操作码 | fault | invalid_op( ) | SIGILL |
7 | 设备不可用 | fault | device_not_available( ) | - |
8 | 串行处理异常错误 | abort | doublefault_fn() | - |
9 | 协处理器错误 | abort | coprocessor_segment_overrun( ) | SIGFPE |
10 | 非法TSS | fault | invalid_TSS( ) | SIGSEGV |
11 | 段引用错误 | fault | segment_not_present( ) | SIGBUS |
12 | 栈段错误 | fault | stack_segment( ) | SIGBUS |
13 | 通用保护 | fault | general_protection( ) | SIGSEGV |
14 | 页错误 | fault | page_fault( ) | SIGSEGV |
15 | Intel保留 | - | - | - |
16 | 浮点错误 | fault | coprocessor_error( ) | SIGFPE |
17 | 对齐检查 | fault | alignment_check( ) | SIGBUS |
18 | 机器检查 | abort | machine_check() | - |
19 | SIMD浮点异常 | fault | simd_coprocessor_error() | SIGFPE |
Intel保留20-31未来使用。如上表所示,每个异常都有一个专门的处理函数处理,并给造成异常的进程发送一个信号。
现在,我们已经知道了中断信号是如何从设备发出,然后经过高级可编程中断控制器的分配,到达各个指定的CPU中。那么,剩下的工作就是内核的了,内核使用一个中断描述符表(IDT),记录每个中断或者异常编号以及相应的处理函数。那么,收到中断信号后,将相应的处理函数的地址加载到eip寄存器中执行即可。
IDT表中,每一项对应一个中断或者异常,大小8个字节。因而,IDT需要256x8=2048个字节大小的存储空间。
IDT表的物理地址存储在CPU寄存器idtr
中:包括IDT的基地址和最大长度。在使能中断之前,必须使用lidt汇编指令初始化IDT表。
IDT表包含三种类型的描述符,使用Type位域表示(40-43位)。下图分别解释了这三种描述符各个位的意义。
三种描述符分别为:
现在,我们来探究一下CPU控制单元是如何处理中断和异常的。我们假设内核已经完成初始化,CPU工作在保护模式下。
CPU控制单元,在取指令之前,检查控制单元在执行前一条指令的时候是否有中断或异常发生。如果发生中断,控制单元就会做如下处理:
至此,CPU控制单元跳转到中断或异常处理程序处开始执行。等到中断或异常处理完成后,把CPU的使用权让给之前被中断的进程,使用iret指令,该指令强迫控制单元执行下面步骤:
本文分享自 嵌入式ARM和Linux 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!