FreeRTOS挂起和恢复任务相关函数解析
以下FreeRTOS源码函数使用的版本是9.0.0,不同版本的源码会有部分不同如10.4.6,注意甑别。
挂起和恢复任务相关 API 函数有以下几个:
函数 vTaskSuspend()
此函数用于挂起任务,若使用此函数,需要在 FreeRTOSConfig.h 文件中将宏INCLUDE_vTaskSuspend 配置为 1。无论优先级如何,被挂起的任务都将不再被执行,直到任务被恢复。此函数不支持嵌套,不论使用此函数重复挂起任务多少次,只需调用一次恢复任务的函数,那么任务就不再被挂起。传递空句柄将导致调用任务被挂起。
函数如下:
void vTaskSuspend(TaskHandle_t xTaskToSuspend)
{
TCB_t *pxTCB;
taskENTER_CRITICAL();
{
/* 获取要挂起的任务的任务控制块,传入 NULL 则挂起任务自身 */
pxTCB = prvGetTCBFromHandle(xTaskToSuspend);
traceTASK_SUSPEND(pxTCB);
/* 将任务从任务所在任务状态列表(就绪态任务列表或阻塞态任务列表)中移除 */
if (uxListRemove(&(pxTCB->xStateListItem)) == (UBaseType_t)0)
{
/* 成功移除掉了那就重新设置任务对应优先级的就绪位图,以确保其他任务能够正确地继续运行 */
taskRESET_READY_PRIORITY(pxTCB->uxPriority);
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 这个任务是否在等待某个事件,如果是,那就移除所在事件列表项,因为要挂起该任务了 */
if (listLIST_ITEM_CONTAINER(&(pxTCB->xEventListItem)) != NULL)
{
(void)uxListRemove(&(pxTCB->xEventListItem));
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 将TCB的状态列表项插入挂起列表项末尾,实现挂起任务 */
vListInsertEnd(&xSuspendedTaskList, &(pxTCB->xStateListItem));
}
taskEXIT_CRITICAL();
/* 调度器在运行,则需要更新下一个任务的阻塞超时时间 */
if (xSchedulerRunning != pdFALSE)
{
/* Reset the next expected unblock time in case it referred to the
task that is now in the Suspended state. */
taskENTER_CRITICAL();
{
/* 更新下个任务解除阻塞时间 */
prvResetNextTaskUnblockTime();
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if (pxTCB == pxCurrentTCB)
{
/* 如果要挂起的是自己,且调度器在运行,那么表示当前任务自身被挂起,需要进行任务调度 */
if (xSchedulerRunning != pdFALSE)
{
/* The current task has just been suspended. */
/* 首先进行断言验证确保调度器没有被挂起 */
configASSERT(uxSchedulerSuspended == 0);
/* 调用 portYIELD_WITHIN_API 函数主动触发任务切换,使其他就绪任务有机会被调度执行 */
portYIELD_WITHIN_API();
/* 主要用于任务级别的 API 函数内部,用于特定情况下主动触发任务切换 */
}
/* 调度器没运行 */
else
{
/* 调度器没有运行,但是 pxCurrentTCB 所指向的任务刚刚挂起,
必须调整 pxCurrentTCB 以指向不同的任务
当任务被挂起后,如果没有其他就绪任务,即挂起列表中的任务数等于当前任务数 */
if (listCURRENT_LIST_LENGTH(&xSuspendedTaskList) == uxCurrentNumberOfTasks)
{
/* 没有就绪的任务,将 pxCurrentTCB 设置为 NULL。这样,在下次创建任务时,
无论其相对优先级如何,pxCurrentTCB 都会被设置为指向新任务*/
pxCurrentTCB = NULL;
}
else
{
/* 调用函数进行任务切换。这将导致上下文切换,并将执行权交给另一个就绪任务 */
vTaskSwitchContext();
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
1.该函数先获取了待挂起的任务的任务控制块,以便进行列表项移除、添加操作。
2.将待挂起任务从所在的状态列表中移除,如果移除后列表项数量为0则需要更新任务优先级记录。
3.将任务插入至挂起任务列表当中。
4.更新下一个任务解除阻塞的时间,以防被挂起的任务就是下一个阻塞超时的任务。
5.如果要挂起的是自身,并且调度器在运行中,那就需要要求调度器进行任务调度。如果调度器没有在运行,就需要调整当前任务控制块的指向。更新 pxCurrentTCB 的操作,是通过调用函数 vTaskSwitchContext()实现的。
函数 vTaskSwitchContext()
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. */
/* 此函数用于将 pxCurrentTCB 更新为指向优先级最高的就绪态任务 */
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 */
}
}
此函数的重点在于调用了函数 taskSELETE_HIGHEST_PRIORITY_TASK() 更新pxCurrentTCB 指向优先级最高的就绪态任务,函数 taskSELETE_HIGHEST_PRIORITY_TASK()实际上是一个宏定义,task.c 文件中有定义。
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
/* Find the highest priority list that contains ready tasks. */ \
/* 查找就绪态任务列表中最高的任务优先级 */ \
portGET_HIGHEST_PRIORITY(uxTopPriority, uxTopReadyPriority); \
/* 此任务优先级不能是最低的任务优先级 */ \
configASSERT(listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[uxTopPriority])) > 0); \
/* 让 pxCurrentTCB 指向该任务优先级就绪态任务列表中的任务 */ \
listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[uxTopPriority])); \
} /* taskSELECT_HIGHEST_PRIORITY_TASK() */
以上便是任务挂起函数的执行流程,如果挂起的是其他任务,就将其他任务添加至挂起列表当中,如果挂起的是自身,就需要触发一次任务调度进行任务切换。注意:任务挂起后只能由其他任务进行恢复,如果一直不恢复任务将无法得到执行,如果其他任务也进行了挂起,那将永远无法恢复。
函数 vTaskResume()
此函数用于在任务中恢复被挂起的任务,若使用此函数,需要在 FreeRTOSConfig.h 文件中将宏 INCLUDE_vTaskSuspend 配置为 1。不论一个任务被函数 vTaskSuspend()挂起多少次,只需要使用函数 vTakResume()恢复一次,就可以继续运行。
void vTaskResume(TaskHandle_t xTaskToResume)
{
TCB_t *const pxTCB = (TCB_t *)xTaskToResume;
/* It does not make sense to resume the calling task. */
configASSERT(xTaskToResume);
/* The parameter cannot be NULL as it is impossible to resume the
currently executing task. */
/* 恢复的任务不能是自己,恢复的是被挂起的任务 */
if ((pxTCB != NULL) && (pxTCB != pxCurrentTCB))
{
taskENTER_CRITICAL();
{
/* 判断恢复的任务是否被挂起,只有被挂起才会恢复,即返回的是pdTRUE */
if (prvTaskIsTaskSuspended(pxTCB) != pdFALSE)
{
traceTASK_RESUME(pxTCB);
/* As we are in a critical section we can access the ready
lists even if the scheduler is suspended.
由于我们处于临界区,即使调度器被挂起,我们也可以访问就绪列表 */
/* 将任务从挂起状态列表中移除,并添加到就绪列表中 */
(void)uxListRemove(&(pxTCB->xStateListItem));
prvAddTaskToReadyList(pxTCB);
/* We may have just resumed a higher priority task. */
/* 任务优先级大于等于当前任务优先级就进行任务切换 */
if (pxTCB->uxPriority >= pxCurrentTCB->uxPriority)
{
/* This yield may not cause the task just resumed to run,
but will leave the lists in the correct state for the
next yield. */
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
该函数很简单,首先判断恢复的任务是不是任务自身,任务自身都在运行着了,谈何恢复?随后判断待恢复的任务是不是正在被挂起的,被挂起的才会进行恢复。将待恢复任务从挂起列表当中移除,并添加至就绪列表等待调度器调度,如果恢复的任务优先级大于当前正在运行的任务的优先级则需要进行任务切换。
函数 xTaskResumeFromISR()
此函数用于在中断中恢复被挂起的任务,若使用此函数,需要在 FreeRTOSConfig.h 文件中将宏 INCLUDE_xTaskResumeFromISR 配置为 1。不论一个任务被函数 vTaskSuspend()挂起多少次,只需要使用函数 vTakResumeFromISR()恢复一次,就可以继续运行。
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume)
使用该函数需要判断返回值,根据返回值进行任务切换。函数 xTaskResumeFromISR()的返回值,如下所示:
FreeRTOS官方:xTaskResumeFromISR()通常被视为危险函数,因为其操作未被锁定。因此,如果中断可能在任务被挂起之前到达,从而中断丢失,则绝对不应使用该函数来同步任务与中断。可使用信号量,或者最好是直达任务通知,来避免这种可能性。
这也就是说如果在中断中不对任务状态进行判断是否处于挂起态就调用从中断中恢复的函数,那么任务在还没挂起的时候就被恢复,这将导致恢复丢失,所以不应该用该函数来进行任务同步,推荐使用信号量、任务通知来进行。
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume)
{
/* xYieldRequired返回的值指示是否需要进行上下文切换 */
BaseType_t xYieldRequired = pdFALSE;
TCB_t *const pxTCB = (TCB_t *)xTaskToResume;
/* 保存中断当前状态 */
UBaseType_t uxSavedInterruptStatus;
configASSERT(xTaskToResume);
/* RTOS ports that support interrupt nesting have the concept of a
maximum system call (or maximum API call) interrupt priority.
Interrupts that are above the maximum system call priority are keep
permanently enabled, even when the RTOS kernel is in a critical section,
but cannot make any calls to FreeRTOS API functions. If configASSERT()
is defined in FreeRTOSConfig.h then
portASSERT_IF_INTERRUPT_PRIORITY_INVALID() will result in an assertion
failure if a FreeRTOS API function is called from an interrupt that has
been assigned a priority above the configured maximum system call
priority. Only FreeRTOS functions that end in FromISR can be called
from interrupts that have been assigned a priority at or (logically)
below the maximum system call interrupt priority. FreeRTOS maintains a
separate interrupt safe API to ensure interrupt entry is as fast and as
simple as possible. More information (albeit Cortex-M specific) is
provided on the following link:
http://www.freertos.org/RTOS-Cortex-M3-M4.html
由于FreeRTOS有中断屏蔽的概念,但高于某个值的中断优先级无法屏蔽。如果定义了configASSERT(),
那么如果一个API函数在一个不在FreeRTOS管理范围之内的中断服务例程中被调用,将导致断言失败。
只有以FromISR结尾的API函数可以从FreeRTOS可管理的中断中进行调用,不受它管理的调用也会有问题 */
/* 验证中断的优先级设置是否符合 FreeRTOS 的要求,是否在管理范围内 */
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
/* 保存中断当前状态 */
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
/* 判断任务是否被挂起 */
if (prvTaskIsTaskSuspended(pxTCB) != pdFALSE)
{
traceTASK_RESUME_FROM_ISR(pxTCB);
/* Check the ready lists can be accessed. */
/* 任务调度器没被挂起,在运行 */
if (uxSchedulerSuspended == (UBaseType_t)pdFALSE)
{
/* Ready lists can be accessed so move the task from the
suspended list to the ready list directly.
任务调度器没被挂起,可以访问就绪列表,因此可以直接将任务从挂起列表移动到就绪列表 */
if (pxTCB->uxPriority >= pxCurrentTCB->uxPriority)
{
xYieldRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
(void)uxListRemove(&(pxTCB->xStateListItem));
prvAddTaskToReadyList(pxTCB);
}
/* 调度器没运行 */
else
{
/* The delayed or ready lists cannot be accessed so the task
is held in the pending ready list until the scheduler is
unsuspended.
延迟列表或就绪列表无法访问,因此任务将保持在挂起的就绪列表中,直到调度器解除挂起
*/
/* 因为调度器被挂起了,所以就将被恢复的任务插入到等待就绪列表当中
直到调度器被恢复才执行任务的处理,添加到就绪列表当中 */
vListInsertEnd(&(xPendingReadyList), &(pxTCB->xEventListItem));
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 使用此宏函数可以在中断服务程序中恢复中断屏蔽,以允许其他具有较高优先级的中断被处理 */
portCLEAR_INTERRUPT_MASK_FROM_ISR(uxSavedInterruptStatus);
return xYieldRequired;
}
该函数的逻辑与普通恢复函数差不多,只不过是在中断中运行,判断任务是否被挂起,判断调度器是否被挂起,没被挂起则从挂起列表移除添加到就绪列表。调度器被挂起了则先将该任务添加到等待就绪列表当中,待到调度器恢复之后将这些任务添加到就绪列表中等待执行。
该函数还有官方的一大串描述,大概说的是FreeRTOS某些函数在调用的时候会屏蔽中断,以达到临界区的访问执行,但FreeRTOS屏蔽的中断范围是由一个阈值的,如果在一个中断屏蔽的阈值之外的中断服务例程中调用API函数这通常会触发断言,只有以FromISR结尾的API函数可以从FreeRTOS管理的中断中进行调用。