首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

上下文切换控制三色灯

我们知道操作系统的一个核心功能就是进行任务调度。任务调度的意义在于通过对资源的分时复用,使得计算机好像可以同时拥有多个进程处理很多任务。 所谓的上下文切换是指操作系统变更执行任务的这一过程,它发生在任务调度后,涉及到两个动作——保存上文和恢复下文。

保存上文的目的是方便有效的恢复下文,这点与处理器响应中断请求进入和退出服务函数时的操作有些类似。 但两者所讨论的范畴是不一样的,中断服务是处理器响应外部事件的机制,上下文切换是操作系统变更执行任务的机制。 两者一个在于底层的硬件,一个是上层软件的抽象。

本文将介绍上下文切换的基本原理和STM32针对操作系统在硬件上所做的优化机制。同时我们还会实现一个简单的上下文切换机制,来控制开发板上的LED灯不断闪烁。

1. 上下文切换的原理

在文章的一开始,我们就提到上下文切换一共涉及到保存上文和恢复下文两方面的任务。那么保存上文都需要保存些什么内容呢?这些内容应该保存在哪里呢? 在回答这两个问题之前,我们需要看一下处理器是如何执行程序的,图1描述了处理器的结构。

在处理器中最关键的部分就是CPU,它的核心是一个算术逻辑单元(Arithmetic Logical Unit, ALU),负责了几乎所有的计算任务。 处理器内核中还有一系列的寄存器构成了Register Bank,它们为ALU提供了运算所用的数据。 指令流水线则是用来获取和解析指令的单元。 在每一个指令周期,ALU先从指令流水线中获取一条已经解析的指令并执行,执行过程中可能会用到Register Bank中的数据,运行结果也会放回Register Bank。 同时处理器还会根据ALU执行过程中是否发生过进位, 运行结果是否为零等条件来更新状态寄存器xPSR,它是程序进行条件跳转时的判断依据。

图1 处理器的结构框图

指令和数据一开始是在内存中的,需要先把它们装载到处理器内核中才可以执行。 在Register Bank中有一个程序计数器(Progam Counter, PC),因为它是Register Bank中编号为15的寄存器,所以又被称为R15。 PC寄存器决定了处理器装载指令的地址,指令流水线根据PC的值从内存中获取指令后,PC值会自动增加以获取下一条指令。 通过指令直接或者间接修改PC的值可以实现跳转,进而实现分支语句或者函数调用等语言特性。

在我所理解的概念里面,PC寄存器的存在就可以使我们借助CPU实现我们想要的任何功能了。 但一般情况下,处理器还会实现一个栈机制,用来方便函数调用、中断服务时保存和恢复处理器状态。

处理器通过一个栈指针寄存器(Stack Pointer, SP)提供栈机制。我们需要在内存中专门开辟一段空间用作栈,配置SP指向栈顶。 以后通过压栈和出栈的方式向栈空间中存入和取出数据。压栈时数据被写到SP所指的栈顶中,然后SP自减(取决于处理器的实现,在STM32中自减)指向下一个可用的地址。 出栈时SP自加,数据从栈顶中取出。入栈和出栈的操作提供了栈机制的后进先出(LIFO)的特性。 利用这一特性,退出一个调用的函数时,可以从栈空间中获取进入函数时保存的内容,进而恢复执行调用该函数之前的操作。

在操作系统中,我们希望各个进程之间是相互独立的,它们被装载在内存的不同地址空间下。 尽管用户进程实现的功能各不相同,归结到处理器层面它们都是一样的。通过PC和状态寄存器控制程序的逻辑,借助SP栈机制实现函数调用。 在ALU中进行运算,通过Register Bank把数据装载进CPU或者写回内存。 因此,上下文切换必须保证各个进程在使用CPU进行计算时不会改变其它进程的数据和控制逻辑。 那么在保存上文时,我们就要把Register Bank中的寄存器、系统状态寄存器先写到内存中,在下次切换回来时从内存中把这些寄存器的值再装回CPU。

从内存中装载数据到CPU中大体上有两种方法:1.使用LDR等装载数据的指令,2.通过栈操作实现。 我所见到的上下文切换都是用的第二种方法,这样在切换到下文时,只需要由任务调度器确定目标进程的栈空间地址,修改栈指针指向栈顶, 然后依次把各个寄存器值出栈就达到了恢复下文的目的。所以,上文是保存在进程的栈空间中的。至于第一种方法能否实现上下文切换没有仔细思考过。

