目录
本章基于FreeRtos V9.0.0版本分析
一、协程简介
FreeRtos中应用既可以使用任务,也可以使用协程(Co-Routine),或者两者混合使用。但是任务和协程使用不同的API函数,因此不能通过队列(信号量)将数据从任务发给协程,反之亦然。
与任务相比,协程的调度在线程中,不支持抢占调度,只能使用协作式调度,实时精度低;由于没有中断参与,协程具有占用资源少、CPU利用率高的优点。协程与定时器有点类似。
注意:任务与协程混合使用时,协程的调度可以在某个任务中。
二、协程工作机制
2.1 协程控制块结构
与任务相比较,协程控制块即可表示整个协程,不存在协程栈,也不存在上下文切换。
// 协程的回调函数(参数1:协程控制块句柄,参数2:协程ID uxIndex)
typedef void (*crCOROUTINE_CODE)( CoRoutineHandle_t, UBaseType_t );
// 协程结构体
typedef struct corCoRoutineControlBlock
{
crCOROUTINE_CODE pxCoRoutineFunction; /*< 入口函数. */
ListItem_t xGenericListItem; /*< 协程状态条目,用于状态切换. */
ListItem_t xEventListItem; /*< 协程事项表项,用于信号等阻塞*/
UBaseType_t uxPriority; /*< 优先级. */
UBaseType_t uxIndex; /*< ID,当多个协程使用相同入口函数时,用于区分协同例程,为回调函数的第二个参数 */
uint16_t uxState; /*< 协程状态. */
} CRCB_t; /* 协同程序控制块。Note的大小必须与TCB_t的uxPriority相同. */
2.2 协程管理方式
协程的管理方式与任务管理类似,通过链表管理,链表定义如下:
/* 就绪和闭锁协程队列. --------------------*/
static List_t pxReadyCoRoutineLists[configMAX_CO_ROUTINE_PRIORITIES]; /*< 就绪链表. */
static List_t xDelayedCoRoutineList1; /*< 阻塞链表1. */
static List_t xDelayedCoRoutineList2; /*< 阻塞链表2. */
static List_t * pxDelayedCoRoutineList; /*< 指向当前阻塞链表 */
static List_t * pxOverflowDelayedCoRoutineList; /*< 指向溢出阻塞链表. */
static List_t xPendingReadyCoRoutineList; /*< 临时就绪链表,*/
链表结构和操作方式可参考《基于STM32F103ZE平台分析FreeRtos(二)——任务部分》章节。
就绪链表:链接就绪协程,根据协程插入链表的先后顺序排列,新就绪协程插入到链表尾部。
阻塞链表:连接阻塞协程,采用双链表管理,阻塞协程根据阻塞时间片大小,按照从大到小的顺序插入链表,表头指向阻塞时间最近的协程。
双链表管理方式可参考《基于STM32F103ZE平台分析FreeRtos(二)——任务部分》章节。
临时就绪链表:中断中使用,当中断需要释放阻塞协程时,为避免数据冲突,不会直接操作就绪链表,会将释放的协程先插入到临时就绪链表。在协程调度中,由调度器将临时链表中的协程移到正式就绪链表中。
2.3 协程调度方式
协程只有协作式调度,调度器在线程中循环进行,不存在上下文切换,一个协程执行完成后才能执行下一个协程,每次从协程的回调函数入口执行,每次循环执行一个协程的回调函数。
调度的原则是选择优先级最高的协程执行,如果优先级最高的协程有多个,则轮询执行这几个协程。
2.4 协程通信机制
协程支持从中断和线程中以FIFO方式操作消息队列,实现协程间的通信,操作过程与任务类似,但是不能与任务共用消息队列。
当协程与任务混合使用时,协程调度基于某个任务存在,此时协程可以通过调用任务的通信接口与任务通信,其本质还是任务间的通信。
三、协程状态及状态切换
3.1 协程状态
1、就绪( Ready):该协程在就绪链表(pxReadyCoRoutineLists[])或临时就绪链表(xPendingReadyCoRoutineList)中, 就绪的协程已经具备执行的能力,等待调度器调度。
2、运行(Running):该协程在就绪列表中,但是正在调度中执行, 调度器选择运行的永远是处于最高优先级的就绪态协程。
3、阻塞(Blocked): 如果协程正在等待消息,就会从就绪链表移除,并根据阻塞时间插入到阻塞链表中。
3.2 状态切换
1、创建创建→就绪态:协程创建后,根据优先级将协程状态条目连接至就绪链表尾部,等待调度器进行调度。
2、就绪态→运行态:系统调度器启动后(在线程中启动),按照规则依次执行就绪状态的各个协程,当前执行的协程即是运行态;由于调度器在线程中执行,运行态协程不会被其他协程抢占。
3、运行态→就绪态:协程在调度中运行完成后,如果执行过程中没有阻塞,则执行完成切入就绪态,链表无变化。
4、运行态→阻塞态:正在运行的协程发生阻塞(收发消息等待)时,该协程会从就绪列表中移除,并根据阻塞时间片数,设置协程条目xItemValue值(xItemValue=调度入口时间片计数+阻塞时间片数),将该协程依据xItemValue大小插入到阻塞链表,协程由运行态变成阻塞态,然后执行完剩余所有代码后,才退出运行态。
5、阻塞态→就绪态:协程阻塞结束后(阻塞时间到或等待的信号被释放等),此协程会从阻塞链表移除,加入就绪链表,从而由阻塞态变成就绪态。
四、协程创建
协程只提供动态创建接口xCoRoutineCreate:动态新建协程控制块,可以回收利用。
形参:
pxCoRoutineCode:协程回调函数,回调定义如下,函数有2个形参,形参1:协程控制块句柄;形参2:协程ID(即uxIndex)。
typedef void (*crCOROUTINE_CODE)( CoRoutineHandle_t, UBaseType_t );
uxPriority:协程优先级,数值越大,优先级越高。
uxIndex:协程ID,用于区分不同协程调用同一回调函数,为回调函数的第二个参数。
代码分析:
1. 动态创建协程控制块。
2. 第一个协程创建时,初始化管理链表。
3. 协程控制块根据形参初始化:回调函数、优先级、ID。
4. 初始化链表状态条目xGenericListItem和事项条目xEventListItem,其持有者指向协程控制块句柄。
5. 事件条目值xEventListItem->xItemValue设置为优先级(与控制块优先级相反,数值越小,优先级越高);
6. 协程状态条目xGenericListItem插入到就绪链表尾部,并更新最大优先级。
BaseType_t xCoRoutineCreate(
crCOROUTINE_CODE pxCoRoutineCode, // 协程回调函数
UBaseType_t uxPriority, // 协程优先级
UBaseType_t uxIndex ) // 协程ID,用于区分不同协程调用同一回调函数
{
BaseType_t xReturn;
CRCB_t *pxCoRoutine;
//【1】 动态创建协程控制块
pxCoRoutine = ( CRCB_t * ) pvPortMalloc( sizeof( CRCB_t ) );
if( pxCoRoutine )
{
if( pxCurrentCoRoutine == NULL )
{
pxCurrentCoRoutine = pxCoRoutine;
//【1.1】 第一个协程创建时,初始化管理链表
prvInitialiseCoRoutineLists();
}
// 【2】优先级容错
if( uxPriority >= configMAX_CO_ROUTINE_PRIORITIES )
{
uxPriority = configMAX_CO_ROUTINE_PRIORITIES - 1;
}
/* 【3】协程控制块初始化. */
pxCoRoutine->uxState = corINITIAL_STATE;// 状态
pxCoRoutine->uxPriority = uxPriority; // 优先级
pxCoRoutine->uxIndex = uxIndex; // ID
pxCoRoutine->pxCoRoutineFunction = pxCoRoutineCode;// 回调
/*【4】初始化链表挂接条目. */
vListInitialiseItem( &( pxCoRoutine->xGenericListItem ) );
vListInitialiseItem( &( pxCoRoutine->xEventListItem ) );
/*【5】更新条目持有者。*/
listSET_LIST_ITEM_OWNER( &( pxCoRoutine->xGenericListItem ), pxCoRoutine );
listSET_LIST_ITEM_OWNER( &( pxCoRoutine->xEventListItem ), pxCoRoutine );
/*【6】事件条目设置为优先级。*/
listSET_LIST_ITEM_VALUE( &( pxCoRoutine->xEventListItem ), ( ( TickType_t ) configMAX_CO_ROUTINE_PRIORITIES - ( TickType_t ) uxPriority ) );
/*【7】插入到就绪链表,并更新最大优先级*/
prvAddCoRoutineToReadyQueue( pxCoRoutine );
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
return xReturn;
}
五、协程调度分析
5.1 源码分析
1.将临时就绪链表xPendingReadyCoRoutineList中的协程移至正式就绪链表pxReadyCoRoutineLists。
2.协程阻塞判断
(1)取全局时间片计数器xTickCount,并计算重入时间差。
(2)根据时间片遍历阻塞链表,对阻塞时间到的条目,从阻塞链表移除,添加到就绪链表。
(3)如果时间片有翻转现象,对阻塞链表进行切换。
(4)更新协程调度入口时间片xCoRoutineTickCount,协程阻塞时间基于该值计算。
3.调度执行
(1)选择最高优先级协程回调函数执行,如果最高优先级协程有多个,则轮询执行,每次循环执行一个协程的回调。
(2)回调函数形参为运行态协程控制块 pxCurrentCoRoutine和协程控制块识别码pxCurrentCoRoutine->uxIndex。
/*---------------------协程调度(通过循环调度)---------------------*/
void vCoRoutineSchedule( void )
{
/*【1】临时就绪链表协程移到正式就绪链表*/
prvCheckPendingReadyList();
/*【2】阻塞态->就绪态切换,查看是否有阻塞的协同例程超时.*/
prvCheckDelayedList();
/*【3】检查最高优先级*/
while(listLIST_IS_EMPTY(&(pxReadyCoRoutineLists[uxTopCoRoutineReadyPriority])))
{
if(uxTopCoRoutineReadyPriority==0)
{
return;
}
--uxTopCoRoutineReadyPriority;
}
/*【4】遍历列表,因此具有相同优先级的协同例程获得相同的处理器时间份额*/
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentCoRoutine, &( pxReadyCoRoutineLists[ uxTopCoRoutineReadyPriority ] ) );
/*【5】调用协程回调函数*/
( pxCurrentCoRoutine->pxCoRoutineFunction )( pxCurrentCoRoutine, pxCurrentCoRoutine->uxIndex );
return;
}
/*-----------------------------------------------------------*/
5.2 逻辑图分析
六、协程通信
协程通信主要是消息队列的收发,与任务消息队列收发类似,可参考《基于STM32F103ZE平台分析FreeRtos(四)——消息队列》章节学习。
协程通信接口只能用于协程间通信,不能与任务复用消息队列。
6.1 协程发送消息(线程)
协程发送消息必须在协程中调用,调用过程不会与其他协程存在数据冲突。
1. 先进入临界区,协程不存在暂停调度的说法,直接闭锁中断,即进入无嵌套临界区。
2. 如果队列满,根据形参阻塞当前协程 ,阻塞时间片为调度器入口时间xCoRoutineTickCount 加阻塞形参xTicksToWait(xCoRoutineTickCount + xTicksToDelay);将当前协程从就绪链表移至阻塞链表,按照阻塞时间片从大到小顺序插入;同时协程事项条目插入到队列等待链表xTasksWaitingToSend;按照优先级从低到高顺序插入。
3. 退出临界区,退出临界区后,可能会有中断接收消息,但不可能有其他协程接收消息。
4. 再次进入临界区,
5. 如果队列未满, 将消息固定拷贝到消息队尾,并判断链表xTasksWaitingToReceive(接收协程阻塞链表)是否有协程阻塞,释放出被阻塞的最高优先级协程(【阻塞态】->【就绪态】)。
6. 退出临界区。
/*-------------协程发送消息(线程)------------------------------*/
BaseType_t xQueueCRSend( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait )
{
BaseType_t xReturn;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
// 【1】进入临界区
portDISABLE_INTERRUPTS();
{
// 【2】队列已经满,协程阻塞
if( prvIsQueueFull( pxQueue ) != pdFALSE )
{
// 【2.1.1】阻塞一段时间,协程从就绪链表移除,移入阻塞链表
// 【2.2.2】阻塞时间为xCoRoutineTickCount + xTicksToWait,xCoRoutineTickCount 为调度入口时间
if( xTicksToWait > ( TickType_t ) 0 )
{
vCoRoutineAddToDelayedList( xTicksToWait, &( pxQueue->xTasksWaitingToSend ) );
portENABLE_INTERRUPTS();
return errQUEUE_BLOCKED;
}
// 【2.2】无阻塞,直接返回失败
else
{
portENABLE_INTERRUPTS();
return errQUEUE_FULL;
}
}
}
// 【3】退出临界区,不会有协程切换,但是中断可能会有消息发送,本协程继续执行!
portENABLE_INTERRUPTS();
// 【4】进入临界区
portDISABLE_INTERRUPTS();
{
// 【4.1】队列有空闲
if( pxQueue->uxMessagesWaiting < pxQueue->uxLength )
{
//【4.1.2】压入消息
prvCopyDataToQueue( pxQueue, pvItemToQueue, queueSEND_TO_BACK );
xReturn = pdPASS;
/*【4.1.2】是否有等待数据的协程?有的话可以释放(阻塞态->就绪态) */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive)) == pdFALSE)
{
// 释放等待的协程
if( xCoRoutineRemoveFromEventList(&(pxQueue->xTasksWaitingToReceive)) != pdFALSE )
{
xReturn = errQUEUE_YIELD;// 优先级高于当前协程
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
// 【4.2】队列满,返回失败
else
{
xReturn = errQUEUE_FULL;
}
}
portENABLE_INTERRUPTS();
return xReturn;
}
6.2 协程接收消息(线程)
协程接收消息必须在协程中调用,调用过程不会与其他协程存在数据冲突。
1. 进入临界区,协程不存在暂停调度的说法,直接闭锁中断,即进入无嵌套临界区。
2. 如队列没有消息,根据形参阻塞当前协程 ,阻塞时间片为调度器入口时间xCoRoutineTickCount 加阻塞形参xTicksToWait(xCoRoutineTickCount + xTicksToDelay);将当前协程从就绪链表移至阻塞链表,按照阻塞时间片从大到小顺序插入;同时协程事项条目插入到队列等待链表xTasksWaitingToReceive;按照优先级从低到高顺序插入。
3.退出临界区,退出临界区后,可能会有中断发送消息,但不可能有其他协程发送消息。
4.再次进入临界区,
5.如果队列有消息, 固定从队列按照FIFO方式读取消息,并判断链表xTasksWaitingToSend(发送协程阻塞链表)是否由阻塞协程,并释放出被阻塞的最高优先级协程(【阻塞态】->【就绪态】)。
6.退出临界区。
/*--------------------- 协程接收消息(线程)--------------------------------------*/
BaseType_t xQueueCRReceive( QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait )
{
BaseType_t xReturn;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
//【1】进入临界区
portDISABLE_INTERRUPTS();
{
// 【2.1】无消息,阻塞协程
if( pxQueue->uxMessagesWaiting == ( UBaseType_t ) 0 )
{
//【2.1.1】 阻塞时间有效,
if( xTicksToWait > ( TickType_t ) 0 )
{
vCoRoutineAddToDelayedList( xTicksToWait, &( pxQueue->xTasksWaitingToReceive ) );
portENABLE_INTERRUPTS();
return errQUEUE_BLOCKED;
}
//【2.1.2】 阻塞时间无效,返回失败
else
{
portENABLE_INTERRUPTS();
return errQUEUE_FULL;
}
}
// 【2.2】 有消息,不阻塞
else
{
mtCOVERAGE_TEST_MARKER();
}
}
//【】
portENABLE_INTERRUPTS();
portDISABLE_INTERRUPTS();
{
// 有消息 读取新消息
if( pxQueue->uxMessagesWaiting > ( UBaseType_t ) 0 )
{
pxQueue->u.pcReadFrom += pxQueue->uxItemSize;
if( pxQueue->u.pcReadFrom >= pxQueue->pcTail )
{
pxQueue->u.pcReadFrom = pxQueue->pcHead;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--( pxQueue->uxMessagesWaiting );
( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.pcReadFrom, ( unsigned ) pxQueue->uxItemSize );
xReturn = pdPASS;
// 发送是否阻塞
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
// 阻塞-就绪
if( xCoRoutineRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
xReturn = errQUEUE_YIELD;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
xReturn = pdFAIL;
}
}
portENABLE_INTERRUPTS();
return xReturn;
}
6.3 向协程发送消息(中断)
1. 中断中操作消息先进入临界区(可嵌套)。
2. 如果队列满,退出发送。
3. 如果队列未满, 将消息固定拷贝到消息队尾,并判断链表xTasksWaitingToReceive(接收协程阻塞链表)是否有阻塞的协程,并释放出被阻塞的最高优先级的协程(【阻塞态】->【就绪态】);此时释放的协程暂时插入临时就绪链表xPendingReadyCoRoutineList;由协程调度器统一处理。
4.退出临界区。
/*------------------向协程发送消息(中断)-----------------------------------------*/
BaseType_t xQueueCRSendFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t xCoRoutinePreviouslyWoken )
{
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
if( pxQueue->uxMessagesWaiting < pxQueue->uxLength )
{
prvCopyDataToQueue( pxQueue, pvItemToQueue, queueSEND_TO_BACK );
// 中断唤醒一个接收阻塞的协程
if( xCoRoutinePreviouslyWoken == pdFALSE )
{
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
if( xCoRoutineRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
return pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
return xCoRoutinePreviouslyWoken;
}
6.4 接收协程消息(中断)
1.中断中操作消息先进入临界区(可嵌套)。
2.如果队列无消息,退出接收。
3.如果队列有消息, 固定按照FIFO方式获取消息,并判断链表xTasksWaitingToSend(发送协程阻塞链表)是否有阻塞的协程,并释放出被阻塞的最高优先级的协程(【阻塞态】->【就绪态】);此时释放的协程暂时插入临时就绪链表xPendingReadyCoRoutineList;由线程中的调度器统一处理。
4.退出临界区。
/*--------------------接收协程消息(中断)--------------------------*/
BaseType_t xQueueCRReceiveFromISR( QueueHandle_t xQueue, void *pvBuffer, BaseType_t *pxCoRoutineWoken )
{
BaseType_t xReturn;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
/*我们无法阻止ISR,所以检查是否有可用的数据。如果没有,那就什么都不做就离开 */
if( pxQueue->uxMessagesWaiting > ( UBaseType_t ) 0 )
{
/* 从队列拷贝数据. */
pxQueue->u.pcReadFrom += pxQueue->uxItemSize;
if( pxQueue->u.pcReadFrom >= pxQueue->pcTail )
{
pxQueue->u.pcReadFrom = pxQueue->pcHead;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--( pxQueue->uxMessagesWaiting );
( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.pcReadFrom, ( unsigned ) pxQueue->uxItemSize );
// 唤醒一个发送协程
if((*pxCoRoutineWoken)== pdFALSE )
{
// 移除事项
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
// 恢复阻塞等待发送的协程,更新优先级
if( xCoRoutineRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
*pxCoRoutineWoken = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
return xReturn;
}