RTOS任务调度过程(上下文切换)
解析:
RTOS任务调度的机制是基于内核异常机制的,即每产生一次调度就会产生一次内核异常,那么,要了解RTOS的任务调度机制需要先明白内核的中断系统。RTOS使用其中三个特殊的异常:systick的中断、SVC异常以及PendSV异常。这三个异常是任务能跑起来的根本原因。
Systick中断就是我们常说的滴答定时器中断,Freertos 调度器依靠滴答定时器中断不断地运行,假设每过1ms滴答定时器就会中断一次,调度器就会随着systick的中断1ms运行一次。
除了滴答定时器中断外没和还有很多其他的中断,若滴答定时器中断很频繁且优先级很高,那么有可能其他的内核中断会被频繁的打断。为了抑制这种情况的发生,RTOS中将systick的中断优先级设置为所有中断中最低的优先级(15)。这样滴答定时器就不会打乱其他任何中断执行。同样调度器运行也随着滴答定时器中断优先级,也就是说,任务切换的优先级永远低于内核其他异常。
我们用户也不能直接操作访问硬件设备,此时我们可以产生SVC异常请求,是用于产生调用系统函数的请求;RTOS任务切换时就会先请求SVC异常;当用户程序想要访问某个硬件时,首先触发一个SVC异常,发出对系统服务函数的调用请求,然后操作系统提供的SVC异常服务函数得到执行,该函数再调用操作硬件的系统函数,最终间接访问硬件设备。
SVC异常是必须立即得到响应的,如果因为优先级不比当前正在执行的异常高,或者其他原因导致不能立即执行,则将上访成为硬件fault。
PendSV是可以挂起的异常,它和SVC协同使用,顾名思义,PendSV和SVC不同,PendSV可以像普通中断一样被挂起而延迟执行,RTOS可以利用该特点,等待其他重要任务处理完成后才执行该异常。挂起PendSV的方法是:往NVIC的PendSV挂起寄存器写1。
嵌入式实时操作系统要求能够实时快速响应处理中断请求,如果在处理中断的时候,发生一个任务切换请求,则会触发fault异常,这是不被允许的。为避免此问题,任务切换需要放在可以挂起的异常中去处理,即PendSV,并且将PendSV异常的优先级设为最低,让其处理在所有中断处理完毕后再执行,这有利于任务切换,也是嵌入式实时OS的设计关键,
从上面我们已经知道任务切换是依赖于内核异常的,通常我们使用中断的时候,会发现内核会自动保留现场,中断处理完就会继续中断前的任务,内核是如何知道我上一次执行到哪的呢?它是依靠自身的特殊寄存器记录下来当前程序执行到的函数地址,并把函数的形参、局部变量以及返回值保存起来,等中断处理完了,就从特殊寄存器中恢复这些信息,那么你的程序才得以无误的恢复现场。这保留现场和恢复现场的过程就叫做上下文切换。这个特殊寄存器我们称之为主栈MSP。
任务间的上下文的切换也是基于这个原理的。在创建任务前,我们都会为每个任务开辟一定大小的任务栈空间。在每次任务切换的时候我们按照系统一样将现场保存起来,然后检出高优先级任务的现场,不过这次保存任务现场的不是前面中断所用到的主栈MSP,而是我们创建任务前开辟的任务栈PSP。
MSP用于内核和异常处理
PSP用于应用任务
正如前面所说,cortex-M3具有双堆栈机制,任务创建的时候我们为每个任务设定了栈空间,这个空间用来保存任务在CPU中运行的上下文,也就是CPU中和任务执行相关的数据,如果这些数据丢了,那么任务也就没办法恢复运行了。
当我们的任务TaskA正在运行,此时比它更高优先级的TaskB需要抢占CPU的使用权,此时内核会产生一个SVC异常,OS收到请求后,做好上下文切换,并挂起PendSV异常,CPU退出SVC异常后,就进入PendSV异常,在PendSV异常里面需要做上下文切换,细节如下:
OS首先要做的是将TaskA的运行状态,运行到的函数地址,语句地址、函数的形参、局部变量以及返回值从PSP中弹出,再压入我们开辟的任务栈A中去,完成对TaskA的现场保护;然后从任务栈B中获取上一次运行TaskB的信息:运行到的函数地址,语句地址、函数的形参、局部变量以及返回值,将这些信息压入PSP中,做到TaskB的现场恢复,等退出PendSV异常后,CPU就会恢复了TaskB任务,并继续执行TaskB上一次执行到的语句。这样就完成了一次上下文切换,任务抢占(切换)成功。
有什么不当的地方欢迎补充及指正