2. STM32针对操作系统的优化

《权威指南》中提到, Cortex-M在设计的过程中就为操作系统做了一些优化,比如说它提供了两个栈指针MSP和PSP,通常MSP用作操作系统内核和中断服务,而PSP则用于用户进程。 栈指针比较好理解,就不细说了。Cortex-M还提供了SysTick、SVC、PendSV等一些比较复杂的机制,本着够用的原则,这里只介绍PendSV中断及其在上下文切换过程中的用法。 其余的机制在以后用到的时候再详细解说。

PendSV是一个异常中断,它的中断编号是14, 中断优先级是可以配置的,通常会赋予它最低的优先级。 可以通过向中断控制与状态寄存器(SCB->ICSR)的bit28写1触发。上下文切换推荐放在PendSV的中断服务函数中进行,当操作系统判断需要执行一次上下文切换时, 就通过SCB->ICSR提交一个中断请求。由于PendSV被赋予了最低的优先级,所以只有在所有其它中断服务结束后, 才可以进行上下文切换,而且在切换过程中如果有更高抢占优先级的中断触发时,会优先处理抢占的中断。 这样处理的好处是,系统对于中断可以及时的响应,在有实时性需求的系统中这点尤为重要。

在早期的系统当中,上下文切换发生在任务调度的时候,为了满足系统实时性需求,OS会在调度之前先检查一下是否有中断正在被处理或者还没有处理。 如果条件为真,那么OS不会进行调度,而是等待下次调度的机会。这样会使得上下文切换被推迟了很长一段时间,如果刚好有一个中断的频率与系统调度的频率接近, 那么这种推迟调度的现象就会尤为明显。推迟调度表现在用户层面上就是某个进程甚至是整个系统被卡死,得不到响应。

具有最低优先级的PendSV中断的存在很好的解决了这个问题。它使得OS可以先进行任务调度,如果需要进行上下文切换,那么就请求一个PendSV中断,然后退出调度。 当不再有其它中断需要被处理的时候,系统就会自动的在PendSV的中断服务中进行上下文切换。 这样OS就不需要在调度的时候先进行一次检查了,也不需要等待下次调度再进行切换了。

图2 PendSV上下文切换示例图

图2中描述了一个利用PendSV进行上下文切换的过程,这里假设操作系统使用SysTick计时器触发进行任务调度。 一开始系统正在执行任务A,到a时刻触发了一个中断系统进入该中断的服务函数ISR。 在b时刻SysTick计时中断触发OS进行任务调度,并提出一个PendSV中断请求进行上下文切换。 在c时刻,OS调度完毕系统恢复到ISR中继续中断响应。 d时刻,中断服务结束,系统处理PendSV的中断请求,执行上下文切换。 e时刻,上下文切换结束,系统开始执行任务B。

3. 一个简单的上下文切换机制

现在我们知道,STM32推荐的上下文切换机制是分两段的。首先由OS进行任务调度提交一个PendSV的中断请求,然后在PendSV中真正的实现上下文调度。 按照这一思路,我们将实现一个简单的上下文切换机制。在这个demo中,我们将创建两个任务taska和taskb,在taska中控制三色灯亮红色,taskb控制其亮绿色。 还要实现一个任务调度器,控制两个任务来回切换,而任务调度由任务自身触发。

需要感谢和声明的是,这里主要参考了μC/OS的代码,为我们提供了一个很好的出发点。我们根据自己的需要做了一些简单的修改。 程序的源码托管在了Github上, 这里打包了本文示例中所用的代码和工程文件。

