异常和中断
一、术语说明
- 逻辑控制流: 指的是程序从开始第一条指令之后,PC中产生的值序列 : 其中是指令的地址,是的下一条指令。其中与并不一定要是在内存中相邻的,也就是由于跳转,调用,返回,引起的指令地址不相邻的情况我们也称为下一条,简单地说就是 逻辑控制流是程序本身就可以预测到的指令序列。
- 异常控制流 : 但是有些指令并不是可以在程序中预测到的,比如缺页异常,打印机缺纸,发生这种事件是不可预测的,我们将这种突变称为异常控制流。这里要说的中断,异常,系统调用都是异常控制流。但是有的书将他们统称为异常,或者中断,我们这里统称为异常控制流(Exception Control Flow,ECF), 简称异常流,以区分异常中断和系统调用。
异常控制流分为4类:
类别 | 原因 | 异步/同步 | 返回行为 |
---|---|---|---|
中断(interrupt) | 来自处理机外部设备的信号(e.g. IO) | 异步 | 总是下一条指令 |
陷阱(trap) | 有意的异常 | 同步 | 总是下一条指令 |
故障(fault) | 潜在可恢复错误 | 同步 | 可能返回当前指令 |
终止(abort) | 不可恢复错误 | 同步 | 杀死程序,不返回 |
- 这里的下一条仅仅是逻辑上的,跳转,返回,或者调用的下一条,并不一定是内存上相邻的下一条,(注:无特别说明均如此)
异常流的处理方式:(这里讲的是一个笼统的处理方式,不同的异常流可能会不一样。不涉及细节)
- 保存现场,即保存PC和状态寄存器
- 进入内核态
- 找到相应的处理程序(有一个异常流表记录了所有的异常流条目)
- 根据处理程序的结果返回
异常流与过程调用的区别:
- 返回地址,过程调用一定是将返回地址(逻辑流下一条指令)压栈,而异常可能是下一条指令,可能是当前指令。
- 处理器会把状态字寄存器压栈,而过程调用不会
- 异常流处理运行在内核模式下
- 如果控制从用户程序转到内核,所有的这些项目都被压倒内核栈中。
二、异常
指CPU内部出现的中断,即在CPU执行特定指令时出现的非法情况。同时异常也称为同步中断,因此只有在一条指令执行后才会发出中断,不可能在指令执行期间发生异常。
异常是由于执行了现行指令所引起的。由于系统调用引起的中断属于异常。
2.1产生的原因
- 程序的错误产生的,编程异常通常叫做软中断(eg:除数为0)
- 内核必须处理的异常条件产生的(eg:缺页)
2.2产生的目的
Linux利用异常来达到两个截然不同的目的:
-
给进程发送一个信号以通报一个反常情况
例如,如果进程执行了一个被0除的操作,CPU则会产生一个“除法错误”异常,并由相应的异常处理程序向当前进程发送一个SIGFPE信号。当前进程接收到这个信号后,就要采取若干必要的步骤,或者从错误中恢复,或者终止执行(如果这个信号没有相应的信号处理程序)。
-
管理硬件资源:内核使用两种异常来有效地管理硬件资源,相应的处理程序也就更复杂。在这种情况下,异常并不表示一种错误情况:
- 用“设备不可用”异常来推迟装载浮点寄存器。
- 用“缺页”异常推迟把新页框分配给进程。
2.3特点
-
产生都不使用中断控制器,中断号由指令直接给出。不能被屏蔽。
-
异常没有自己的进程上下文,会用到当前进程的进程上下文。
-
在CPU执行一个异常处理程序时,就不再响应其他异常和中断请求服务。
如果此时发生了一个异常,CPU不能去响应它,又不能把它的信息丢失该怎么办呢?这是就用到了堆栈,把所有的信息压入栈。等当前异常处理后,才从堆栈中取出信息再响应刚才的异常。
-
X86处理处理器中大约有20种异常。Linux内核必须为每种异常提供一个专门的异常处理程序。
三、中断
也称为异步中断。因此它是由其他硬件设备依照CPU时钟信号随机产生,即意味着中断能在指令之间发生。
中断是由于系统中某事件引起的,该事件与现行指令无关。
3.1特点
-
中断主要是响应外部硬件设备的。
-
产生通过中断控制器,中断号是由中断控制器提供的(NMI硬中断中断号系统指定为02H)。中断又分为外部可屏蔽中断(INTR)和外部非屏蔽中断(NMI)
- 所有IO设备产生的中断请求均引起可屏蔽中断。
- 硬件故障引起的故障则产生非屏蔽中断。
-
中断使用自己的中断上下文,原来的进程上下文保持不变,而且可以返回中断之前所作的事件。
-
在CPU执行一个异常处理程序时,就不再响应其他异常和中断请求服务。
如果此时产生多个非屏蔽中断时,CPU的处理方法跟异常处理方法一样,使用堆栈。
四、关系
相同点:都是CPU对系统发生的某个事情做出的一种反应。
区别:中断由外因引起,异常由CPU本身原因引起。
引入原因:
-
中断:为了支持CPU和设备之间的并行操作
当CPU启动设备进行输入/输出后,设备便可以独立工作, CPU转去处理与此次输入/输出不相关的事情;当设备完成 输入/输出后,通过向CPU发中断报此次输入/输出的结 果,让CPU决定如何处理以后的事倩
-
异常:表示CPU执行指令时本身出现的问题
如算术溢出、除零、取数时的奇偶错,访存地址时越界或执行了“陷入指令”等,这时硬件改变了CPU当前的执行流程,转到相应的错误处理程序或异常处理程序或执行系统调用
引发中断或异常的事件
- 中断:外部事件引起,正在运行的程序所不期望的
- 异常:内部执行指令引起
五、Cortex-M中的SVC异常与PendSV异常
SVC(系统服务调用,亦简称系统调用,System Service Call)和 PendSV(可悬起系统调用,Pend System Service Call)),它们多用于在操作系统之上的软件开发中。
SVC 用于产生系统函数的调用请求。例如,操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就会产生一个 SVC 异常,然后操作系统提供的 SVC 异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。
PendSV(可悬起的系统调用),它和 SVC 协同使用。一方面, SVC异常是必须立即得到响应的(若因优先级不比当前正处理的高, 或是其它原因使之无法立即响应, 将造成硬件fault),应用程序执行 SVC 时都是希望所需的请求立即得到响应。另一方面, PendSV 则不同,它是可以像普通的中断一样被悬起的(不像 SVC 那样立刻执行)。 OS 可以利用它“缓期执行” 一个异常——直到其它重要的任务完成后才执行动作。 悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。 悬起后, 如果优先级不够高,则将缓期等待执行。
PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。例如,一个系统中有两个就绪的任务,上下文切换被触发的场合可以是:
- 执行一个系统调用
- 系统滴答定时器(SYSTICK,System Tick Timer)中断(轮转调度中需要)
假设有这么一个系统,里面有两个就绪的任务,并且通过 SysTick 异常启动上下文切换:
但若在产生 SysTick 异常时正在响应一个中断,则SysTick 异常会抢占其 ISR(Interrupt Service Routine)。在这种情况下, OS 不得执行上下文切换,否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。因此,在Cortex-M中也是禁止的——如果 OS 在某中断活跃时尝试切入线程模式,将触犯用法 fault 异常。
为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有没有任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了 IRQ(Interrupt ReQuest),则本次 SysTick 在执行后不得作上下文切换,只能等待下一次 SysTick 异常),尤其是当某中断源的频率和 SysTick 异常的频率比较接近时,会发生“共振”。**而PendSV可以完美解决这个问题,PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。**为实现这个机器制,需要PendSV 编程为最低优先级的异常。如果 OS 检测到某 IRQ 正在活动并且被 SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换:
- 任务 A 呼叫 SVC 来请求任务切换(例如,等待某些工作完成)
- OS 接收到请求,做好上下文切换的准备,并且 pend 一个 PendSV 异常。
- 当 CPU 退出 SVC 后,它立即进入 PendSV,从而执行上下文切换。
- 当 PendSV 执行完毕后,将返回到任务 B,同时进入线程模式。
- 发生了一个中断,并且中断服务程序开始执行
- 在 ISR 执行过程中,发生 SysTick 异常,并且抢占了该 ISR。
- OS 执行必要的操作,然后 pend 起 PendSV 异常以作好上下文切换的准备。
- 当 SysTick 退出后,回到先前被抢占的 ISR 中, ISR 继续执行
- ISR 执行完毕并退出后, PendSV 服务例程开始执行,并且在里面执行上下文切换
- 当 PendSV 执行完毕后,回到任务 A,同时系统再次进入线程模式。