RTOS系统的核心是任务管理,而任务管理的核心是任务切换。
PendSV异常
PendSV(可挂起的系统调用)异常对OS操作非常重要,其优先级可以通过编程设置。可以通过将中断控制器的状态寄存器ICSR的bit28,也就是PendSV的挂起位置1来触发PendSV中断。与SVC异常不同,他是不精确的,因此它的挂起状态可在更高优先级处理内设置,且会在高优先级处理完后执行。
利用该特性,若将PendSV设置为最低的异常优先级,可以让PendSV异常处理在所有其他中断处理完后执行,对于上下文切换非常有用,也是各中OS设计中的关键。
在具有嵌入式OS的典型系统中,处理器时间被分割成了多个时间片。若系统中有两个任务,这两个任务会交替执行。
上下文被触发的场合可以是:
1、执行一个系统调用
2、系统滴答定时器(systick)中断。
在OS中,任务调度器决定是否执行上下文切换。如果IRQ(中断请求)在systick前产生,则systick可能会抢占IRQ处理,这种情况下,OS不应该执行上下文切换,否则中断会被延迟处理。对于有些硬件平台,如果有活跃的中断服务,OS试图返回线程模式,会触发异常。
在OS设计中,要解决这个问题,可以在运行中断服务时不执行上下文切换,此时检查栈帧中的压栈xPSR或者NVIC中的中断活跃寄存器。不过系统的性能或受到影响,特别是当中断源在systick前后持续产生请求时,这样上下文切换就没有执行时间了。
要解决这个问题,PendSV异常将上下文切换请求延迟到其他IRQ处理完成后,此时需要将PendSV优先级设置为最低。若OS需要执行上下文切换,他会设置PendSV的挂起状态,并在PendSV异常内执行上下文切换。
- 任务A呼叫SVC来请求任务切换。比如主动sleep。
- OS收到请求后,做好上下文切换准备,并且pend一个pendsv中断。
- 当cpu退出svc后,它立刻进入pensv,从而执行上下文切换。
- 当pendsv执行完成后,将返回任务B,同时进入线程模式。
- 发生一个中断,并且中断开始运行。
- 在中断允许过程中,发生systick中断,抢占了ISR。
- OS做必要的准备,然后pend一个pendsv异常。
- systick退出后,继续执行ISR。
- ISR执行完毕后,pendsv开始执行,并且在里面执行上下文切换。
- 当pensv执行完毕后,回到任务A,同时进入线程模式。
- FreeRTOS系统的任务切换最终都是在pendsv中断服务里完成的。
任务切换场合
- 执行一个系统调用。
- 系统滴答定时器systick中断。
系统调用
执行系统调用就是执行FreeRTOS系统提供的相关API函数,比如任务切换taskYIELD,FreeRTOS有些API函数也会调用taskYIELD()这些函数也会导致任务切换,这些函数都称之为系统调用。
任务切换就是一个宏。
/* Scheduler utilities. */
#define portYIELD() \
{ \
/* Set a PendSV to request a context switch. */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
/* Barriers are normally not required but do ensure the code is completely \
* within the specified behaviour for the architecture. */ \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
systick
systick也会进行任务切换。触发pendsv中断。
void xPortSysTickHandler( void )
{
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
* executes all interrupts must be unmasked. There is therefore no need to
* save and then restore the interrupt mask value as its value is already
* known. */
portDISABLE_INTERRUPTS();
traceISR_ENTER();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
traceISR_EXIT_TO_SCHEDULER();
/* A context switch is required. Context switching is performed in
* the PendSV interrupt. Pend the PendSV interrupt. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
else
{
traceISR_EXIT();
}
}
portENABLE_INTERRUPTS();
}
PendSV中断服务程序
前面说了FreeRTOS任务切换的具体过程是在PendSV中断服务函数里完成的,看下任务切换过程究竟是怎么进行的。
void xPortPendSVHandler( void )
{
/* This is a naked function. */
__asm volatile
(
" .syntax unified \n"
//读取进程指针,保存在寄存器R0里面
" mrs r0, psp \n"
" \n"
//获取当前任务的任务控制块,并将任务控制块地址保存在寄存器R2中
" ldr r3, pxCurrentTCBConst \n" /* Get the location of the current TCB. */
" ldr r2, [r3] \n"
" \n"
" subs r0, r0, #32 \n" /* Make space for the remaining low registers. */
//将寄存器R0的值写入到寄存器R2所保存的地址中去,也就是将新的栈顶保存在任务控制块
//的第一个字段中。此时R0保存着最新的堆栈栈顶指针,所以要将这个最新值写入到任务控制块的第一个字段,而上面已经获得了任务控制块的首地址写入到了R2中。
" str r0, [r2] \n" /* Save the new top of stack. */
//保存r4和r14这几个寄存器的值
" stmia r0!, {r4-r7} \n" /* Store the low registers that are not saved automatically. */
" mov r4, r8 \n" /* Store the high registers. */
//保存r4~r11
" mov r5, r9 \n"
" mov r6, r10 \n"
" mov r7, r11 \n"
" stmia r0!, {r4-r7} \n"
" \n"
//将寄存器r3的值临时压栈,寄存器r3保存了当前任务控制块,而接下来调用函数vTaskSwitchContext,为了防止r3被串改,需要临时保存
" push {r3, r14} \n"
" cpsid i \n"
//获取下一个要运行的任务,并将pxCurrentTCB更新为这个要运行的任务。
" bl vTaskSwitchContext \n"
" cpsie i \n"
" pop {r2, r3} \n" /* lr goes in r3. r2 now holds tcb pointer. */
" \n"
" ldr r1, [r2] \n"
//获取新的要运行的任务的任务堆栈栈顶,并将栈顶保存在r0中。
" ldr r0, [r1] \n" /* The first item in pxCurrentTCB is the task top of stack. */
" adds r0, r0, #16 \n" /* Move to the high registers. */
//r4~r11出栈,也就是即将运行的任务现场的恢复
" ldmia r0!, {r4-r7} \n" /* Pop the high registers. */
" mov r8, r4 \n"
" mov r9, r5 \n"
" mov r10, r6 \n"
" mov r11, r7 \n"
" \n"
//更新进程指针psp
" msr psp, r0 \n" /* Remember the new top of stack for the task. */
" \n"
" subs r0, r0, #32 \n" /* Go back for the low registers that are not automatically restored. */
" ldmia r0!, {r4-r7} \n" /* Pop low registers. */
" \n"
//执行此代码后硬件自动恢复寄存器r0~r3 r12 lr pc xPSR 任务切换完成
" bx r3 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst: .word pxCurrentTCB "
);
}
vTaskSwitchContext
在pendsv里调用函数获取下一个要运行的任务,也就是查找已经就绪的最高优先级任务。
void vTaskSwitchContext( void )
{
traceENTER_vTaskSwitchContext();
if( uxSchedulerSuspended != ( UBaseType_t ) 0U )
{
/* The scheduler is currently suspended - do not allow a context
* switch. */
//调度器挂起不允许任务切换
xYieldPendings[ 0 ] = pdTRUE;
}
else
{
xYieldPendings[ 0 ] = pdFALSE;
traceTASK_SWITCHED_OUT();
/* 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(); /*lint !e9079 void * is used as this macro is used with timers too. Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
traceTASK_SWITCHED_IN();
/* After the new task is switched in, update the global errno. */
#if ( configUSE_POSIX_ERRNO == 1 )
{
FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
}
#endif
#if ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 )
{
/* Switch C-Runtime's TLS Block to point to the TLS
* Block specific to this task. */
configSET_TLS_BLOCK( pxCurrentTCB->xTLSBlock );
}
#endif
}
traceRETURN_vTaskSwitchContext();
}
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
do { \
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 ] ) ); \
} while( 0 )
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) _BitScanReverse( ( DWORD * ) &( uxTopPriority ), ( uxReadyPriorities ) )
uxTopReadyPriority不代表处于就绪态的最高优先级了,而是使用每个bit代表一个优先级,bit0代表优先级0,bit31就代表优先级31。当某个优先级有就绪任务时就将其对应的bit置1。计算前导零个数,前导零个数就是计算uxReadyPriority的前导零个数,前导零个数是从bit31到第一个为1的bit,期间为0的个数。
每个bit对应一个list,每个list里有多个列表项(task)。如下图:
例如二进制:
1000 0000 0000 0000,的前导零个数是0
0000 1001 1111 0001,的前导零个数是4.
得到前导零个数之后,再用31减去这个数就得到处理最高优先级了。
已经找到处于就绪态的最高优先级了,接下来就是从对应的列表里找出下一个要运行的任务,查找方法就是使用listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列表项,然后赋值给pxCurrentTCB。这样就确定下一个要运行的任务了。
时间片调度
如果存在多个优先级相同的task。freertos支持相同优先级时间片轮询调度。如果有相同优先级的多个task,那么也需要进行任务切换。从list里一直获取列表项就可以。
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
#if ( configNUMBER_OF_CORES == 1 )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}