任务管理(二)
多任务系统在同一时刻中,只有一个任务占有CPU的使用权。一个任务长期占有CPU会影响对系统的效应能力,故任务需要在空闲的时候,通过延时/挂起来让出CPU的使用权给其他任务,这样任务调度器才能调度其他任务运行。
一、任务延时 vTaskDelay
任务延时就是当前任务将CPU让出一定时间,将任务插入延时链表中,等到延时时间到达才将任务恢复到就绪状态,等待调度运行。
其实质就是将任务移动到延时链表/挂起链表。
注意进入延时函数时,会挂起调度器,这是为了防止在插入就绪链表过程中,由于中断产生打乱了代码逻辑
void vTaskDelay(const TickType_t xTicksToDelay)
{
BaseType_t xAlreadyYielded = pdFALSE;
if (xTicksToDelay > (TickType_t)0U)
{
configASSERT(uxSchedulerSuspended == 0);
//TODO 挂起调度器、防止中断导致任务插入就绪链表、扰乱代码逻辑
vTaskSuspendAll();
{
traceTASK_DELAY();
//当调度器挂起时,任务不能插入就绪表
//添加当前任务到延时链表
prvAddCurrentTaskToDelayedList(xTicksToDelay, pdFALSE);
}
//恢复调度器、若恢复调度器时、已经完成一次任务调度、则接下来不需要再产生一次任务调度
xAlreadyYielded = xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//若调度器恢复时未进行任务调度,则启动一次任务调度
if (xAlreadyYielded == pdFALSE)
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
static void prvAddCurrentTaskToDelayedList(TickType_t xTicksToWait,
const BaseType_t xCanBlockIndefinitely)
{
TickType_t xTimeToWake;
const TickType_t xConstTickCount = xTickCount; //保存当前时钟计数值为常量
//进入延时链表、设置标记
pxCurrentTCB->ucDelayAborted = pdFALSE;
//首先将当前任务移出就绪链表 若返回为0说明该优先级链表为空 需要重置最高优先级
if (uxListRemove(&(pxCurrentTCB->xStateListItem)) == (UBaseType_t)0)
{
portRESET_READY_PRIORITY(pxCurrentTCB->uxPriority, uxTopReadyPriority);
}
else
{
mtCOVERAGE_TEST_MARKER();
}
{
//若延时时间最大且无限期等待,则直接加入挂起链表 任务调度器不对其管理
if ((xTicksToWait == portMAX_DELAY) && (xCanBlockIndefinitely != pdFALSE))
{
vListInsertEnd(&xSuspendedTaskList, &(pxCurrentTCB->xStateListItem));
}
else //插入延时链表
{
//计算任务下次由时钟中断唤醒的时间戳 注意时钟溢出(当前时间+需要等待的时间=下次醒来的时间点)
xTimeToWake = xConstTickCount + xTicksToWait;
//将时间戳设置为任务的状态节点值
listSET_LIST_ITEM_VALUE(&(pxCurrentTCB->xStateListItem), xTimeToWake);
if (xTimeToWake < xConstTickCount)
{
//延时时间溢出、任务插入延时溢出链表
vListInsert(pxOverflowDelayedTaskList, &(pxCurrentTCB->xStateListItem));
}
else
{
//未溢出,插入当前延时链表
vListInsert(pxDelayedTaskList, &(pxCurrentTCB->xStateListItem));
//更新xNextTaskUnblockTime
if (xTimeToWake < xNextTaskUnblockTime)
{
xNextTaskUnblockTime = xTimeToWake;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}
/* Avoid compiler warning when INCLUDE_vTaskSuspend is not 1. */
(void)xCanBlockIndefinitely;
}
}
二、绝对任务延时 xTaskDelayUntil
另一种延时方式:延时一个任务直到指定的时间点到来。经常用于周期性的运行一个任务。
使用vTaskDelay()由于任务执行的时间是不确定的(可能被中断打断),故相对延时无法实现周期信运行一个任务。而使用绝对延时xTaskDelayUntil()能保证任务在指定的时间点唤醒,延时时间由上一次运行时的时间点与所需延时频率计算。
该函数不同之处在于计算延时时间、最后都同样的调用prvAddCurrentTaskToDelayedList函数。
BaseType_t xTaskDelayUntil(TickType_t *const pxPreviousWakeTime,
const TickType_t xTimeIncrement)
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
configASSERT(pxPreviousWakeTime);
configASSERT((xTimeIncrement > 0U));
configASSERT(uxSchedulerSuspended == 0);
//TODO 挂起调度器、防止中断导致任务插入就绪链表、扰乱代码逻辑
vTaskSuspendAll();
{
//保证xTickCount在本代码块内不会被修改
const TickType_t xConstTickCount = xTickCount;
//计算下次唤醒时间
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
if (xConstTickCount < *pxPreviousWakeTime)
{
// -----cur++++++pre----- 只有当xTimeToWake在+号时才能进入延时
if ((xTimeToWake < *pxPreviousWakeTime) && (xTimeToWake > xConstTickCount))
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
// +++++pre-----cur++++++ 只有当xTimeToWake在+号时才能进入延时
if ((xTimeToWake < *pxPreviousWakeTime) || (xTimeToWake > xConstTickCount))
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
//更新pxPreviousWakeTime
*pxPreviousWakeTime = xTimeToWake;
if (xShouldDelay != pdFALSE)
{
traceTASK_DELAY_UNTIL(xTimeToWake);
//prvAddCurrentTaskToDelayedList需要的是相对延时时间 故是xTimeToWake - xConstTickCount
prvAddCurrentTaskToDelayedList(xTimeToWake - xConstTickCount, pdFALSE);
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
//恢复调度器、执行任务调度
xAlreadyYielded = xTaskResumeAll();
if (xAlreadyYielded == pdFALSE)
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
return xShouldDelay;
}
三、终止延时
有开始、就会有结束。其他任务可唤醒处于延时中的函数。其实质就是将任务移动到就绪链表、并进行任务调度。
BaseType_t xTaskAbortDelay(TaskHandle_t xTask)
{
TCB_t *pxTCB = xTask;
BaseType_t xReturn;
configASSERT(pxTCB);
//挂起任务调度器
vTaskSuspendAll();
{
//任务的状态为阻塞时,才能解锁
if (eTaskGetState(xTask) == eBlocked)
{
xReturn = pdPASS;
//从延时链表中移除
//TODO 当调度器挂起时、中断不会修改任务的xStateListItem.所以在这里的操作是安全的
(void)uxListRemove(&(pxTCB->xStateListItem));
//从事件链表中移除 (中断可能修改xEventListItem 故需要进入临界区)
taskENTER_CRITICAL();
{
if (listLIST_ITEM_CONTAINER(&(pxTCB->xEventListItem)) != NULL)
{
(void)uxListRemove(&(pxTCB->xEventListItem));
//记录他已经解除挂起
pxTCB->ucDelayAborted = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
//退出延时后,进入就绪态(因为中断中不能访问就绪链表,故当前函数不能在中断中调用)
prvAddTaskToReadyList(pxTCB);
{
//解锁任务的优先级高于当前任务优先级,发生抢占
if (pxTCB->uxPriority > pxCurrentTCB->uxPriority)
{
//挂起一次任务调度(因为当前调度器挂起了)
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
xReturn = pdFAIL;
}
}
(void)xTaskResumeAll();
return xReturn;
}
四、任务挂起
将指定任务挂起、该任务不会被恢复运行,除非用户代码恢复该任务。其代码逻辑就是将任务移动到挂起链表、并判断是否需要任务调度。
void vTaskSuspend(TaskHandle_t xTaskToSuspend)
{
TCB_t *pxTCB;
//进入临界区
taskENTER_CRITICAL();
{
//获取任务tcb
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();
}
//将任务插入挂起链表
vListInsertEnd(&xSuspendedTaskList, &(pxTCB->xStateListItem));
}
taskEXIT_CRITICAL();
if (xSchedulerRunning != pdFALSE)
{
//更新下次任务解锁时间。刚刚挂起的任务可能是下个解锁的任务
taskENTER_CRITICAL();
{
prvResetNextTaskUnblockTime();
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//挂起任务是正在运行的任务
if (pxTCB == pxCurrentTCB)
{
if (xSchedulerRunning != pdFALSE)
{
//当前任务被挂起,执行一次任务调度
configASSERT(uxSchedulerSuspended == 0);
portYIELD_WITHIN_API();
}
else //todo 调度器关闭所以必须调整pxCurrentTCB
{
if (listCURRENT_LIST_LENGTH(&xSuspendedTaskList) == uxCurrentNumberOfTasks)
{
//所有任务都被挂起 pxCurrentTCB为null,下次创建新任务时,就执行新任务
pxCurrentTCB = NULL;
}
else
{
//更新pxCurrentTCB
vTaskSwitchContext();
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
五、恢复任务
挂起的任务只有得到恢复才能运行。以下是函数在任务函数中调用。
void vTaskResume(TaskHandle_t xTaskToResume)
{
TCB_t *const pxTCB = xTaskToResume;
configASSERT(xTaskToResume);
//恢复的任务不可能是null 并且 不可能是正在运行的任务
if ((pxTCB != pxCurrentTCB) && (pxTCB != NULL))
{
taskENTER_CRITICAL();
{
//判断是否真的在挂起链表中
if (prvTaskIsTaskSuspended(pxTCB) != pdFALSE)
{
traceTASK_RESUME(pxTCB);
//移出挂起链表、加入就绪链表
(void)uxListRemove(&(pxTCB->xStateListItem));
prvAddTaskToReadyList(pxTCB);
//恢复任务的优先级较高、进行任务调度
if (pxTCB->uxPriority >= pxCurrentTCB->uxPriority)
{
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
六、从中断中恢复任务
与上一次函数的区别是:
-
该函数只能在中断中调用。
-
在中断中进入临界区使用的函数也不一样。同时,还需要注意当调度器挂起时,不能在中断中直接修改就绪链表,而是先将任务缓存到xPendingReadyList,当调度器恢复时,再将任务移动到就绪链表。
-
在中断中不能进行任务调度,所以该函数的返回值表示是否需要在退出中断后进行任务调度
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume)
{
BaseType_t xYieldRequired = pdFALSE; //任务调度标记 由于中断中不能执行上下文切换、故使用他来代替上下文切换
TCB_t *const pxTCB = xTaskToResume;
UBaseType_t uxSavedInterruptStatus;
configASSERT(xTaskToResume);
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
//进入中断级临界区
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
//判断是否真的挂起
if (prvTaskIsTaskSuspended(pxTCB) != pdFALSE)
{
traceTASK_RESUME_FROM_ISR(pxTCB);
if (uxSchedulerSuspended == (UBaseType_t)pdFALSE)
{
//调度器运行中、若任务优先级更高、则产生任务调度
if (pxTCB->uxPriority >= pxCurrentTCB->uxPriority)
{
xYieldRequired = pdTRUE; //需要进行任务调度
xYieldPending = pdTRUE; //一个任务调度请求被挂起
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//将任务移动到就绪链表
(void)uxListRemove(&(pxTCB->xStateListItem));
prvAddTaskToReadyList(pxTCB);
}
else
{
//调度器挂起 不能在中断中直接修改就绪链表,先将任务缓存到xPendingReadyList
vListInsertEnd(&(xPendingReadyList), &(pxTCB->xEventListItem));
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR(uxSavedInterruptStatus);
return xYieldRequired;
}