前面分析了启动任务调度器的源码,在创建好空闲任务、启动滴答定时器(systick)后就调用SVC中断跳转到任务去执行,但作为OS我们一般不会只创建一个任务,都是有多任务需求才使用OS,后续任务一旦多了,就涉及了到了任务切换,这也是OS的核心,如何根据任务的优先级的和当前状态来切换任务,怎么保证每个任务都有执行的机会,这就是调度器做的工作,而调度器的核心就是任务切换,任务切换工作都在是PendSV中断中进行的,在执行一次系统调用或systick定时器中断里面定时触发一次PendSV中断就会产生一次任务上下文切换。为什么使用PendSV中断来进行任务切换,在Cortex-M3权威手册中的解释非常清晰:
PendSV(可悬起的系统调用),它和 SVC 协同使用。一方面, SVC异常是必须立即得到响应的(若因优先级不比当前正处理的高, 或是其它原因使之无法立即响应, 将上访成硬 fault——译者注), 应用程序执行 SVC 时都是希望所需的请求立即得到响应。另一方面, PendSV 则不同,它是可以像普通的中断一样被悬起的(不像 SVC 那样会上访)。 OS 可以利用它“缓期执行” 一个异常——直到其它重要的任务完成后才执行动作。 悬起 PendSV 的方法是: 手工往 NVIC 的PendSV 悬起寄存器中写 1。 悬起后, 如果优先级不够高,则将缓期等待执行。PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。 例如, 一个系统中有两个就绪的任务,上下文切换被触发的场合可以是:
1、执行一个系统调用
2、系统滴答定时器( SYSTICK)中断,(轮转调度中需要)
让我们举个简单的例子来辅助理解。假设有这么一个系统,里面有两个就绪的任务,并且通过 SysTick 异常启动上下文切换。如下图所示。
上图是两个任务轮转调度的示意图。但若在产生 SysTick 异常时正在响应一个中断,则SysTick 异常会抢占其 ISR。在这种情况下, OS 不得执行上下文切换,否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。因此,在 CM3 中也是严禁没商量——如果 OS 在某中断活跃时尝试切入线程模式,将触犯用法 fault 异常。
为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有没有任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick 在执行后不得作上下文切换,只能等待下一次 SysTick 异常),尤其是当某中断源的频率和 SysTick 异常的频率比较接近时,会发生“共振”。现在好了,PendSV 来完美解决这个问题了。PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。如果 OS 检测到某 IRQ 正在活动并且被SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换。如下图所示
其中事件的流水账记录如下:
1、任务 A 呼叫 SVC 来请求任务切换(例如,等待某些工作完成)
2、OS 接收到请求,做好上下文切换的准备,并且 pend 一个 PendSV 异常。
3、当 CPU 退出 SVC 后,它立即进入 PendSV,从而执行上下文切换。
4、当 PendSV 执行完毕后,将返回到任务 B,同时进入线程模式。
5、发生了一个中断,并且中断服务程序开始执行
6、在 ISR 执行过程中,发生 SysTick 异常,并且抢占了该 ISR。
7、OS 执行必要的操作,然后 pend 起 PendSV 异常以作好上下文切换的准备。
8、当 SysTick 退出后,回到先前被抢占的 ISR 中, ISR 继续执行
9、ISR 执行完毕并退出后, PendSV 服务例程开始执行,并且在里面执行上下文切换
10、当 PendSV 执行完毕后,回到任务 A,同时系统再次进入线程模式。
前面提到了任务切换的触发场合:
1、执行一个系统调用
2、系统滴答定时器( SYSTICK)中断,(轮转调度中需要)
系统调用和systick中断服务函数都是通过写寄存器来实现挂起PendSV中断来切换任务上下文的,先看一下systick中断服务函数的实现:
void SysTick_Handler(void)
{
/* xTaskGetSchedulerState 解析在下面*/
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)
{
/* 调度器已经开启,调用处理函数 */
xPortSysTickHandler();
}
}
BaseType_t xTaskGetSchedulerState( void )
{
BaseType_t xReturn;
if( xSchedulerRunning == pdFALSE )
{
/* 如果调度器还没开启,返回调度器未启动状态 */
xReturn = taskSCHEDULER_NOT_STARTED;
}
else
{
/* 调度器已经开启 */
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
/* 调度器未处于休眠状态(默认为pdFALSE),返回调度器已启动状态 */
xReturn = taskSCHEDULER_RUNNING;
}
else
{
/* 调度器处于休眠状态,返回调度器休眠状态 */
xReturn = taskSCHEDULER_SUSPENDED;
}
}
/* 返回状态值 */
return xReturn;
}
上面代码在进入systick中断服务函数后先判断调度器当前的状态,在运行状态下的话会去调用处理函数,如下:
void xPortSysTickHandler( void )
{
/* 关中断 */
vPortRaiseBASEPRI();
{
/* 计数值递增,这个函数在时间管理章节再分析 */
if( xTaskIncrementTick() != pdFALSE )
{
/* 往中断控制及状态寄存器ICSR(地址:0xE000_ED04)的bit28写1挂起一次PendSV中断 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
/* 开中断 */
vPortClearBASEPRIFromISR();
}
在判断到需要进行一次任务调度,就会挂起一次PendSV中断,由于PendSV的中断优先级最低,所以在执行完所有中断操作后就会进入PendSV的中断服务函数:
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
/* 获取当前psp(进程堆栈)地址到r0 */
mrs r0, psp
isb
/* 获取当前任务控制块指针到r3*/
ldr r3, =pxCurrentTCB
ldr r2, [r3]
/* 将当前堆栈的r4-r11手动压栈,其它寄存器在进入中断时自动压栈了 */
stmdb r0!, {r4-r11}
/* *r2 = r0 把当前psp的地址保存到当前任务控制块的堆栈指针中*/
str r0, [r2]
/* 下面的操作可能修改r3,r14,所以将r3和r14先压栈 */
stmdb sp!, {r3, r14}
/* 关中断 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
/* 进行任务切换,经过这步, pxCurrentTCB(由r3保存着)这个变量变为将要切换的任务控制块指针了*/
bl vTaskSwitchContext
/* 开中断 */
mov r0, #0
msr basepri, r0
/* 前面将r3和14压栈,这里出栈返回 */
ldmia sp!, {r3, r14}
/* 从新的任务控制块中获取出新的栈psp */
ldr r1, [r3]
ldr r0, [r1]
/* 从新的任务堆栈中将r4-r11手动出栈,其它寄存器会在退出中断时自动出栈 */
ldmia r0!, {r4-r11}
/* 更新进程栈psp为切换后的任务堆栈 */
msr psp, r0
isb
/* 跳转到新的任务去执行 */
bx r14
nop
}
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
/* 调度器处于休眠状态不允许任务切换 */
xYieldPending = pdTRUE;
}
else
{
xYieldPending = pdFALSE;
traceTASK_SWITCHED_OUT();
/* 开启了栈溢出检查 */
taskCHECK_FOR_STACK_OVERFLOW();
/* 找出最高优先级任务,找出后赋值给pxCurrentTCB和更新uxTopReadyPriority */
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
}
}
上面找出最高优先级任务有两种方式,一种通用方式(使用软件来实现的话所有处理器都适用),一种硬件方式(需要CPU支持),当宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 为0时使用通用方式,当为1时使用硬件方式。
通用方式的定义如下:
/* 从就绪列表中找到最高优先级任务 */
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
/* uxTopReadyPriority 在每次把任务添加到就绪列表的时候会更新 */ \
UBaseType_t uxTopPriority = uxTopReadyPriority; \
\
/* 一个优先级一个列表,查看当前最高优先级就绪列表下是否有任务 */ \
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \
{ \
configASSERT( uxTopPriority ); \
/* 如果当前最高优先级就绪列表没任务就查看下一个优先级列表 */ \
--uxTopPriority; \
} \
\
/* 获取下一个优先级最高任务的任务控制块 */ \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
uxTopReadyPriority = uxTopPriority; \
}
硬件方式:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
}
/* cortex处理器带有计算前导0个数指令:CLZ */
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
硬件方式使用前导0个数CLZ指令来找出最高优先级任务,使用硬件方法的时候 uxTopPriority 就不代表处于就绪态的最高优先级了,而是使用每个bit代表一个优先级,bit0代表优先级0,bit31代表优先级31,当某个优先级有就绪任务的话就将其对应的bit置1,那么使用硬件方法的话最多只能有32个优先级。__clz( ( uxReadyPriorities ) 就是计算 uxReadyPriorities 的前导零个数,前导零个数就是指从最高为开始(bit31)到第一个为1的bit,其间0的个数,如下:
二进制 1000 0000 0000 0000 的前导零个数为0
二进制 0001 0001 0000 0000 的前导零个数为3
得到 uxReadyPriorities 的前导零个数以后再用31减去这个前导零个数得到的就是处于就绪态的最高优先级了,比如优先级30为此时处于就绪态的最高优先级,30的前导零个数为1,即 31-1=30,得到就绪态的最高优先级为30。
得到就绪态的最高优先级后,使用下面这个函数(listGET_OWNER_OF_NEXT_ENTRY)来找到这个就绪优先级列表中下一个要执行的任务:
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \
{ \
List_t * const pxConstList = ( pxList ); \
/* pxIndex 为上一个任务索引,下一个要执行的即pxNext */ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{ \
/* 由于是环形列表,切默认有一个结束项(xListEnd),如果pxIndex刚好为最后一项,则再指向后面一项, */\
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
} \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}
找到要执行的任务后获取它的任务控制块赋值给 pxCurrentTCB ,这样 xPortPendSVHandler 中的 r3 就会更新为最新的任务控制块,从任务控制块中获取出任务堆栈后跳到相应的任务去执行。上面的systick中断服务函数中只有 xTaskIncrementTick 这个函数返回 pdTRUE 才会触发一次 PendSV 中断,这个函数留到时间管理章节再分析。另外执行系统调用也会触发PendSV 中断,实际上就是某个接口函数对寄存器操作的封装,如下:
#define portYIELD() \
{ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
很多情况下都会调用这个接口,比如在任务创建完后,如果调度器是处于运行状态下的,就会在创建完任务后强制执行一次任务调度,还有一些其他其它会调用到,后面遇到再进行分析。