上下文切换的核心在于PendSV的中断函数,我们先来看下这个服务函数都做了些什么。 代码如下,这段代码是用汇编写的,因为我们需要直接访问PSP等内核的寄存器,C语言没有相应的语句可以实现。

        XTOS_PendSV_Handler
        Context_Switch_Save_Senario
        	CPSID	I											; 关中断
            MRS     R0, PSP                                     ; 获取当前PSP
            CBZ     R0, Context_Switch_Load_Senario             ; 如果PSP为0,说明是首次调用,没有上文,直接恢复下文

        	TST 	R14, #0x10									; 根据EXC_RETURN的bit4, 判定是否开启了FPU
        	IT 		EQ											; 若开启了, 则保存S16-S31
        	VSTMDBEQ R0!, {S16-S31} 
	
            SUBS    R0, R0, #0x20                               ; 32位, 4字节对齐,8个寄存器, 0x08 * 4 = 0x20
            STM     R0, {R4-R11}								; 保存R4-R11到当前进程的栈空间中

            LDR     R1, =gp_xtos_cur_task                       ; gp_xtos_cur_task->pTopOfStack = SP;
            LDR     R1, [R1]
            STR     R0, [R1]                                    ; R0中记录了上文栈顶

        Context_Switch_Load_Senario
        	LDR		R0, =gp_xtos_cur_task						; gp_xtos_cur_task = gp_xtos_next_task
        	LDR		R1, =gp_xtos_next_task
        	LDR		R2, [R1]
        	STR		R2, [R0]

        	LDR		R0, [R2]									; R0 = gp_xtos_next_task->pTopOfStack
        	LDM		R0, {R4-R11}								; 装载下文的Callee Saved Registers
        	ADDS	R0, R0, #0x20

        	TST 	R14, #0x10									; 根据EXC_RETURN的bit4, 判定是否开启了FPU
        	IT 		EQ											; 若开启了, 则装载S16-S31
        	VLDMIAEQ R0!, {S16-S31} 

        	MSR		PSP, R0										; 更新PSP = gp_xtos_next_task->pTopOfStack
        	ORR		LR, LR, #0x04								; 确保返回时使用PSP作为栈指针

        	CPSIE	I											; 开中断
        	BX		LR											; 中断返回恢复中断现场时,从栈空间中取出Caller Saved Registers

大体上分为保存上文(Context_Switch_Save_Senario)和恢复下文(Context_Switch_Load_Senario)两个部分。

在保存上文中,主要做了四件事情:

  1. 根据PSP是否为0判定是否为首次进行上下文切换。这一判定是开启操作系统后执行第一个任务时才会成立的。此前因为没有任何任务被执行过, 所以也就不存在上文,直接跳转到恢复下文操作。
  2. 根据装载在LR中的EXC_RETURN的值判定是否开启了FPU,相应的保存FPU的S16-S31寄存器。
  3. 保存R4-R11寄存器到当前的栈空间中。
  4. 更新当前任务的tcb(task control block)的栈顶指针。

Cortex-M4中的Register Bank有15个寄存器,其中R0-R3, 和R13-R15在中断触发时由处理器将之压入了栈空间,因此我们这里只保存了R4-R11寄存器。由处理器的中断机制入栈的寄存器,称为Caller Saved Register, 而需要我们手动入栈的寄存器称为Callee Saved Register。类似的,对于浮点运算的寄存器其S0-S15都是Caller Saved Register。

在恢复下文中,基本上是把保存上文的操作反过来做了一遍,主要做了如下事情:

  1. 获取切换目标任务的栈顶指针,准备恢复下文。
  2. 从目标任务的栈空间中出栈R4-R11。
  3. 恢复FPU的S16-S31寄存器。
  4. 更新PSP指向目标任务的栈顶,修改LR寄存器中EXT_RETURN使得,从服务函数中返回进入任务后,使用PSP作为栈指针。
  5. 打开中断并退出中断服务,交由处理器的中断机制恢复其它的Caller Saved Registers。

在实现了XTOS_PendSV_Handler之后,我们还需要修改一下启动文件中向量表PendSV的服务函数名称如下图所示:

实现了上下文切换之后,我们还需要解决如何创建任务以及如何开启操作系统的问题,我们将在下一篇文章中详细的解释。

4. 总结

在本文中我们多次强调了上下文切换涉及到保存上文和恢复下文两个操作。需要保存的上文是处理器内核的各个寄存器, 上文将被保存到当前正在执行的任务的栈空间中。恢复下文则通过修改PSP指针从目标任务的栈空间中装载先前保存的各个寄存器的值。

STM32推荐使用具有最低优先级的PendSV中断来进行上下文切换,这样可以保证系统的各个中断响应是实时的, 同时能够保证操作系统的任务调度能够在没有中断需要处理时较快的响应。

最后我们参考μC/OS的代码,根据实际需要做了一些修改。下一篇文章中,我们将详细介绍如何创建任务、如何开启操作系统、 任务如何触发操作系统进行任务调度。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1