目录
本章基于FreeRtos V9.0.0版本分析
在学习本章节前需要先理解《基于STM32F103ZE平台分析FreeRtos(四)——消息队列》章节,因为信号量的创建、获取、释放都是基于队列的功能实现的。
一、信号量简介
信号量是一种任务间通信的机制,是一种特殊的队列(没有队员),主要实现任务之间同步、临界区的互斥访问。
1.1 信号分类
分类 | 结构简介(参考Queue_t结构体) | 适用场合 |
计数信号 | 特殊队列(没有队员), (1)最大长度uxLength表示最大资源数; (2)队员大小uxItemSize为0; (3)uxMessagesWaiting表示剩余资源数。 | (1)多任务同步。 (2)多资源竞争。 |
二值信号 | 特殊计数信号(最大资源数固定1), (1)最大长度uxLength为1; (2)队员大小uxItemSize为0; (3)uxMessagesWaiting表示二值状态,非0即1。 | (1)互斥功能(可能会有优先级翻转)。 (2)同步功能。 |
互斥信号 | 特殊二值信号(支持优先级继承机制), (1)最大长度uxLength为1; (2)队员大小uxItemSize为0; (3)uxMessagesWaiting表示解闭锁(1-解锁;0-闭锁); (4)pxMutexHolder表示当前持有信号的任务。 | 互斥功能(具备优先级继承机制,不具备递归调用机制;强行递归调用会死锁) |
递归互斥信号 | 特殊互斥信号(支持递归), (1)最大长度uxLength为1; (2)队员大小uxItemSize为0; (3)uxMessagesWaiting表示解闭锁(1-解锁;0-闭锁); (4)pxMutexHolder表示当前持有信号的任务; (5)uxRecursiveCallCount表示递归次数。 | 互斥功能(具备优先级继承机制,具备递归调用机制) |
1.2 信号量接口
信号量接口统一通过宏定义设置,在文件semphr.h中设置。
信号创建接口:信号创建有2种方式,静态创建和动态创建,与队列创建类似。静态创建即控制块通过全局变量存储,不能释放的;动态创建的控制块为动态内存,可以生灭;本章我们仅以动态创建为例进行分析。
/*---------------------------信号创建接口(动态)-----------------------------*/
// 创建二值信号(动态)
#define vSemaphoreCreateBinary( xSemaphore )
#define xSemaphoreCreateBinary()
// 创建计数信号(动态)
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount )
// 创建互斥信号(动态)
#define xSemaphoreCreateMutex()
// 创建递归互斥信号(动态)
#define xSemaphoreCreateRecursiveMutex()
/*---------------------------信号创建接口(静态)-----------------------------*/
// 创建二值信号(静态)
#define xSemaphoreCreateBinaryStatic( pxStaticSemaphore )
// 创建计数信号(静态)
#define xSemaphoreCreateCountingStatic( uxMaxCount, uxInitialCount, pxSemaphoreBuffer )
// 创建互斥信号(静态)
#define xSemaphoreCreateMutexStatic( pxMutexBuffer )
// 创建递归互斥信号(静态)
#define xSemaphoreCreateRecursiveMutexStatic( pxStaticSemaphore )
信号操作接口:信号创建完成,便可以通过操作接口进行互斥、和同步实现。
/*---------------------------获取信号接口-----------------------------*/
// 获取信号(线程)
#define xSemaphoreTake( xSemaphore, xBlockTime )
// 获取信号(中断)
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )
// 获取递归互斥信号
#define xSemaphoreTakeRecursive( xMutex, xBlockTime )
/*---------------------------释放信号接口-----------------------------*/
// 释放信号(线程)
#define xSemaphoreGive( xSemaphore )
// 释放递归互斥信号
#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken )
// 释放递归互斥信号
#define xSemaphoreGiveRecursive( xMutex )
/*--------------------------- 删除信号接口----------------------------*/
#define vSemaphoreDelete( xSemaphore )
/*--------------------------- 获取互斥信号拥有者----------------------*/
#define xSemaphoreGetMutexHolder( xSemaphore )
/*--------------------------- 获取信号个数----------------------------*/
#define uxSemaphoreGetCount( xSemaphore )
二、二值信号
2.1 结构体
二值信号是一种没有队员的队列,即仅有队列控制块,uxLength固定为1,控制块中的读写指针、存储指针均指向控制块本身,uxMessagesWaiting表示当前二值状态,非0即1。具备信号释放和获取的阻塞任务链表,但在实际使用中,只有信号获取会被阻塞。
2.2 源码及功能分析
信号创建:信号创建调用了队列创建函数xQueueGenericCreate,创建出的队列没有队员,最大长度为1,类型为queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号。针对互斥、同步分别设置接口。
#define vSemaphoreCreateBinary( xSemaphore ) :初始化并释放信号,uxMessagesWaiting=1,可用于互斥的场合。
#define vSemaphoreCreateBinary( ):仅初始化信号,uxMessagesWaiting=0,返回句柄,用于同步的场合。
关于函数xQueueGenericCreate的具体分析,请参考 《基于STM32F103ZE平台分析FreeRtos(四)——消息队列》。
// 二值信号创建,状态初始化为1,用于互斥
#define vSemaphoreCreateBinary( xSemaphore ) \
{ \
( xSemaphore ) = xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE ); \
if( ( xSemaphore ) != NULL ) \
{ \
( void ) xSemaphoreGive( ( xSemaphore ) ); \
} \
}
// 二值信号创建,状态初始化为0,用于同步
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
信号获取:信号获取支持线程和中断模式,通过调用队列接收函数xQueueGenericReceive和xQueueReceiveFromISR实现,接收队员为空;接收成功后uxMessagesWaiting减1,函数返回成功;
任务获取接口,如果uxMessagesWaiting==0,任务阻塞,阻塞时间xBlockTime 可设;如果超期仍不成功,返回失败。
中断获取接口,如果uxMessagesWaiting==0,无阻塞,返回失败。
关于函数xQueueGenericSend和xQueueReceiveFromISR的具体分析,请参考 《基于STM32F103ZE平台分析FreeRtos(四)——消息队列》。
// 获取信号(线程)
#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )
// 获取信号(中断)
#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )
信号释放:信号释放支持线程和中断模式,通过调用队列发送函数xQueueGenericSend和xQueueGiveFromISR实现,发送方式固定为FIFO,队员为空,如果发送成功,uxMessagesWaiting增加为1。如果uxMessagesWaiting为0,不阻塞直接跳过。
信号释放无需考虑阻塞的问题,一方面是使用过程中在获取信号(即uxMessagesWaiting==0)后才会调用信号释放xSemaphoreGive,另一方面,即便调用xSemaphoreGive出现失败也关系不大。
函数xQueueGiveFromISR与xQueueGenericSendFromISR类似,只是xQueueGiveFromISR固定发送方式为FIFO,并且增加了互斥信号和信号条目大小的断言,其他逻辑完全一致。
关于函数xQueueGenericSend和xQueueGenericSendFromISR的具体分析,参考 《基于STM32F103ZE平台分析FreeRtos(四)——消息队列》。
// 释放信号(线程)
#define xSemaphoreGive( xSemaphore ) xQueueGenericSend((QueueHandle_t) (xSemaphore),NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
// 释放信号(中断)
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )
2.3 应用实例
2.3.1 同步功能(二值信号)
例如:任务A完成全局数据更新后,任务B再进行全局数据解析,任务B优先级高于任务A。
// 同步信号
SemaphoreHandle_t semSyn;
// 数据
int data[100];
// 任务A对数据进行采集
void taskA()
{
for(;;)
{
// 更新采集数据data[100]
.......
// 释放二值信号
xSemaphoreGive( semSyn)
}
}
// 任务B对数据进行解析
void taskB()
{
for(;;)
{
// 获取二值信号,获取不到阻塞100个时间片
if(xSemaphoreTake( semSyn,100 )==pdPASS)
{
// 解析采集数据data[100]
.......
}
}
}
void main()
{
// 新建二值信号,默认二值状态为0
semSyn=vSemaphoreCreateBinary( );
// 新建任务A
// 新建任务B
//启动调度器
}
2.3.2 互斥功能(二值信号)
例如:任务A和任务B均需要读写全局数据data[]。
// 同步信号
SemaphoreHandle_t semSyn;
// 数据
int data[100];
// 任务A对数据进行操作
void taskA()
{
for(;;)
{
// 获取二值信号,获取不到阻塞200个时间片
if(xSemaphoreTake( semSyn ,200 )==pdPASS)
{
// 读写数据data[100]
.......
xSemaphoreGive( semSyn )
}
}
}
// 任务B对数据进行操作
void taskB()
{
for(;;)
{
// 获取二值信号,获取不到阻塞100个时间片
if(xSemaphoreTake( semSyn ,100 )==pdPASS)
{
// 读写数据data[100]
.......
xSemaphoreGive( semSyn )
}
}
}
void main()
{
// 新建二值信号,默认二值状态为1
vSemaphoreCreateBinary(semSyn);
// 新建任务A
// 新建任务B
//启动调度器
}
三、计数信号
3.1 结构体
计数信号是一种没有队员的队列,即仅有队列控制块,控制块中的读写指针、存储指针均指向控制块本身;uxLength在初始化时可设,表示最大资源数;uxMessagesWaiting表示当前剩余的资源数,在信号获取和释放中会发生增减,该值小于等于uxLength。结构体具备信号释放和获取的阻塞任务链表,但在实际使用中,只有信号获取会被阻塞。
3.2 源码及功能分析
信号创建:信号创建调用了队列创建函数xQueueCreateCountingSemaphore或xQueueCreateCountingSemaphoreStatic,创建出的队列没有队员,最大长度为可设(根据资源数量设置),类型为queueQUEUE_TYPE_COUNTING_SEMAPHORE计数信号。
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) :uxMaxCount表示最大资源数,uxInitialCount表示当前剩余资源数。
关于函数xQueueGenericCreate的具体分析,参考 《基于STM32F103ZE平台分析FreeRtos(四)——消息队列》。
// 创建计数信号(动态)
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
// 创建计数信号(静态)
#define xSemaphoreCreateCountingStatic( uxMaxCount, uxInitialCount, pxSemaphoreBuffer ) xQueueCreateCountingSemaphoreStatic( ( uxMaxCount ), ( uxInitialCount ), ( pxSemaphoreBuffer ) )
// 创建计数量信号函数
QueueHandle_t xQueueCreateCountingSemaphore(
const UBaseType_t uxMaxCount, // 最大资源数
const UBaseType_t uxInitialCount )// 剩余资源数
{
QueueHandle_t xHandle;
// 【1】断言
configASSERT( uxMaxCount != 0 );
configASSERT( uxInitialCount <= uxMaxCount );
// 【2】新建队列,空队员,最大长度uxLength= uxMaxCount ;队列类型为queueQUEUE_TYPE_COUNTING_SEMAPHORE计数信号
xHandle = xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE );
// 【3】赋值剩余资源uxMessagesWaiting= uxInitialCount
if( xHandle != NULL )
{
( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;
traceCREATE_COUNTING_SEMAPHORE();
}
else
{
traceCREATE_COUNTING_SEMAPHORE_FAILED();
}
return xHandle;
}
信号释放和信号获取与【二、二值信号】章节代码一致,不过常用于多任务。
3.3 应用实例
3.3.1 资源竞争
例如:任务A、任务B、中断1均需要通过data()获取资源,但是data资源只有2组。
// 计数信号
SemaphoreHandle_t semSyn;
// 数据
extern int data();
// 任务A竞争数据
void taskA()
{
for(;;)
{
// 占有资源,如果没有资源,阻塞100时间片
if(xSemaphoreTake( semSyn,100 )==pdPASS)
{
// 竞争数据data()
.......
// 释放资源
xSemaphoreGive( semSyn )
}
}
}
// 任务B竞争数据
void taskB()
{
for(;;)
{
// 占有资源,如果没有资源,阻塞100时间片
if(xSemaphoreTake( semSyn,100 )==pdPASS)
{
// 竞争数据data()
.......
// 释放资源
xSemaphoreGive( semSyn )
}
}
}
// 中断1竞争数据
interrupt int1()
{
for(;;)
{
// 占有资源
if(xSemaphoreTakeFromISR( semSyn,NULL)==pdPASS)
{
// 竞争数据data()
.......
// 释放资源
xSemaphoreGiveFromISR( semSyn ,NULL)
}
}
}
void main()
{
// 新建计数信号,默认资源数为2,当前剩余资源数为2
semSyn=xSemaphoreCreateCounting(2,2);
// 新建任务A
// 新建任务B
//启动调度器
}
3.3.2 多任务同步(或逻辑)
例1:任务A需要等待任务B、任务C中任意一个采集完数据data()后才能进行处理,任务A优先级高于任务B、任务C。
// 计数信号
SemaphoreHandle_t semSyn;
// 数据
extern int dataB();
extern int dataC();
// 任务A处理数据
void taskA()
{
for(;;)
{
// 占有资源,如果没有资源,阻塞100时间片
if(xSemaphoreTake( semSyn,100 )==pdPASS)
{
// 处理数据dataB()、dataC()
.......
}
}
}
// 任务B数据采集
void taskB()
{
for(;;)
{
// 采集数据dataB()
.......
// 释放资源
xSemaphoreGive( semSyn )
}
}
// 任务C数据采集
void taskA()
{
for(;;)
{
// 采集数据dataC()
.......
// 释放资源
xSemaphoreGive( semSyn )
}
}
void main()
{
// 新建计数信号,最大资源数为2,当前剩余资源数为0
semSyn=xSemaphoreCreateCounting(2,0);
// 新建任务A
.....
// 新建任务B
.....
// 新建任务C
.....
//启动调度器
.....
}
3.3.2 多任务同步(与逻辑)
例1:任务A需要等待任务B和任务C均采集完数据data()后才能进行处理,任务A优先级高于任务B、任务C。
// 计数信号
SemaphoreHandle_t semSyn;
// 数据
extern int dataB();
extern int dataC();
// 任务A处理数据
void taskA()
{
for(;;)
{
// 判断资源数量
if(uxSemaphoreGetCount(semSyn)==2)
{
// 处理数据dataB()、dataC()
.......
xSemaphoreTake(semSyn,0);
xSemaphoreTake(semSyn,0);
}
taskDelay();
}
}
// 任务B数据采集
void taskB()
{
for(;;)
{
// 采集数据dataB()
.......
// 释放资源
xSemaphoreGive( semSyn )
}
}
// 任务C数据采集
void taskC()
{
for(;;)
{
// 采集数据dataC()
.......
// 释放资源
xSemaphoreGive( semSyn );
}
}
void main()
{
// 新建计数信号,最大资源数为2,当前剩余资源数为0
semSyn=xSemaphoreCreateCounting(2,0);
// 新建任务A
.....
// 新建任务B
.....
// 新建任务C
.....
//启动调度器
.....
}
四、互斥信号
4.1 结构体
互斥信号是一种特殊二值信号,uxLength固定为1,uxMessagesWaiting表示当前二值状态,非0即1。比二值信号多pxMutexHolder(当前持有信号的任务)、uxRecursiveCallCount(递归次数)。并且控制块头地址设置为空,用于与其他信号区分。
4.2 源码及功能分析
信号创建:信号创建调用了队列创建函数xQueueCreateMutex或xQueueCreateMutexStatic,创建出的队列没有队员,最大长度为1,类型为queueQUEUE_TYPE_MUTEX互斥信号。创建完成初始化互斥信号结构:
互斥结构体头地址:pcHead=NULL,用于与其他队列和信号区分。
互斥信号最大长度:uxLength=1;
互斥信号持有任务:pxMutexHolder=NULL;
互斥信号递归计数:uxRecursiveCallCount=0;
互斥信号当前状态:uxMessagesWaiting=1;默认解锁。
关于函数xQueueGenericCreate和xQueueGenericSend的具体分析,参考 《基于STM32F103ZE平台分析FreeRtos(四)——消息队列》。
// 创建互斥信号(动态)
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
// 创建互斥信号(静态)
#define xSemaphoreCreateMutexStatic( pxMutexBuffer ) xQueueCreateMutexStatic( queueQUEUE_TYPE_MUTEX, ( pxMutexBuffer ) )
// 创建互斥信号函数(动态)
QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )
{
Queue_t *pxNewQueue;
const UBaseType_t uxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0;
// 【1】创建队列,空队列,最大长度为1,队列类型为互斥信号
pxNewQueue = ( Queue_t * ) xQueueGenericCreate( uxMutexLength, uxMutexSize, ucQueueType );
// 【2】初始化互斥信号结构体
prvInitialiseMutex( pxNewQueue );
return pxNewQueue;
}
static void prvInitialiseMutex( Queue_t *pxNewQueue )
{
if( pxNewQueue != NULL )
{
pxNewQueue->pxMutexHolder = NULL; // 初始化信号持有任务
pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX; // 即头部pcHead初始化为NULL
pxNewQueue->u.uxRecursiveCallCount = 0; // 递归计数初始化为0
traceCREATE_MUTEX( pxNewQueue );
// 信号状态uxMessagesWaiting初始化为1
( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK );/*从预期状态的信号量开始。*/
}
else
{
traceCREATE_MUTEX_FAILED();
}
}
信号释放和信号获取与2.2章节一致,但会涉及优先级继承逻辑。
4.3 优先级继承
假设任务H、任务M、任务L;优先级由高到低:H>M>L。
4.3.1 优先级自动翻转
任务H、任务L使用二值信号进行互斥时, 任务H因等待任务L释放信号而出现阻塞,但是任务L由于优先级低的原因,可能会被任务M打断,造成不能及时释放信号;从而导致任务H长时间阻塞。
实质上是变相将任务H的优先级拉低到了与任务L一致,任务H优先级出现翻转,这样不符合实时系统的设计理念。
4.3.2 优先级继承机制
为了避免这种现象,互斥信号增加优先级继承机制,任务H因等待任务L释放信号而出现阻塞时,将任务L优先级强制为与任务H一致,这样可以和任务H轮询执行,不会被任务M打断;当任务L释放信号后,再将任务L恢复成原来的优先级。
4.3.3 优先级继承源码分析
任务控制块(TCB)中与互斥信号相关和优先级继承相关的变量。
UBaseType_t uxPriority; /*< 任务当前的优先级。 0优先级最低*/
// 互斥量功能
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; /*< 任务初始化优先级,用于取消优先级继承,恢复原始优先级 */
UBaseType_t uxMutexesHeld; /*< 互斥信号持有个数*/
#endif
假设任务H、任务L;优先级:H>L。
(1)任务获取互斥信号成功后,会更新互斥信号持有者,更新信号持有任务为当前任务,同时任务累计任务控制块(TCB->uxMutexesHeld)持有信号计数。
比如:任务L成功获取互斥信号,任务L互斥信号持有数量uxMutexesHeld累加,互斥信号的持有者pxMutexHolder指向任务L句柄。
void *pvTaskIncrementMutexHeldCount( void )
{
// 如果在任何任务创建之前调用xSemaphoreCreateMutex()那么pxCurrentTCB将为NULL
if( pxCurrentTCB != NULL )
{
// 累计任务持有信号计数器
( pxCurrentTCB->uxMutexesHeld )++;
}
return pxCurrentTCB;
}
(2)高优先级任务获取互斥信号阻塞后,信号持有任务(低优先级)进行优先级继承处理。
比如:互斥信号被任务L持有,任务H获取互斥信号阻塞;由于任务L优先级小于任务H优先级,会进行优先级继承,将任务L优先级设置为与任务H一致。
注意:如果多个互斥信号在任务L存在嵌套获取,且有多个高优先级任务因互斥被阻塞,则任务L互斥信号持有数量uxMutexesHeld与成功获取的信号个数一致;任务L继承的优先级取被阻塞任务最高的优先级。
// 优先级继承(当前任务被低优先级任务阻塞),pxMutexHolder互斥信号持有任务
void vTaskPriorityInherit( TaskHandle_t const pxMutexHolder )
{
TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;
if( pxMutexHolder != NULL )
{
// 【1】当前运行任务pxCurrentTCB优先级高与信号持有任务优先级
if( pxTCB->uxPriority < pxCurrentTCB->uxPriority )
{
// 【2】信号持有任务事项条目未使用,更新信号持有任务事项条目优先级
if(( listGET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ) ) & taskEVENT_LIST_ITEM_VALUE_IN_USE ) == 0UL ) //判断事件值设置位置
{
listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxCurrentTCB->uxPriority );
else
{
mtCOVERAGE_TEST_MARKER();
}
/*【3.1】如果信号持有任务优先级处于就绪状态,则需要将其移除、修改、移入。*/
if( listIS_CONTAINED_WITHIN(&( pxReadyTasksLists[ pxTCB->uxPriority ] ),&( pxTCB->xStateListItem ) ) != pdFALSE )
{
// 【3.1.1】从原始优先级就绪链表移除
if( uxListRemove(&(pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
// 【3.1.2】重置优先级
pxTCB->uxPriority = pxCurrentTCB->uxPriority;
// 【3.1.3】添加至高优先级就绪链表
prvAddTaskToReadyList( pxTCB );
}
// 【3.2】非就绪状态只修改优先级
else
{
pxTCB->uxPriority = pxCurrentTCB->uxPriority;
}
traceTASK_PRIORITY_INHERIT( pxTCB, pxCurrentTCB->uxPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
(3)信号持有任务释放互斥信号,进行优先级继承恢复处理。
比如:任务H获取互斥信号阻塞,并且信号持有任务L进行了优先级翻转;此时由于任务L完成了操作,进行互斥信号释放完成后;会执行优先级恢复操作,任务L恢复为它原来的优先级uxBasePriority。
注意:如果由多个互斥信号在任务L存在嵌套问题,且有多个高优先级任务因互斥被阻塞,则任务L互斥信号持有数量uxMutexesHeld与成功获取的信号个数一致;且任务L翻转的优先级取被阻塞任务最高的优先级,只有在最后一个互斥信号释放后,任务L的优先级才会恢复,并强制上下文切换。
// 取消优先级继承,pxMutexHolder 互斥信号持有任务
BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder )
{
TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;
BaseType_t xReturn = pdFALSE;
if( pxMutexHolder != NULL )
{
// 【1】断言,持有者为当前任务
configASSERT( pxTCB == pxCurrentTCB );
configASSERT( pxTCB->uxMutexesHeld );
// 【2】当前任务持有数减少
( pxTCB->uxMutexesHeld )--;
/*【3】互斥对象的持有者是否继承了另一个任务的优先级?*/
if( pxTCB->uxPriority != pxTCB->uxBasePriority )
{
/*【3.1】只有当没有其他互斥锁被持有时才取消继承 */
if( pxTCB->uxMutexesHeld == ( UBaseType_t ) 0 )
{
// 【3.2】移除状态条目(从就绪链表)
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/*【3.3】恢复优先级. */
traceTASK_PRIORITY_DISINHERIT( pxTCB, pxTCB->uxBasePriority );
pxTCB->uxPriority = pxTCB->uxBasePriority;
//【3.4】重置事件列表项值。如果这个任务正在运行,它不能用于任何其他目的,并且它必须正在运行才能返回互斥锁
listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxTCB->uxPriority );
//【3.5】恢复就绪
prvAddTaskToReadyList( pxTCB );
/*返回true,表示需要进行上下文切换。这实际上只需要在一个角落的情况下,
即持有多个互斥锁,并且互斥锁以不同于获取它们的顺序返回。如果在返回第一个互斥锁时没有发生上下文切换,即使任务正在等待它,
那么在返回最后一个互斥锁时应该发生上下文切换,无论任务是否正在等待它。*/
xReturn = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
return xReturn;
}
4.4 应用实例
注意:互斥信号的获取与释放必然要成对出现,获取成功后,进行临界访问,访问完成一定要释放。
例如:任务A和任务B均需要读写全局数据data[]。
// 互斥信号
SemaphoreHandle_t semSyn;
// 数据
int data[100];
// 任务A对数据进行采集
void taskA()
{
for(;;)
{
// 获取互斥信号,获取不到阻塞200个时间片
if(xSemaphoreTake( semSyn ,200 )==pdPASS)
{
// 读写数据data[100]
.......
xSemaphoreGive( semSyn )
}
}
}
// 任务B对数据进行解析
void taskB()
{
for(;;)
{
// 获取互斥信号,获取不到阻塞100个时间片
if(xSemaphoreTake( semSyn ,100 )==pdPASS)
{
// 读写数据data[100]
.......
xSemaphoreGive( semSyn )
}
}
}
void main()
{
// 新建互斥信号,默认解锁状态
semSyn=xSemaphoreCreateMutex();
// 新建任务A
// 新建任务B
//启动调度器
}
五、递归互斥信号
递归互斥信号与互斥信号唯一不一样的地方就是多了递归处理。
互斥信号对同一个信号的获取和释放必须成对出现,没释放前,不能重复获取,否则会死锁。
递归互斥信号可以重复获取,通过参数uxRecursiveCallCount记录获取次数,获取次数与释放次数必须一致(即进行了多少次成功获取,就必须进行多少次释放)。
互斥信号适用于比较简单明了的代码中;在结构复杂、模块化的代码中,递归互斥信号更加安全。
5.1 结构体
与互斥信号一致,参考【四、互斥信号】章节。
5.2 源码及功能分析
信号创建:信号创建调用了队列创建函数xQueueCreateMutex或xQueueCreateMutexStatic,与互斥信号调用函数一致,创建出的队列没有队员,最大长度为1,类型为queueQUEUE_TYPE_RECURSIVE_MUTEX递归互斥信号。
// 创建递归信号(动态)
#define xSemaphoreCreateRecursiveMutex() xQueueCreateMutex( queueQUEUE_TYPE_RECURSIVE_MUTEX )
// 创建递归信号(静态)
#define xSemaphoreCreateRecursiveMutexStatic( pxStaticSemaphore ) xQueueCreateMutexStatic( queueQUEUE_TYPE_RECURSIVE_MUTEX, pxStaticSemaphore )
信号获取:递归信号获取为独立接口xSemaphoreTakeRecursive( xMutex, xBlockTime ),任务第一次成功获取信号处理方式与互斥信号处理一样,只是多了递归计数(uxRecursiveCallCount)累加处理。当任务持有者重复获取信号时,递归计数(uxRecursiveCallCount)累加,并获取成功。其他非任务持有者获取信号仍然会被阻塞。
关于函数xQueueGenericReceive的具体分析,参考 《基于STM32F103ZE平台分析FreeRtos(四)——消息队列》。
// 获取递归信号
#define xSemaphoreTakeRecursive( xMutex, xBlockTime ) xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )
// 获取递归互斥信号
BaseType_t xQueueTakeMutexRecursive( QueueHandle_t xMutex, TickType_t xTicksToWait )
{
BaseType_t xReturn;
Queue_t * const pxMutex = ( Queue_t * ) xMutex;
configASSERT( pxMutex );
traceTAKE_MUTEX_RECURSIVE( pxMutex );
// 【1】任务持有者重复调用。
if( pxMutex->pxMutexHolder == ( void * ) xTaskGetCurrentTaskHandle() )
{
( pxMutex->u.uxRecursiveCallCount )++;
xReturn = pdPASS;
}
// 【2】非任务持有者调用
else
{
xReturn = xQueueGenericReceive( pxMutex, NULL, xTicksToWait, pdFALSE );
if( xReturn != pdFAIL )
{
( pxMutex->u.uxRecursiveCallCount )++;
}
else
{
traceTAKE_MUTEX_RECURSIVE_FAILED( pxMutex );
}
}
return xReturn;
}
信号释放:递归信号释放为独立接口xSemaphoreGiveRecursive( xMutex, xBlockTime ),当多次获取递归互斥信号后,任务持有者重复获取信号时,递归计数(uxRecursiveCallCount)递减,并释放成功。最后第一次释放信号处理方式与互斥信号释放处理一样,只是多了递归计数(uxRecursiveCallCount)递减。
由此可见,递归信号的获取和释放次数必须一致。
关于函数xQueueGenericSend的具体分析,参考 《基于STM32F103ZE平台分析FreeRtos(四)——消息队列》。
// 释放递归信号
#define xSemaphoreGiveRecursive( xMutex ) xQueueGiveMutexRecursive( ( xMutex ) )
// 释放互斥信号阻塞的接收任务
BaseType_t xQueueGiveMutexRecursive( QueueHandle_t xMutex )
{
BaseType_t xReturn;
Queue_t * const pxMutex = ( Queue_t * ) xMutex;
configASSERT( pxMutex );
// 【1】信号持有任务释放,
if( pxMutex->pxMutexHolder == ( void * )xTaskGetCurrentTaskHandle() )
{
traceGIVE_MUTEX_RECURSIVE( pxMutex );
//【2】 递归计数自减
( pxMutex->u.uxRecursiveCallCount )--;
//【3】最后一次释放,才能真正释放信号
if( pxMutex->u.uxRecursiveCallCount == ( UBaseType_t ) 0 )
{
/*返回互斥对象。这将自动解除可能等待访问互斥锁的任何其他任务的阻塞*/
( void ) xQueueGenericSend( pxMutex, NULL, queueMUTEX_GIVE_BLOCK_TIME, queueSEND_TO_BACK );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
traceGIVE_MUTEX_RECURSIVE_FAILED( pxMutex );
}
return xReturn;
}
5.3 优先级继承
与互斥信号一致,参考【四、互斥信号】章节。
5.4 应用实例
注意:递归互斥信号的获取与释放次数必须一致,即进行了多少次成功获取,就必须进行多少次释放。
例如:任务A和任务B均通过接口读写全局数据data[],接口模块比较独立,自带互斥锁。
// 递归互斥信号
SemaphoreHandle_t semSyn;
// 数据
int data[100];
void setData()
{
if(xSemaphoreTakeRecursive( semSyn ,200 )==pdPASS)
{
// 读写数据data[100]
.......
xSemaphoreGiveRecursive( semSyn )
}
}
// 任务A对数据进行采集
void taskA()
{
for(;;)
{
// 获取互斥信号,获取不到阻塞200个时间片
if(xSemaphoreTakeRecursive( semSyn ,200 )==pdPASS)
{
setData();
xSemaphoreGiveRecursive( semSyn )
}
}
}
// 任务B对数据进行解析
void taskB()
{
for(;;)
{
// 获取互斥信号,获取不到阻塞100个时间片
if(xSemaphoreTakeRecursive( semSyn ,100 )==pdPASS)
{
setData();
xSemaphoreGiveRecursive( semSyn )
}
}
}
void main()
{
// 新建递归互斥信号,默认解锁状态(uxMessagesWaiting=1)
semSyn=xSemaphoreCreateRecursiveMutex();
// 新建任务A
// 新建任务B
//启动调度器
}
六、信号量使用常见问题
6.1 死锁问题
互斥信号强行递归:当一个任务已经获取互斥信号时,再次调用获取互斥信号接口,将因为互斥信号无法释放而永远获取失败, 这种情况应当使用递归互斥信号实现互斥功能。
// 互斥信号
SemaphoreHandle_t semSyn;
semSyn=xSemaphoreCreateMutex();
void taskA()
{
for(;;)
{
// 获取互斥信号,获取不到阻塞200个时间片
if(xSemaphoreTake( semSyn ,200 )==pdPASS)
{
.......
// 死锁
if(xSemaphoreTake( semSyn ,200 )==pdPASS)
{
.......
xSemaphoreGive( semSyn )
}
xSemaphoreGive( semSyn )
}
}
}
任务相互同步死锁:
任务A与任务B相互同步,如下代码,由于二值信号semSynA、semSynB均不能释放,任务A、B均会死锁。像这种情况,在初始化时先解锁一个信号(比如初始化调用xSemaphoreGive( semSynA)),便可以正常运行。
// 同步信号
SemaphoreHandle_t semSynA;
SemaphoreHandle_t semSynB;
semSynA=vSemaphoreCreateBinary( );
semSynB=vSemaphoreCreateBinary( );
// 任务A对数据进行采集
void taskA()
{
for(;;)
{
if(xSemaphoreTake( semSynB,100 )==pdPASS)
{
xSemaphoreGive( semSynA)
}
}
}
// 任务B对数据进行解析
void taskB()
{
for(;;)
{
if(xSemaphoreTake( semSynA,100 )==pdPASS)
{
xSemaphoreGive( semSynB)
}
}
}
6.2 饥饿问题
抢占式调度中,高优先级任务阻塞时间设置不合理,或者直接无阻塞,会导致低优先级任务长期无法运行,高优先级任务也无法获取信号。如下所示,任务A优先级高于任务B。
// 同步信号
SemaphoreHandle_t semSyn;
semSyn=vSemaphoreCreateBinary( );
// 任务A对数据进行采集
void taskA()
{
for(;;)
{
if(xSemaphoreTake( semSyn,0 )==pdPASS)
{
xSemaphoreGive( semSyn)
}
}
}
// 任务B对数据进行解析
void taskB()
{
xSemaphoreGive( semSyn)
}
6.3 优先级翻转问题
采用二值信号实现任务间互斥功能,如果涉及的任务优先级种类比较多。 高优先级任务因等待低优先级任务释放信号而出现阻塞,实质上是变相将高优先级的优先级拉低,优先级出现翻转。
这种情况应当使用互斥信号实现互斥功能。
6.4 逻辑复杂问题
当信号量使用较多时,互斥和同步逻辑将会变的十分复杂。这时候需要改变一下代码架构来避免逻辑问题。
我们可以通过统一接口的方法减少信号接口调用:对临界区访问进行保护,通过统一接口访问临界区,在临界区接口中统一增加互斥锁。各个任务只需要调用接口即可。对资源申请,通过统一接口调配。 当程序模块比较多时,互斥锁尽量使用递归互斥信号。