中断和异常
- 定义:中断通常被定义为一个事件,该事件改变了CPU的执行顺序。
- 分类:中断常分为同步中断和异步中断。在intel微处理器中,把同步中断也称为异常,异步中断称为中断。
- 同步中断(异常):当指令执行时由CPU控制单元产生的,之所以称为同步,是因为只有在一条指令执行终止执行后CPU才会发出中断。异常是程序的错误产生的,或者是由内核必须处理的异常条件产生的。
- 异步中断(中断):其他设备发出的中断,具有"随机性",也叫外部中断。由间隔定时器和IO设备产生。
- 中断或异常处理执行的代码不是一个进程,他是一个内核控制路径。另外,中断处理程序比一个进程要“轻”,因为中断的上下文很少,建立或终止中断处理需要的时间很少。
- 中断优先级分级(另一种分类情况下的分级):故障中断>访管中断/程序性中断>外中断>IO中断
- 中断处理是由内核执行的最敏感任务之一,针对中断的响应包括这么几部分:关中断,保存断点,针对中断源(紧急就准备执行,不紧急就排队)。
IRQ和中断
- 每个能发出中断请求的硬件设备控制器都有一条IRQ(interrupt ReQuest)的输出线。而所有IRQ线都和一个可编程中断控制器(PIC)相连。响应中断的具体流程如下:
- PIC监视IRQ
- IRQ发出信号给PIC,PIC把这个信号转换为一个向量,并存放在一个IO端口,从而允许CPU通过总线读取这个向量
- 发送给了CPU,即产生一个中断
- 等待直到CPU通过这个中断信号写进PIC的一个IO端口来确认他。
- 继续监视
- 显然上述的情况是针对单个CPU的简单情况,现在已经是多CPU核心了,为了充分发挥SMP体系结构的并行性,能够把中断传递给系统中的每个CPU至关重要。因此,Intel引入了IO高级可编程控制器(IO APIC)(IO Advanced Programmable interrupt Controller)。使用方式如下图:
- 关于irq的相关代码位于
./arch/i386/kernel/iqr.c
中(这里的例子是以i386(intel32位)体系架构为例,现在的PC体系架构应该是在./arch/x86_64/kernel中),这里展示部分代码:
中断描述表IDT
- 中断描述表(interrupt description table IDT)是一个系统表(其他的系统表还有进程proc表,file表,inode表等等),每一个向量在表中有相应的中断或异常处理程序的入口地址。
- IDT有三种类型的描述符。分别是任务门描述符,中断门描述符和陷阱门描述符。linux利用中断门处理中断,利用陷阱门处理异常。
- 中断和异常的硬件处理:从硬件的角度来看CPU如何处理中断和异常。
中断和异常处理程序的嵌套执行
- 每个中断或异常都会引起一个内核控制路径,相应的内核控制路径的第一部分指令就是把那些寄存器的内容保存在内核堆栈的指令中,最后一部分指令就是恢复寄存器内容并让CPU返回到用户态的那些指令。而内核控制路径可以任意嵌套,和中断响应的时刻图类似。对中断进行处理的内核控制路径,其最后一部分指令并不总能使当前进程返回到用户态,如果嵌套深度大于1,这些指令将执行上次被打断的内核控制路径,此时CPU还在内核态。
- 在
/proc/interrupts
文件会显示IRQ(interrupt request),从左至右依次是中断号,中断在各CPU发生的次数,中断设备名称,硬件中断号,中断处理函数:
- 在
/proc/iqr
目录下面会为每个注册的irq创建一个以irq编号为名字的子目录,每个子目录下分别有以下条目:- smp_affinity irq和cpu之间的亲缘绑定关系,(default_smp_affinity=3,系统默认每个中断可以由两个cpu进行处理)
- smp_affinity_hint 只读条目,用于用户空间做irq平衡只用;
- spurious 可以获得该irq被处理和未被处理的次数的统计信息;
- handler_name 驱动程序注册该irq时传入的处理程序的名字;
异常处理
- 处理异常中设置三个门结构,陷阱门,任务门和中断门初始化,其结构位于
include/asm-x86_84\desc.h
定义,(这里的asm应该是汇编的意思吧,但是却是一个头文件):
相应的实现则是在./arch/x84_64/kernel/traps.c
中:
- 异常处理结构有一共标准的结构,由以下三部分组成:
- 在内核堆栈中保存大多数寄存器的内容(这部分由汇编实现),
- 用高级的C函数处理异常
- 通过
ret_from_exception()
函数从异常处理程序退出
一个典型的异常处理程序被调用的步骤
- 为异常程序保存寄存器的值:假设一个通用的异常处理的名字为handler_name,每一个异常处理程序都以下列的汇编指令开始:
handler_name: pushl $0 /* only for some exceptions*/ pushl $do_handler_name jmp error_code
- 当异常发生时,如果控制单元没有自动地把一个硬件出错代码插入到栈中,相应的汇编语言片段会包含一条pushl $0指令,在栈中垫一个空值,然后,把高级C函数的地址压栈,它的名字由异常处理程序和
do_
前缀构成。