本章是FreeRTOS系统的核心内容,更是面试求职过程中重要的考点。对于实时操作系统,任务切换决定了任务执行顺序,任务切换也决定了效率的高低。下面将详细介绍任务切换的内容。
任务切换是依靠PendSV中断(可挂起的系统调用)来实现的,因此PendSV中断对操作系统来说是重点。
PendSV中断是可编程中断,触发条件是:将中断控制和状态寄存器ICSR的bit28置为1。与SVC不同,它是不精准的,因此它的挂起状态可在更高优先级中断中处理内设置,而且会在更高优先级处理完后执行。
利用此特性,将PendSV设置为最低优先级,在其他中断执行完后,在进行任务切换。
在典型的嵌入式系统中,处理时间将会被划分多个片段,假设只有两个任务,任务将交替执行,如下图:
此时上下文切换场合为:
1.执行系统调用;
2.系统滴答定时器中断(SysTick)。
若中断请求(IRQ)在SysTick中断之前产生,则SysTick中断会抢占IRQ的处理,IRQ处理没有结束,此时不应该进行任务切换,否则IRQ处理就会被延迟,这是不能容忍的。下图是IRQ延迟:
为了解决这个问题,PendSV将上下文切换延迟到IRQ处理完了以后,PendSV需要将优先级设置为最低。PendSV任务切换如下图:
在上一节中提到了任务切换的场合:
1.执行系统调用;
2.系统滴答定时器中断(SysTick)。
系统调用就是执行FreeRTOS能够进行任务切换的API,比如taskYIELD(),调用taskYIELD()函数的一些API也称为系统调用。taskYIELD()具体如下:
#define taskYIELD() portYIELD()
#define portYIELD() \
{ \
\
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \ //-----------(1)
\
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
(1)将ICSR的bit28置为1,触发中断,就可以在PendSV中断服务程序中进行任务切换。
FreeRTOS的滴答定时器中断服务函数也能进行任务切换,服务程序如下:
void SysTick_Handler(void)
{
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已运行
{
xPortSysTickHandler();
}
}
SysTick_Handler又调用了xPortSysTickHandler()函数来实现切换:
void xPortSysTickHandler( void )
{
vPortRaiseBASEPRI(); //-----------------(1)
{
if( xTaskIncrementTick() != pdFALSE )
{
portNVIC_INT_CTRL_REG = \
portNVIC_PENDSVSET_BIT; //-----------------(2)
}
}
vPortClearBASEPRIFromISR(); //-----------------(3)
}
(1)关闭中断;
(2)将ICSR的bit28置为1,挂起PendSV来启动PendSV中断;
(3)打开中断。
从以上两个场合可以看出,在FreeRTOS进行任务切换都是通过触发PendSV中断来实现。
终于来到了PendSV中断服务程序PendSV_Handler(),FreeRTOS任务的切换都是在这里完成的,下面将详细介绍。
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB; /* 指向当前激活的任务 */
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp /* PSP内容存入R0 */
isb /* 指令同步隔离,清流水线 */
ldr r3, =pxCurrentTCB /* 当前激活的任务TCB指针存入R2 */
ldr r2, [r3]
stmdb r0!, {r4-r11} /* 保存剩余的寄存器,异常处理程序执行前,硬件自动将xPSR、PC、LR、R12、R0-R3入栈 */
str r0, [r2] /* 将新的栈顶保存到任务TCB的第一个成员中 */
stmdb sp!, {r3, r14} /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,调用函数时,返回地址自动保存到R14中,
所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护; R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,
函数调用后会用到,因此也要入栈保护*/
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界区 */
msr basepri, r0
dsb /* 数据和指令同步隔离 */
isb
bl vTaskSwitchContext /* 调用函数,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
mov r0, #0 /* 退出临界区*/
msr basepri, r0
ldmia sp!, {r3, r14} /* 恢复R3和R14*/
ldr r1, [r3]
ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
ldmia r0!, {r4-r11} /* 出栈*/
msr psp, r0
isb
bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,然后出栈,
这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
nop
}
主堆栈指针MSP:对于 Cortex-M3 硬件,当系统复位后,默认使用 MSP 指针。MSP 指针用于操作系统内核以及处理异常(也就是说中断服务程序中默认强制使用 MSP 指针,这是硬件自动设置的)。
进程堆栈指针PSP:任务(进程)使用 PSP 指针,操作系统负责从 MSP 指针切换到 PSP 指针。
任务堆栈:每个任务都有自己的“任务堆栈”,在任务创建时会创建指定大小的任务堆栈,这是任务能够独立运行的前提条件之一。在任务中定义的局部变量,会优先使用寄存器,寄存器不够时就使用任务堆栈的空间。如果在任务中调用其它函数,则调用前的保存信息也存到任务堆栈中去。
堆栈:操作系统内核以及异常处理程序中使用 MSP 指针,所以它们也需要一个堆栈空间,我们称之为“堆栈”,这个堆栈空间和任务堆栈空间在物理上是绝对不可以重叠的。
以上介绍了几个重要的概念,下面详析中断服务函数。
mrs r0, psp
将任务堆栈指针 PSP 的值保存到寄存器 R0中。
ldr r3, =pxCurrentTCB /* 当前激活的任务TCB指针存入R2 */
ldr r2, [r3]
获取当前激活的任务 TCP 指针。
r0!, {r4-r11}
将寄存器 R4~R11 保存到当前激活的程序任务堆栈中,并且同步更新寄存器R0的值。
str r0, [r2]
寄存器 R2 中保存当前激活的任务 TCB 指针。
stmdb sp!, {r3, r14}
将R3和R14 临时压入堆栈。调用函数时,返回地址自动保存到R14 中,所以一旦调用发生,R14 的值会被覆盖,因此需要入栈保护。R3 保存的当前激活的任务 TCB 指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护。
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
进入临界区。
bl vTaskSwitchContext
调用函数,选择下一个要执行的任务,也就是寻找处于就绪态的最高优先级任务。下节详细介绍。
mov r0, #0
msr basepri, r0
退出临界区。
ldmia sp!, {r3, r14}
将寄存器 R3 和 R14 从堆栈中恢复。
ldr r1, [r3]
ldr r0, [r1]
获取变量pxCurrentTCB指向的任务TCB 指针,并将 TCB 的第一个成员——当前堆栈栈顶的指针变量pxTopOfStack的值保存到寄存器R0 中,也就是将即将运行的任务堆栈栈顶值存入R0。
ldmia r0!, {r4-r11}
将寄存器 R4~R11 出栈,并同时更新 R0 的值。
msr psp, r0
将最新的任务堆栈栈顶赋值给线程堆栈指针 PSP。
bx r14
从异常中断服务程序退出,任务切换完成。
在PendSV中断服务函数中,调用vTaskSwitchContext()来找到下一个要运行的任务,这是任务切换的核心。
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) //-----------------(1)
{
xYieldPending = pdTRUE;
}
else
{
xYieldPending = pdFALSE;
traceTASK_SWITCHED_OUT();
taskCHECK_FOR_STACK_OVERFLOW();
taskSELECT_HIGHEST_PRIORITY_TASK(); //-----------------(2)
traceTASK_SWITCHED_IN();
}
}
(1)任务调度器挂起不能进行任务切换;
(2)获取下一个要运行的任务。
查找下一个要运行的任务有两种方法:通用方法和硬件的方法。
● configUSE_PORT_OPTIMISED_TASK_SELECTION 设置为 0 ;
● 可以用于所有 FreeRTOS 支持的硬件;
● 完全用 C 实现,效率略低于特殊方法;
● 不强制要求限制最大可用优先级数目。
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
/* 从就绪列表数组中找出最高优先级列表*/ \
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ]))) \
{ \
configASSERT( uxTopReadyPriority ); \
--uxTopReadyPriority; \
} \
\
/* 相同优先级的任务使用时间片共享处理器就是通过这个宏实现*/ \
listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) ); \
}
● 并非所有硬件都支持;
● 必须将 configUSE_PORT_OPTIMISED_TASK_SELECTION 设置为 1;
● 依赖一个或多个特定架构的汇编指令(一般是类似计算前导零 [CLZ] 指令);
● 比通用方法更高效;
● 一般强制限定最大可用优先级数目为 32(0~31)。
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
/* 从就绪列表数组中找出最高优先级列表*/ \
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
}
https://blog.csdn.net/qq_27114397/article/details/83017512