FreeRTOS任务切换的简易分析
- 架构:Cortex-M3
- 版本:FreeRTOS V9.0.0
- 前言:之前分析了创建任务、启动调度器,在做完这些工作后,就是该完成所有RTOS的最核心的部分,任务的上下文切换,可以说,任务切换就是RTOS。
在分析之前,先分析Cortex-M3的SVC和PendSV
1.SVC
首先要有一个概念,特权模式和用户模式。在用户模式下,有的寄存器你是操作不了的,需要进入到特权模式下,才能操作。svc其实就是一个系统调用,可以在用户模式下调用,调用后就会进入到SVC的异常服务例程里,这时,CPU就已经进入到了特权模式了,这种机制很好的将内核区域和用户区域区分开来。
2.PendSV
要引用这个异常之前,要知道以前的OS,是使用SYSTICK来进行任务切换的:
![](https://i-blog.csdnimg.cn/blog_migrate/04a9fe7ebc2f90d5e8e0cbe0dbd82f20.png)
当发生外部中断的时候,正准备处理,此时SYSTICK异常也触发了,那么由于SYSTICK的优先级大于IRQ的,所以会先进行上下文切换,再执行中断,很显然,中断被延迟执行了,这个是不符合RTOS的实时要求的。
这种方式被否决了,为了解决这个问题,早期的OS会检测中断是否活跃,当没有任何中断需要响应时才切换上下文:
![](https://i-blog.csdnimg.cn/blog_migrate/f19cdaadba471c316de5a5b64dbf7cd9.png)
那么会有一个问题,当IRQ执行完才会切换任务,那么切换任务会被拖延。
这时候就引用了新的异常PendSV
![](https://i-blog.csdnimg.cn/blog_migrate/0ec518245ee9c6aa82db2d2e0d206e53.png)
可以看到,当要发生一次任务切换时,并不是马上去切换,而且先挂起一个PendSV异常,如果此时没有优先级比PendSV高的中断,那么就响应PendSV异常,PendSV响应服务程序里面就会去切换任务。假如此时来了一个中断异常,那么就先执行中断服务例程,执行中断期间优先级更高的SysTick触发异常,就会先执行Systick异常服务例程,例程里面会挂起一个PendSV异常,然后退出Systick并进入到刚刚被打断的中断服务,中断服务完成后,就会去响应PendSV切换上下文。
以上都是参考《Cortex-M3权威指南》
3.任务切换
根据上面异常两个的分析,可以知道FreeRTOS任务切换的位置就是在PendSV里面。
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp
isb
ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
ldr r2, [r3]
stmdb r0!, {r4-r11} /* Save the remaining registers. */
str r0, [r2] /* Save the new top of stack into the first member of the TCB. */
stmdb sp!, {r3, r14}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14}
ldr r1, [r3]
ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0!, {r4-r11} /* Pop the registers and the critical nesting count. */
msr psp, r0
isb
bx r14
nop
}
首先,进入到PendSV的时候,CPU是处于特权模式下的,也就是说当前的SP用的是MSP,还要注意的是此时pxTopOfStack
和PSP的值是相同的。把psp的值赋值到R0,再把pxCurrentTCB->pxTopOfStack
的值赋值到R2,然后手动把R4-R11往中PSP中入栈,入栈完了以后,把R0的值更新到pxTopOfStack(R2)里,然后把R3和R14的值存入MSP中,保存的目的,是因为等会调用了vTaskSwitchContext
后R3和R14会被修改,R3当前pxCurrentTCB。开启临界区,执行vTaskSwitchContext
,获取到新的pxCurrentTCB(R1)的地址后,退出临界区,把R3和R14恢复,然后把R1(pxCurrentTCB)的值放pxCurrentTCB中,那么pxCurrentTCB就指向新任务的TCB了。取新任务的pxTopOfStack,手动出栈R4~R11,然后把R0(出栈后的指针)赋值到PSP中,然后跳转到新任务中。这就是整个切换任务的过程。
总结:
1. 获取PSP指针
2. 将当前任务的现场(R4~R11)保存,保存现场后,更新`pxTopOfStack`的地址。
3. R3和R14的值存入MSP中
4. 进入临界区
5. 调用`vTaskSwitchContext`
6. 退出临界区
7. 恢复R3和R14
8. 把`pxCurrentTCB`的值修改为R1(新任务的TCB指针)
9. 手动出栈R4~R11
10. 把R0赋值给PSP
11. 跳转到新任务执行代码
![](https://i-blog.csdnimg.cn/blog_migrate/f02fe8f6301b4501414d7cb7fcd0c020.png)
4.任务优先级选择
细看vTaskSwitchContext
这个函数
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
/* The scheduler is currently suspended - do not allow a context
switch. */
xYieldPending = pdTRUE;
}
else
{
xYieldPending = pdFALSE;
traceTASK_SWITCHED_OUT();
#if ( configGENERATE_RUN_TIME_STATS == 1 )
{
#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
#else
ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
#endif
/* Add the amount of time the task has been running to the
accumulated time so far. The time the task started running was
stored in ulTaskSwitchedInTime. Note that there is no overflow
protection here so count values are only valid until the timer
overflows. The guard against negative values is to protect
against suspect run time stat counter implementations - which
are provided by the application, not the kernel. */
if( ulTotalRunTime > ulTaskSwitchedInTime )
{
pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
ulTaskSwitchedInTime = ulTotalRunTime;
}
#endif /* configGENERATE_RUN_TIME_STATS */
/* Check for stack overflow, if configured. */
taskCHECK_FOR_STACK_OVERFLOW();
/* Select a new task to run using either the generic C or port
optimised asm code. */
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
/* Switch Newlib's _impure_ptr variable to point to the _reent
structure specific to this task. */
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
}
}
主要看taskSELECT_HIGHEST_PRIORITY_TASK();
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
/* Find the highest priority list that contains ready tasks. */ \
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
} /* taskSELECT_HIGHEST_PRIORITY_TASK() */
- 首先通过
portGET_HIGHEST_PRIORITY
获取到当前哪个优先级链表上有任务处于就绪状态
具体如下:
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
CLZ是Cortex特有的指令,是用于计算从高位到低位,连续0的个数,比如:0001 0000 0000 0000 0001 0000 0000 0000 ,结果是3,那么31-3 = 28,优先级28链表上有就绪任务等待执行。
2.获取到某个优先级链表后,通过listGET_OWNER_OF_NEXT_ENTRY
获取这个优先级链表上的哪个任务等待执行
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \
{ \
List_t * const pxConstList = ( pxList ); \
/* Increment the index to the next item and return the item, ensuring */ \
/* we don't return the marker used at the end of the list. */ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
} \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}
假设有两个任务,任务A和任务B,运行两次listGET_OWNER_OF_NEXT_ENTRY
![](https://i-blog.csdnimg.cn/blog_migrate/f558eb8cd7b6a6282925469ea9ebfa26.png)