FreeRTOS任务调度研究

这篇文章不介绍FreeRTOS移植,只是最近针对多核ARM Cortex系列平台做了移植后的一篇总结研究文章。所以不涉及对FreeRTOS整体的介绍,而只是分析任务调度这一块的机制。对应的Demo参考自CORTEX_A9_Zynq_ZC702。

一、触发任务调度的两种机制

1、taskYIELD()
这种机制其实是通过ARM swi 异常触发task context switch。

2、IRQ handler
通过tick中断,来检测是否需要进行任务切换。
最终两者任务切换的方式过程都是一样的,以taskYIELD()为例,代码如下:
/******************************************************************************
* SVC handler is used to start the scheduler.
*****************************************************************************/
.align 4
.type FreeRTOS_SWI_Handler, %function
FreeRTOS_SWI_Handler:
/* Save the context of the current task and select a new task to run. */
portSAVE_CONTEXT
LDR R0, vTaskSwitchContextConst
BLX R0
portRESTORE_CONTEXT

ps.这是一个weak函数,可以自行重定义。

二、任务调度过程

总的来说,任务调度包含三个部分:保存当前任务现场;选择下一个执行的任务;恢复任务现场。

FreeRTOS保护的现场都通过system模式进行访问,针对每个任务,在其创建的时候都会在栈内开辟一段固定大小的空间用来保存之前任务的上下文(包括CPU状态,运行栈,内部寄存器)。

为什么是system mode? 因为system mode和usr mode看到的是一份寄存器内容。如下图所示

能反应usr模式下的运行状态,又不会影响其他特权模式。

而且system mode会进到PL1 state

2.1 恢复现场(通常也是触发执行第一个task的方式)
.macro portRESTORE_CONTEXT

/* Set the SP to point to the stack of the task being restored. */
LDR R0, pxCurrentTCBConst // 当前要恢复的任务对应的TCB
LDR R1, [R0]
LDR SP, [R1] // TCB的第一个成员(pxTopOfStack),创建任务的时候根据A系列上下文内容计算得到,恢复的时候通过这个SP来发现保存的上下文,注意:这个时候是system mode。

// 下面就是需要恢复的上下文,和创建任务时保存的顺序相反(push/pop)。
/* Is there a floating point context to restore? If the restored
ulPortTaskHasFPUContext is zero then no. */
LDR R0, ulPortTaskHasFPUContextConst
POP {R1}
STR R1, [R0]
CMP R1, #0

/* Restore the floating point context, if any. */
POPNE {R0}
VPOPNE {D16-D31}
VPOPNE {D0-D15}
VMSRNE FPSCR, R0

/* Restore the critical section nesting depth. */
LDR R0, ulCriticalNestingConst
POP {R1}
STR R1, [R0]

/* Ensure the priority mask is correct for the critical nesting depth. */
LDR R2, ulICCPMRConst
LDR R2, [R2]
CMP R1, #0
MOVEQ R4, #255
LDRNE R4, ulMaxAPIPriorityMaskConst
LDRNE R4, [R4]
STR R4, [R2]

/* Restore all system mode registers other than the SP (which is already
being used). */
POP {R0-R12, R14}

/* Return to the task code, loading CPSR on the way. */
RFEIA sp! // 执行完这条指定之后system mode下的sp重新指向了真正的栈顶,然后CPU回到任务lr指定的位置(初始值为任务对应的函数入口)。

.endm

在创建完任务,调用vTaskStartScheduler()之后就是通过这个宏进到第一个任务的。

2.2 保存现场

和恢复现场相比较,保存现场就是要把各种状态放到SP(pxTopOfStack)对应的地方上。代码如下:
.macro portSAVE_CONTEXT

/* Save the LR and SPSR onto the system mode stack before switching to
system mode to save the remaining system mode registers. */
SRSDB sp!, #SYS_MODE // 先把lr和spsr入栈到system mode的栈中
CPS #SYS_MODE // 切到system mode
PUSH {R0-R12, R14} // 和恢复现场的顺序对应,寄存器入栈

/* Push the critical nesting count. */
LDR R2, ulCriticalNestingConst
LDR R1, [R2]
PUSH {R1}

/* Does the task have a floating point context that needs saving? If
ulPortTaskHasFPUContext is 0 then no. */
LDR R2, ulPortTaskHasFPUContextConst
LDR R3, [R2]
CMP R3, #0

/* Save the floating point context, if any. */
FMRXNE R1, FPSCR
VPUSHNE {D0-D15}
VPUSHNE {D16-D31}
PUSHNE {R1}

/* Save ulPortTaskHasFPUContext itself. */
PUSH {R3}

/* Save the stack pointer in the TCB. */
LDR R0, pxCurrentTCBConst
LDR R1, [R0]
STR SP, [R1] // 这个时候的sp还是system mode下的sp,所以上下文内容都是在system mode下进行保存和切换的。

.endm

将save context和restore context结合起来看,任务上下文的切换始终发生在system mode,即能改变user mode下的运行栈,又不会破坏其他特权级别(诸如svc,irq,fiq,hyp)下的运行栈。是一个实质上介于特权级别和用户级别的一个中间层级。

