任务切换点
FreeRTOS是用中断的方式,将完整的任务函数分拆成代码段。任务函数运行中断,被称做任务挂起。可能挂起任务的原因有两种:
- 同步和任务管理函数中,显式挂起当前任务。
- 当前时间片超时,挂起当前任务。
下面是一个完整的任务程序如何被切成的几个代码段的示例:
void Task1(...) // task function
{
for (;;) // task loop
{
// --- Slice 1 ---
...
// RR timeout (时间片超时可能发生在不同的位置)
// --- Slice 2 ---
...
vTaskDelay(...) // task suspend
// --- Slice 3 ---
...
xSemaphoreTake(...) // 如果信号量被锁住,任务被挂起,此处代码段被分割
// --- Slice 4 ---
...
...
// RR timeout (时间片超时可能发生在不同的位置)
// --- Slice 5 ---
...
xSemaphoreGive(...) // 信号量恢复不会导致任务挂起
...
// Slice 5结束后,如果没有RR中断,会合并Slice 1继续运行。
}
}
注意:并不是上述所有点都会发生中断。时间片超时中断可能在任意点发生。同步函数则需要判断同步条件来决定是否挂起任务。
时间片超时
如果启用了时间片轮转(configUSE_TIME_SLICING == 1
),FreeRTOS会在定时器中断中判断当前时间片是否超时,来决定是否需要挂起正在运行的任务。
例如在ARM CM4中, SysTick
中断中有如下处理:
void xPortSysTickHandler( void )
{
uint32_t ulDummy;
ulDummy = portSET_INTERRUPT_MASK_FROM_ISR();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* Pend a context switch. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( ulDummy );
}
SysTick
是ARM系统最基本的时钟中断,通常被设置成1ms一次。由于它可以用来做一些和定时器相关的操作,它的优先级一般设成比较低,防止一些循环任务和其他设备中断发生逻辑冲突,照成某种程度上的死循环。
FreeRTOS在SysTick
中断中,通过xTaskIncrementTick
函数来判断是否需要进行任务切换。如果需要进行任务切换,就通过硬件寄存器的设置,触发PendSV
中断。任务切换就发生在PendSV
的中断处理函数中。
任务挂起中断
在同步函数或者任务管理函数中,如果当前任务需要被挂起(比如同步锁被锁住,vTaskSuspend
,vTaskDelay
被调用等),挂起函数同样会触发一个软中断,在中断处理函数中进行切换任务。
譬如vTaskDelay
函数的最后:
/* Force a reschedule if xTaskResumeAll has not already done so, we may
* have put ourselves to sleep. */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
再看vTaskSuspend
函数中也有类似代码:
if( pxTCB == pxCurrentTCB )
{
if( xSchedulerRunning != pdFALSE )
{
/* The current task has just been suspended. */
configASSERT( uxSchedulerSuspended == 0 );
portYIELD_WITHIN_API();
}
else
{
// ...
}
}
而portYIELD_WITHIN_API
是一个需要移植的函数,需要触发任务挂起中断。在ARM CM4中,它被定义成:
#define portYIELD_WITHIN_API() \
{ \
/* 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. */ \
__asm volatile ( "dsb" ::: "memory" ); \
__asm volatile ( "isb" ); \
}
可以看到,其中也是触发了PendSV
中断。dsb
和isb
保证中断前后的数据和指令被完整隔离(清空微指令流水线)。
PendSV vs SVC
ARM中有两个中断都可以实现软中断的效果,就是PendSV
和SVC
。两者的区别在于优先级。SVC
是无条件即时执行的,有点像错误处理。PendSV
则是受中断器件管理,遵循屏蔽码和优先级的设置。
portYIELD_WITHIN_API
大都被放在了临界区中,所以PendSV
最早会在临界区结束的时候被触发。FreeRTOS为此保留了一些处理其他动作的机会,比如上述宏定义中的dsb
,isb
,用于清空代码和数据流水线,之后再进行任务切换,保证上下文被完全隔离。
同样在SysTick
中断中可以看到,触发PendSV
前后也有开关中断的动作,类似一个临界区。PendSV
会在开中断后才会执行。
FreeRTOS只有在即第一个任务开始运行时,会立即触发SVC
中断。彼时没有其他任务需要执行,也不需要保存前一个任务的上下文。因此FreeRTOS采用了一个不同的中断,用不同的中断处理函数来避免在PendSV
中多做一些判断操作。
PendSV
和SVC
的中断处理过程在后文中有详细描述。
临界区
FreeRTOS的任务切换是由软中断实现的,因此临界区需要阻止中断的发生。考虑到设备中断中也会有任务切换的需求,临界区一般会关掉所有的中断。参考下列ARM CM3的实现:
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
// ...
}
/*-----------------------------------------------------------*/
void vPortExitCritical( void )
{
// ...
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
portDISABLE_INTERRUPTS
和portENABLE_INTERRUPTS
会开关所有的中断,包括PendSV
在临界区中无法被触发,也就不会切换任务。