接上文主干内容,我们需要对PendSV内部的源码进行分析,才能更好的得出FreeRTOS是怎么真正执行任务切换的,他怎么知道我下一个任务是什么且任务调用栈的地址,以及如何保护现场、恢复现场?
为此我们找到PendSV的中断函数源码如下,对应汇编指令的用途已经备注好:
//PendSV内部实现
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
//8字节对齐
PRESERVE8
//将PSP指针保存在r0寄存器中,PSP指针保存的时正在执行任务的任务栈指针
mrs r0, psp
isb
//r3保存当前TCB的地址
ldr r3, =pxCurrentTCB
//栈顶指针的地址(结构体第一个成员的地址和结构体的地址相同)
//&栈顶指针的地址 ---> 被分配内存的栈顶地址
ldr r2, [r3]
//压栈操作,r0寄存器保存的指针向下移动,将任务信息保存到内存分配的任务堆栈中
stmdb r0!, {r4-r11}
//把栈顶指针的值修改为r0寄存器保存的指针指向的位置,利于恢复任务操作
str r0, [r2]
/*----------------------------以上为保存现场操作--------------------------*/
/*----------------------------以下为恢复现场操作--------------------------*/
//将r3、r14保存到MSP指针中,压栈操作
stmdb sp!, {r3, r14}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
//通过 vTaskSwitchContext 获取当前优先级最高的任务
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14}
//获取当前任务(任务切换后)的任务栈顶
ldr r1, [r3]
ldr r0, [r1]
//出栈,将内存中保存的任务信息恢复到CPU寄存器中
ldmia r0!, {r4-r11}
msr psp, r0
isb
bx r14
nop
}
对此过程,我们需要结合一张图去理解,如下为保存现场:
那么恢复现场就是相反的操作,把任务栈中的信息全部pop进CPU的内部寄存器中执行,那么最重要的其实就是代码中找到下一个优先级最高的任务,这里就是vTaskSwitchContext这个API,源码如下:
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,就是来寻找优先级最高的那个任务,并把下一个优先级最高(要运行的任务)TCB赋给pxCurrentTCB指针上,之后再通过获取当前任务的栈顶指针并将该任务现场还原。这样就达到了任务切换的目的。
本篇文章接着上文,将任务切换的具体流程解释明白了。总结一下,还是由上文所说的主干开始,当滴答定时器中断后,FreeRTOS会记录系统时钟、根据系统时钟置换阻塞列表、将阻塞列表中过期任务解挂放入就序列表、更新下一次任务解挂阻塞时间、标记是否需要任务切换、如果需要任务切换最后启用PendSV中断进行任务的切换。