2.3 任务切换

任务切换方法由vTaskSwitchContext提供,负责从当前的ready list中选择一个优先级最高的任务。保存到pxCurrentTCB中。

至此,整个FreeRTOS的调度程序就工作起来了。

三、Tick Handler

前面讲到,触发FreeRTOS调度的有两种机制,一是swi handler,二是irq handler,准确的讲是tick handler。

一般针对每一个平台的port,都会实现一个对应的FreeRTOS_Tick_Handler,以Cortex A9为例,其位于Source/portable/GCC/ARM_CA9/port.c中。

void FreeRTOS_Tick_Handler( void )
{
/* Set interrupt mask before altering scheduler structures. The tick
handler runs at the lowest priority, so interrupts cannot already be masked,
so there is no need to save and restore the current mask value. It is
necessary to turn off interrupts in the CPU itself while the ICCPMR is being
updated. */
portCPU_IRQ_DISABLE();
portICCPMR_PRIORITY_MASK_REGISTER = ( uint32_t ) ( configMAX_API_CALL_INTERRUPT_PRIORITY << portPRIORITY_SHIFT );
__asm volatile ( "dsb \n"
"isb \n" );
portCPU_IRQ_ENABLE();

/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
ulPortYieldRequired = pdTRUE;
}

/* Ensure all interrupt priorities are active again. */
portCLEAR_INTERRUPT_MASK();
configCLEAR_TICK_INTERRUPT();
}

里面主要就是一个函数:xTaskIncrementTick()。这个函数位于Source/tasks.c。
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;

/* Called by the portable layer each time a tick interrupt occurs.
Increments the tick then checks to see if the new tick value will cause any
tasks to be unblocked. */
traceTASK_INCREMENT_TICK( xTickCount );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
/* Minor optimisation. The tick count cannot change in this
block. */
const TickType_t xConstTickCount = xTickCount + 1;

/* Increment the RTOS tick, switching the delayed and overflowed
delayed lists if it wraps to 0. */
xTickCount = xConstTickCount;

if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}

/* See if this tick has made a timeout expire. Tasks are stored in
the queue in the order of their wake time - meaning once one task
has been found whose block time has not expired there is no need to
look any further down the list. */
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* The delayed list is empty. Set xNextTaskUnblockTime
to the maximum possible value so it is extremely
unlikely that the
if( xTickCount >= xNextTaskUnblockTime ) test will pass
next time through. */
xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
break;
}
else
{
/* The delayed list is not empty, get the value of the
item at the head of the delayed list. This is the time
at which the task at the head of the delayed list must
be removed from the Blocked state. */
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );

if( xConstTickCount < xItemValue )
{
/* It is not time to unblock this item yet, but the
item value is the time at which the task at the head
of the blocked list must be removed from the Blocked
state - so record the item value in
xNextTaskUnblockTime. */
xNextTaskUnblockTime = xItemValue;
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}

/* It is time to remove the item from the Blocked state. */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );

/* Is the task waiting on an event also? If so remove
it from the event list. */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}

/* Place the unblocked task into the appropriate ready
list. */
prvAddTaskToReadyList( pxTCB );

/* A task being unblocked cannot cause an immediate
context switch if preemption is turned off. */
#if ( configUSE_PREEMPTION == 1 )
{
/* Preemption is on, but a context switch should
only be performed if the unblocked task has a
priority that is equal to or higher than the
currently executing task. */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
}
}

/* Tasks of equal priority to the currently running task will share
processing time (time slice) if preemption is on, and the application
writer has not explicitly turned time slicing off. */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */

#if ( configUSE_TICK_HOOK == 1 )
{
/* Guard against the tick hook being called when the pended tick
count is being unwound (when the scheduler is being unlocked). */
if( uxPendedTicks == ( UBaseType_t ) 0U )
{
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICK_HOOK */
}
else
{
++uxPendedTicks;

/* The tick hook gets called at regular intervals, even if the
scheduler is locked. */
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}

#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */

return xSwitchRequired;
}

总的来讲,xTaskIncrementTick负责更新block list和ready list。然后设置标志位xSwitchRequired,告诉调度器是否需要进行任务切换。

整个函数分为两个分支: uxSchedulerSuspended == ( UBaseType_t ) pdFALSE 和 uxSchedulerSuspended != ( UBaseType_t ) pdFALSE 。

如果此时scheduler已经挂起的话,什么都不做。否则,做如下动作:

判断当前tick是否大于xNextTaskUnblockTime(初始值为0),如果大于,则需要进一步判断pxDelayedTaskList中的哪些task需要进入到readyList。

取出pxDelayedTaskList中的最小unblock time(xItemValue)的任务,如果当前tick小于该最小值(表示没有任务满足unblock需求),那么设置xNextTaskUnblockTime为xItemValue,然后退出。

否则的话,通过( void ) uxListRemove( &( pxTCB->xStateListItem ) );将该task从block状态移除。紧接着判断是不是在等待event,如果是的话,也从event list移除。

把该任务加到readyList中去。等待调度。判断是否支持preemption,如果支持的话,置xSwitchRequired。

文章转载自:RickeyQ的博客