目录
3.2 任务从中断中恢复函数 xTaskResumeFromISR
这里主要介绍任务的几个状态,以及操作任务的一些函数(任务挂起函数,任务恢复函数,及任务删除函数),这里不涉及“事件”的一些东西,所以下面将所有关于“事件”的源码都注释了;
1.任务状态
1.就绪态:创建任务后,此任务是默认在就绪列表中的,在就绪列表中的任务会按相应的优先级依次执行;(挂载在就绪列表中的任务即为"就绪态");
2.阻塞态:当任务调用阻塞函数/任务等待信号量(事件)时,其会被挂载到相应的延时列表中,此时它会从就绪列表中被移除(只有在就绪列表中的任务才有机会被调用);
3.运行态:正在运行的任务就处在运行态;
4.挂起态:调用vTaskSuspend任务挂起API函数后,任务就会处于挂起态,被挂起的任务会得不到CPU使用权,也不会参与调度;(与阻塞不同的点就是,挂起态的任务是对调度器不可见的,而阻塞态任务在每个TICK时间内,调度器都会在当前延时列表查询阻塞任务是否到期);
2.挂起
2.1任务挂起函数 vTaskSuspend
(调用此函数要设置相应宏)
#if ( INCLUDE_vTaskSuspend == 1 )
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
{
TCB_t *pxTCB;
taskENTER_CRITICAL();
{
pxTCB = prvGetTCBFromHandle( xTaskToSuspend );
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
// if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
// {
// ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
// }
vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) );
}
taskEXIT_CRITICAL();
if( xSchedulerRunning != pdFALSE )
{
taskENTER_CRITICAL();
{
prvResetNextTaskUnblockTime();
}
taskEXIT_CRITICAL();
}
if( pxTCB == pxCurrentTCB )
{
if( xSchedulerRunning != pdFALSE )
{
configASSERT( uxSchedulerSuspended == 0 );
portYIELD_WITHIN_API();
}
else
{
vTaskSwitchContext();
}
}
}
#endif
参数:将要被挂起任务的任务句柄
任务挂起函数具体做了以下操作:
1. 通过调用prvGetTCBFromHandle函数将传入的句柄给函数中的临时TCB指针,若传入的这个指针是NULL的,则返回当前运行任务的任务控制块给临时TCB指针;
2. 将当前任务从其所属的列表中移除(该列表可能是阻塞/就绪列表),若当前任务移除后其之前所属的链表下无节点了,此时就调用taskRESET_READY_PRIORITY来重置优先级:
(可以看到只有被移除节点之前是属于就绪列表才会进入这个更新就绪列表)
#define taskRESET_READY_PRIORITY( uxPriority ) \ { \ if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 ) \ { \ portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \ } \ }
任务等待事件的判断先不管;
3. 若此时调度器还在运行,则重置下一刻将要解除重置的任务的时间(做这个操作是因为前面刚被移除的任务有可能是被插在延时列表中的,所以这里要重置一边阻塞任务解除阻塞的时间);
4. 挂起任务是当前运行任务,且调度器也在运行的话,则触发任务切换,因为当前任务被挂起了,不能再继续运行了;
4.1 挂起任务是当前运行任务,但调度器没有运行的话,直接触发上下文切换vTaskSwitchContext,查看要下一个优先级最高的就绪任务;(注意这里只是将pxCurrentTCB的指向一任务,但并没有跳转,因为调度器被挂起了)
vTaskSuspend总结:
传入“任务句柄”所属的任务挂载到“挂起列表”,并将这个任务从“就绪列表删除”;
若传入参数为NULL,则表示被挂起任务为当前正在运行的任务;
2.2 挂起所有任务函数 vTaskSuspendAll
(这个函数和xTaskResumeAll成对使用)
void vTaskSuspendAll( void )
{
++uxSchedulerSuspended;
}
uxSchedulerSuspended是调度器挂起标志位,它最开始被创建时为0,这里++是为了保证函数嵌套时,有多个函数需要将调度器挂起的环境,当其退出时能保证最外层函数退出后再启用调度器。(不然最里层的函数退出后,就会启用调度器,这保证不了其他函数需要将调度器挂起的运行环境);
vTaskSuspendAll总结:
这个函数就是挂起了调度器,调度器被挂起了,看起来就像挂起了所有任务,但其实所有任务是不能被全部挂起的,因为RTos必须保证有一个任务正在运行,至少空闲任务是在运行的。
3.挂起恢复
3.1 任务恢复函数 vTaskResume
void vTaskResume( TaskHandle_t xTaskToResume )
{
TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;
if( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) )
{
taskENTER_CRITICAL();
{
if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
{
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
taskYIELD_IF_USING_PREEMPTION();
}
}
}
taskEXIT_CRITICAL();
}
}
参数:传入的任务控制块
任务恢复函数具体做了以下操作:
1. 若传入要恢复的任务不为NULL,且不是正在运行的任务,则调用prvTaskIsTaskSuspended函数,确认当前传入任务是否是被挂起的任务:
(下面任务关于事件的部分的判断被删除,基本用不到,可以先不用管事件)
static BaseType_t prvTaskIsTaskSuspended( const TaskHandle_t xTask ) { BaseType_t xReturn = pdFALSE; const TCB_t * const pxTCB = ( TCB_t * ) xTask; //判当前被挂起任务是否属于"xSuspendedTaskList"链表 if( listIS_CONTAINED_WITHIN( &xSuspendedTaskList, &( pxTCB->xStateListItem ) ) != pdFALSE ) { xReturn = pdTRUE; } return xReturn; }
2.若此任务是处在“挂起链表”,则将其从挂起链表中移除,并将它添加到就绪列表中;
3.任务就绪后,要判断一下它的优先级是否比当前正在运行的任务要高一些,若是的话就进行一次任务调度;
vTaskResume总结:
判断传入的任务是否正确,正确就将这个任务从“挂起列表”中移动到“就绪列表”中,之后再看刚刚就绪的任务的优先级是否>正在运行的任务,是就启用调度;
3.2 任务从中断中恢复函数 xTaskResumeFromISR
(调用此函数要设置相应宏)
#if ( ( INCLUDE_xTaskResumeFromISR == 1 ) && ( INCLUDE_vTaskSuspend == 1 ) )
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )
{
BaseType_t xYieldRequired = pdFALSE;
TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;
UBaseType_t uxSavedInterruptStatus;
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
{
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xYieldRequired = pdTRUE;
}
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
}
else
{
vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
}
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
return xYieldRequired;
}
#endif
参数:传入的任务控制块
返回参数:是否需要调度器调度
任务恢复函数(ISR)具体做了以下操作:
1. 首先定义了两个标志位:xYieldRequired(是否需要任务切换)、uxSavedInterruptStatus(保存关闭中断的状态)
2.调用了中断中进入临界区函数portSET_INTERRUPT_MASK_FROM_ISR,其实它最终调用的是ulPortRaiseBASEPRI(),这个函数会返回调用它之前的basepri寄存器的值,之后退出临界段时候恢复basepri的配置;(现在进入临界段前的basepri寄存器信息被保存在了uxSavedInterruptStatus中)
3.判断传入任务是否真的属于“挂起列表”,属于再进行如下判断:
(1): 调度器启用,都满足的话则再判断当前恢复任务的优先级是否>当前正在运行的任务,若是,则将需要调度标志位xYieldRequired置1;
(2):调度器被挂起,此时会将任务挂载到xPendingReadyList 链表中(防止在调度器挂起时恢复任务),之后xPendingReadyList中的任务会在恢复调度器的时候被恢复;(调度器被挂起时,所有要恢复的任务都会被挂载到xPendingReadyList中)
4.将当前任务从“挂起列表”中删除,再将这个任务插入到就绪列表中;
5.退出临界段,将先前的basepri寄存器配置信息传入,之后中断信息会恢复到进入临界段前的状态,最后返回“调度标志位”;
xTaskResumeFromISR总结:
相对与普通恢复挂起任务函数,这个函数最大的不同在于:
1.调度不会在恢复函数中进行,需要调用完这个函数后判断其返回值再确认是否需要调度;
2.需保存进入临界段前的屏蔽寄存器信息,方便之后恢复;
注意:这个函数不能随意在中断中使用,因为挂起任务和这个恢复挂起任务一般是成对出现的,若在任务中挂起任务,此时也没有区协调中断和任务的先后次序,则会发生任务都还没有被挂起,中断就过来了,此时中断中调用中断恢复是没有用的,这就错过了一次中断操作,之后任务被挂起后,只有等下次中断操作了;(也就是错过了一次中断操作)
3.3 恢复所有任务 xTaskResumeAll
BaseType_t xTaskResumeAll( void )
{
TCB_t *pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;
taskENTER_CRITICAL();
{
--uxSchedulerSuspended;
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U )
{
while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xYieldPending = pdTRUE;
}
}
if( pxTCB != NULL )
{
prvResetNextTaskUnblockTime();
}
{
UBaseType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. */
if( uxPendedCounts > ( UBaseType_t ) 0U )
{
do
{
if( xTaskIncrementTick() != pdFALSE )
{
xYieldPending = pdTRUE;
}
--uxPendedCounts;
} while( uxPendedCounts > ( UBaseType_t ) 0U );
uxPendedTicks = 0;
}
}
if( xYieldPending != pdFALSE )
{
#if( configUSE_PREEMPTION != 0 )
{
xAlreadyYielded = pdTRUE;
}
#endif
taskYIELD_IF_USING_PREEMPTION();
}
}
}
}
taskEXIT_CRITICAL();
return xAlreadyYielded;
}
返回参数:是否已经调度了
恢复所有任务函数具体做了以下操作:
1.创建了一个任务调度标志xAlreadyYielded,后面函数返回参数时,返回的就是xAlreadyYielded这个参数;
2.进入临界段后,就开始--uxSchedulerSuspended标志位,之后判断它是否已经为0了,如果是则意味着调度器又被启用了;
3.若现在要启用调度器了,且这个函数的目的是为了恢复所有任务,这就需要将所有挂载在“挂起列表”中的任务恢复,这里使用的方式是用一个do......while....循环,while中只有当“挂起列表”中的任务节点清空才会退出循环,do之中就是将第一个节点从挂起列表移出后,再将其挂载到就绪列表,其中会将每个当前就绪任务与当前正在运行的任务做比对,若刚被就绪的任务优先级>正在运行的任务,则表示恢复任务后需要调度,此时置xAlreadyYielded为1;
4.由于调度器被挂起时,阻塞延时任务的下一刻解除阻塞时间是没有被计算的,所以此时需要调用prvResetNextTaskUnblockTime来更新一下,下一刻解除阻塞时间;
5.由上一点可以知道调度器挂起时,下一刻解除阻塞时间是没有更新的(因为在嘀嗒中断中因为调度被屏蔽了,所以关于调度的程序并没有被执行),此时为了保证阻塞延时的正确性,xTaskIncrementTick这个函数中当调度器被挂起,就会将uxPendedTicks++,这是符合正常调度的时间的;
6.这里同样是调用了一个do.....while.....循环,while的判断条件是uxPendedCounts==0就退出循环,而在do中是将所有调度器失去的时间补回来了,每调用一次xTaskIncrementTick就判断是否需要调度,若需要则置xYieldPending为1,迭代查询完成后将uxPendedCounts置0(这只是个保险操作,因为只有当uxPendedCounts = 0了才会退出循环);
7.若此时是需要调度的则将xAlreadyYielded置1(表示已经调度了一次了),之后立即开启一次调度,最后返回xAlreadyYielded标志;
xTaskResumeAll总结:
1. 它先是将所有在“挂起列表”中的任务移入到“就绪列表”中(所有挂起-->所有就绪),在这之中判断就绪任务是否>运行任务,若是则置调度标志;
2. 而后它以uxPendedCounts任务挂起时间计数变量为指导,将调度器所有失去的时间补回来(也就是连续调用了uxPendedCounts次xTaskIncrementTick函数);
3.最后判断是否需要调度,需要则立即调度,返回是否已经调度变量;
3.4 任务删除函数 vTaskDelete
#if ( INCLUDE_vTaskDelete == 1 )
void vTaskDelete( TaskHandle_t xTaskToDelete )
{
TCB_t *pxTCB;
taskENTER_CRITICAL();
{
pxTCB = prvGetTCBFromHandle( xTaskToDelete );
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
uxTaskNumber++;
if( pxTCB == pxCurrentTCB )
{
vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );
++uxDeletedTasksWaitingCleanUp;
}
else
{
--uxCurrentNumberOfTasks;
prvDeleteTCB( pxTCB );
prvResetNextTaskUnblockTime();
}
}
taskEXIT_CRITICAL();
if( xSchedulerRunning != pdFALSE )
{
if( pxTCB == pxCurrentTCB )
{
portYIELD_WITHIN_API();
}
}
}
#endif
参数:要删除任务的任务句柄;(传入NULL的话就是删除调用这个函数的任务其本身)
删除任务函数具体做了以下操作:
1. 进入临界段后,将调用prvGetTCBFromHandle函数得到合适的任务控制块(NULL就是运行任务本身,或则就是传入的任务句柄所对应的任务)
2. 将当前要删除的任务从其所在节点移除,若此时被删除任务先前所在链表的节点个数为0了,则调用重置优先级函数(前面分析过,只会在"就绪列表"下节点为0了才会置就绪标志的相应bit位为0)
3. uxTaskNumber变量其实是任务的唯一标识/索引,每个任务都有唯一的编号,用于系统中管理任务,它会在创建新任务并插入就绪链表时给这个任务,这里++是为了防止任务的编号重复(uxTaskNumber起作用的情况是“configUSE_TRACE_FACILITY ”宏被打开)
4. 如果是当前运行的任务要被删除,此时会将这个任务插入到“任务等待结束”链表中,最后将uxDeletedTasksWaitingCleanUp+1,表示现在一共有多少个任务被删除了;(之后这上面的任务会在这个链表上被一一删除,之后会在空闲任务中调用prvCheckTasksWaitingTermination函数检查链表,若有则将此任务的堆栈空间删除)
4.1 若删除的不是当前正在运行的任务,则将当前总任务数uxCurrentNumberOfTasks变量--,之后调用vPortFree直接释放被删除任务之前申请的空间,最后由于有未被运行的任务被删除了,我们不知道其先前是处在就绪还是阻塞列表中,这时就应该重新计算一下下一刻阻塞任务被解除的时间;
5. 退出临界段,若此时调度器在运行,且删除的任务是当前任务,此时就直接调用调度器;
vTaskDelete总结:
首先将任务从各个链表中移除(表示现在任务不属于任何状态了),之后再去判断当前被删除任务是运行的任务,还是未运行的任务
1. 若是运行的任务则先将任务挂到结束链表中(之后会被删除)最后触发调度;(这里不直接删除正在运行任务,原因是运行任务中保存有上个任务切换发生时的信息,若此时删除任务堆栈后再启用调度器,会导致调度错乱)
2. 若是未运行任务则可直接释放此任务的空间之后,更新下一刻任务解除阻塞时间,最后触发调用;