OS 特性之PendSV 异常
上一篇文章我们了解了 SVC 异常,其可以作为一个 API ,使应用任务能够访问到系统资源,对于 OS 的设计来说非常重要。今天我们就来看一下另一个对 OS 设计及其重要的异常 —— PendSV 异常。
PendSV(可挂起的系统调用)的异常编号为 14,同样,其具有可编程的优先级。可以写入中断控制和状态寄存器(ICSR)设置挂起位以触发 PendSV 异常(详见:深入理解用于中断控制的 SCB 寄存器)。但其与 SVC 异常不同,它是不精确的。因此,它的挂起状态可在更高优先级的异常处理内设置,且会在更高优先级处理完成后执行。
利用该特性,若将 PendSV 设置为最低的异常优先级,就可以让其处理在所有其他中断处理任务完成后执行,这对于上下文切换非常有用,也是 OS 设计中的核心。
首先我们来看下上下文切换的几个基本概念。在具有嵌入式 OS 的典型系统中,处理时间被划分为了多个时间片。此时若系统中只有两个任务,那么这两个任务会交替执行,如下图所示:

OS 内核的执行可由以下条件触发:
-
应用任务中 SVC 指令的执行。例如,当应用任务由于等待一些数据或事件被耽搁时,它可以调用系统服务以便切换到下一个任务。
-
周期性的 SysTick 异常。
在 OS 代码中,任务调度器可以决定是否执行上下文切换。上图所示的操作假定了 OS 内核的执行由 SysTick 异常触发,且每次触发它都会决定切换到一个不同的任务。
若中断请求(IRQ)在 SysTick 异常前产生,则 SysTick 异常可能会抢占 IRQ 处理。在这种情况下,OS 不应该执行上下文切换,否则,IRQ 的处理就会被延迟,如下图所示:

对于 Cortex-M3 和 Cortex-M4 处理器,当存在活跃的异常服务时,设计默认不允许返回到线程模式(当然存在例外情况,即之前文章中提到的非基本线程使能的情况,以后会对这个概念进行详细的解释)。若存在活跃的中断服务,且 OS 试图返回到线程模式,就会触发使用错误异常。
在一些 OS 设计中,要解决上述问题,可以设计为在运行中断服务时不执行上下文切换,这可以通过检查栈帧中的 xPSR 或 NVIC 中的中断活跃寄存器来实现。但这样的话系统的性能可能会受到影响,特别是中断源在 SysTick 中断前后持续产生请求时,这样上下文切换就可能没有执行的机会。
PendSV 异常则通过将上下文切换请求延迟到所有其他 IRQ 都处理完成后执行来解决上面的这个问题。当然要实现这种效果,需要将 PendSV 的优先级设置到最低。若 OS 需要执行上下文切换,它会设置 PendSV 的挂起状态,并且 PendSV 异常处理中执行上下文切换。下图为利用 PendSV 进行上下文切换的一个实例:

上述实例具有以下的事件流程:
-
A 任务调用 SVC 进行任务切换(例如要等待一些工作完成)。
-
OS 收到请求,准备进行上下文切换,此时挂起 PendSV 异常。
-
当 CPU 退出 SVC 时,会立即进入 PendSV 并进行上下文切换。
-
当 PendSV 完成并返回线程等级时,系统会运行 B 任务。
-
中断产生并进入中断处理。
-
在运行中断处理函数时产生 SysTick 异常(用于 OS 节拍)。
-
OS 执行一些重要的操作,然后挂起 PendSV 异常并准备进行上下文切换。
-
当 SysTick 异常退出时,会返回到先前执行到一半的中断服务程序中。
-
当中断服务程序结束后,PendSV 开始执行实际的上下文切换。
-
当 PendSV 完成后,程序返回到线程等级,这时它会回到任务 A 并继续执行。
除了 OS 环境中的上下文切换,PendSV 还可以用于不存在 OS 的环境中。例如,中断服务程序可能需要一些处理时间,要处理的部分可能会需要最高优先级。不过如果整个 ISR 都是在高优先级中执行的,其他中断服务函数就可能在很长时间内都无法执行。在这种情况下,可以将中断服务处理划分为两个部分,如下图所示:

-
第一部分对时间的要求比较高,需要快速执行,且优先级较高。它位于普通的 ISR 内,在 ISR 结束时,设置 PendSV 的挂起状态。
-
第二部分包括中断服务所需的剩余处理逻辑,它位于 PendSV 处理内且具有较低的异常优先级。
