FreeRTOS的信号量包括二进制信号量、计数信号量、互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量)。关于它们的区别可以参考《 FreeRTOS系列第19篇---FreeRTOS信号量》一文。
信号量API函数实际上都是宏,它使用现有的队列机制。这些宏定义在semphr.h文件中。如果使用信号量或者互斥量,需要包含semphr.h头文件。
二进制信号量、计数信号量和互斥量信号量的创建API函数是独立的,但是获取和释放API函数都是相同的;递归互斥信号量的创建、获取和释放API函数都是独立的。
在《FreeRTOS高级篇5---FreeRTOS队列分析》中,我们分析了队列的实现过程,包括队列创建、入队和出队操作。在那篇文章中我们说过,创建队列API函数实际是调用通用队列创建函数xQueueGenericCreate()来实现的。其实,不但创建队列实际调用通用队列创建函数,二进制信号量、计数信号量、互斥量和递归互斥量也都直接或间接使用这个函数,如表1-1所示。表1-1中红色字体表示是间接调用xQueueGenericCreate()函数。
表1-1:队列、信号量和互斥量创建宏与直接(间接)执行函数
二进制信号量创建实际上是直接使用通用队列创建函数xQueueGenericCreate()。创建二进制信号量API接口实际上是一个宏,定义如下:
#define xSemaphoreCreateBinary() \
xQueueGenericCreate( \
( UBaseType_t ) 1, \
semSEMAPHORE_QUEUE_ITEM_LENGTH, \
NULL, \
NULL, \
queueQUEUE_TYPE_BINARY_SEMAPHORE\
)
通过这个宏定义我们知道创建二进制信号量实际上是创建了一个队列,队列项有1个,但是队列项的大小为0(宏semSEMAPHORE_QUEUE_ITEM_LENGTH定义为0)。
有了队列创建的知识,我们可以很容易的画出初始化后的二进制信号量内存,如图1-1所示。
图1-1:初始化后的二进制信号量对象内存
或许不止一人像我一样奇怪,创建一个没有队列项存储空间的队列,信号量用什么表示?其实二进制信号量的释放和获取都是通过操作队列结构体成员uxMessageWaiting来实现的(图1-1红色部分,uxMessageWaiting表示队列中当前队列项的个数)。经过初始化后,变量uxMessageWaiting为0,这说明队列为空,也就是信号量处于无效状态。在使用API函数xSemaphoreTake()获取信号之前,需要先释放一个信号量。后面讲到二进制信号量释放和获取时还会详细介绍。
创建计数信号量间接使用通用队列创建函数xQueueGenericCreate()。创建计数信号量API接口同样是个宏定义:
#define xSemaphoreCreateCounting(uxMaxCount, uxInitialCount ) \
xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ), (NULL ) )
创建计数信号量API接口有两个参数,含义如下:
我们来看一下函数xQueueCreateCountingSemaphore()如何实现的:
QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_tuxMaxCount, const UBaseType_t uxInitialCount, StaticQueue_t *pxStaticQueue )
{
QueueHandle_t xHandle;
configASSERT( uxMaxCount != 0 );
configASSERT( uxInitialCount <= uxMaxCount );
/*调用通用队列创建函数*/
xHandle =xQueueGenericCreate(
uxMaxCount,
queueSEMAPHORE_QUEUE_ITEM_LENGTH,
NULL,
pxStaticQueue,
queueQUEUE_TYPE_COUNTING_SEMAPHORE );
if( xHandle != NULL )
{
( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;
}
configASSERT( xHandle );
return xHandle;
}
从代码可以看出,创建计数信号量仍然调用通用队列创建函数xQueueGenericCreate()来创建一个队列,队列项的数目由参数uxMaxCount指定,每个队列项的大小由宏queueSEMAPHORE_QUEUE_ITEM_LENGTH指出,我们找到这个宏定义发现,这个宏被定义为0,也就是说创建的队列只有队列数据结构存储空间而没有队列项存储空间。
如果队列创建成功,则将队列结构体成员uxMessageWaiting设置为初始计数信号量值。初始化后的计数信号量内存如图3-1所示。
图1-2:初始化后的计数信号量对象内存
创建互斥量间接使用通用队列创建函数xQueueGenericCreate()。创建互斥量API接口同样是个宏,定义如下:
#define xSemaphoreCreateMutex() \
xQueueCreateMutex( queueQUEUE_TYPE_MUTEX, NULL )
其中,宏queueQUEUE_TYPE_MUTEX用于通用队列创建函数,表示创建队列的类型是互斥量,在文章《FreeRTOS高级篇5---FreeRTOS队列分析》关于通用队列创建函数参数说明中提到了这个宏。
我们来看一下函数xQueueCreateMutex()是如何实现的:
#if ( configUSE_MUTEXES == 1 )
QueueHandle_t xQueueCreateMutex( const uint8_tucQueueType, StaticQueue_t *pxStaticQueue )
{
Queue_t *pxNewQueue;
const UBaseType_tuxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0;
/* 防止编译器产生警告信息 */
( void ) ucQueueType;
/*调用通用队列创建函数*/
pxNewQueue = ( Queue_t * )xQueueGenericCreate( uxMutexLength, uxMutexSize, NULL, pxStaticQueue, ucQueueType );
/* 成功分配新的队列结构体? */
if( pxNewQueue != NULL )
{
/*xQueueGenericCreate()函数会按照通用队列的方式设置所有队列结构体成员,但是我们是要创建互斥量.因此需要对一些结构体成员重新赋值. */
pxNewQueue->pxMutexHolder = NULL;
pxNewQueue->uxQueueType =queueQUEUE_IS_MUTEX; //NULL
/* 用于递归互斥量创建 */
pxNewQueue->u.uxRecursiveCallCount = 0;
/* 使用一个预期状态启动信号量 */
( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK);
}
return pxNewQueue;
}
#endif /* configUSE_MUTEXES */
这个函数是带条件编译的,只有将宏configUSE_MUTEXES定义为1才会编译这个函数。
函数首先调用通用队列创建函数xQueueGenericCreate()来创建一个队列,队列项数目为1,队列项大小为0,说明创建的队列只有队列数据结构存储空间而没有队列项存储空间。
如果队列创建成功,通用队列创建函数还会按照通用队列的方式 初始化所有队列结构体成员。但是这里要创建的是互斥量,所以有一些结构体成员必须重新赋值。在这段代码中,可能你会疑惑,队列结构体成员中,并没有pxMutexHolder和uxQueueType!其实这两个标识符只是宏定义,是专门为互斥量而定义的,如下所示:
#define pxMutexHolder pcTail
#define uxQueueType pcHead
#define queueQUEUE_IS_MUTEX NULL
当队列结构体用于互斥量时,成员pcHead和pcTail指针就不再需要,并且将pcHead指针设置为NULL,表示pcTail指针实际指向互斥量持有者任务TCB(如果有的话)。
最后调用函数xQueueGenericSend()释放一个互斥量,相当于互斥量创建后是有效的,可以直接使用获取信号量API函数来获取这个互斥量。如果某资源同时只准一个任务访问,可以用互斥量保护这个资源。这个资源一定是存在的,所以创建互斥量时会先释放一个互斥量,表示这个资源可以使用。任务想访问资源时,先获取互斥量,等使用完资源后,再释放它。也就是说互斥量一旦创建好后,要先获取,后释放,要在同一个任务中获取和释放。这也是互斥量和二进制信号量的一个重要区别,二进制信号量可以在随便一个任务中获取或释放,然后也可以在任意一个任务中释放或获取。互斥量不同于二进制信号量的还有:互斥量具有优先级继承机制,二进制信号量没有,互斥量不可以用于中断服务程序,二进制信号量可以。
初始化后的互斥量内存如图1-3所示。
图1-3:初始化后的互斥量对象内存
创建递归互斥量间接使用通用队列创建函数xQueueGenericCreate()。创建递归互斥量API接口同样是个宏,定义如下:
#definexSemaphoreCreateRecursiveMutex() \
xQueueCreateMutex(queueQUEUE_TYPE_RECURSIVE_MUTEX, NULL )
其中,宏queueQUEUE_TYPE_RECURSIVE_MUTEX用于通用队列创建函数,表示创建队列的类型是递归互斥量,在文章《FreeRTOS高级篇5---FreeRTOS队列分析》关于通用队列创建函数参数说明中提到了这个宏。
创建互斥量和创建递归互斥量是调用的同一个函数xQueueCreateMutex(),至于参数queueQUEUE_TYPE_RECURSIVE_MUTEX,我们在FreeRTOS一文中已经知道,它只是用于可视化调试,因此创建互斥量和创建递归互斥量可以看作是一样的,初始化后的递归互斥量对象内存也和互斥量一样,如图1-3所示。
无论二进制信号量、计数信号量还是互斥量,它们都使用相同的获取和释放API函数。释放信号量用于使信号量有效,分为不带中断保护和带中断保护两个版本。
用于释放一个信号量,不带中断保护。被释放的信号量可以是二进制信号量、计数信号量和互斥量。注意递归互斥量并不能使用这个API函数释放。其实信号量释放是一个宏,真正调用的函数是xQueueGenericSend(),宏定义如下:
#definexSemaphoreGive( xSemaphore ) \
xQueueGenericSend( \
( QueueHandle_t ) ( xSemaphore ), \
NULL, \
semGIVE_BLOCK_TIME, \
queueSEND_TO_BACK )
可以看出释放信号量实际上是一次入队操作,并且阻塞时间为0(由宏semGIVE_BLOCK_TIME定义)。
对于二进制信号量和计数信号量,根据上一章的内容可以总结出,释放一个信号量的过程实际上可以简化为两种情况:第一,如果队列未满,队列结构体成员uxMessageWaiting加1,判断是否有阻塞的任务,有的话解除阻塞,然后返回成功信息(pdPASS);第二,如果队列满,返回错误代码(err_QUEUE_FULL),表示队列满。
对于互斥量要复杂些,因为互斥量具有优先级继承机制。
优先级继承是个什么过程呢?我们举个例子。某个资源X同时只能有一个任务访问,现在有任务A和任务C都要访问这个资源,任务A的优先级为1,任务C的优先级为10,所以任务C的优先级大于任务A的优先级。我们用互斥量保护资源X,并且当前任务A正在访问资源X。在任务A访问资源X的过程中,来了一个中断,中断事件使得任务C执行。任务C执行的过程中,也想访问资源X,但是因为资源X还被任务A独占着,所以任务C无法获取互斥量,会进入阻塞状态。此时,低优先级任务A会继承高优先级任务C的优先级,任务A的优先级临时的被提升,优先级变成10。这个机制能够将已经发生的优先级反转影响降低到最小。
那么什么是优先级反转呢?还是看上面的例子,任务C的优先级高于任务A,但是任务C因为没有获得互斥量而进入阻塞,只能等待低优先级的任务A释放互斥量后才能运行,这种情况就是优先级反转。
那为什么优先级继承可以降低优先级反转的影响呢?还是看上面的例子,不过我们再增加一个优先级为5的任务B,这三个任务都处于就绪状态。如果没有优先级继承机制,三个任务的优先级顺序为任务C>任务B>任务A。当任务C因为得不到互斥量而阻塞后,任务B会获取CPU权限,等到任务B主动或被动让出CPU后,任务A才会执行,任务A释放互斥量后,任务C才能得到运行。再看一下有优先级继承的情况,当任务C因为得不到互斥量而阻塞后,任务A继承任务C的优先级,现在三个任务的优先级顺序为任务C=任务A>任务B。当任务C因为得不到互斥量而阻塞后,任务A会获得CPU权限,等到任务A释放互斥量后,任务C就会得到运行。看,任务C等待的时间变短了。
有了上面的基础理论,我们就很好理解为什么释放互斥量会比较复杂了。还是可以简化为两种情况:第一,如果队列未满,除了队列结构体成员uxMessageWaiting加1外,还要判断获取互斥量的任务是否有优先级继承,如果有的话,还要将任务的优先级恢复到原始值。当然,恢复到原来值也是有条件的,就是该任务必须在没有使用其它互斥量的情况下,才能将继承的优先级恢复到原始值。然后判断是否有阻塞的任务,有的话解除阻塞,最后返回成功信息(pdPASS);第二,如果如果队列满,返回错误代码(err_QUEUE_FULL),表示队列满。
用于释放一个信号量,带中断保护。被释放的信号量可以是二进制信号量和计数信号量。和普通版本的释放信号量API函数不同,它不能释放互斥量,这是因为互斥量不可以在中断中使用!互斥量的优先级继承机制只能在任务中起作用,在中断中毫无意义。带中断保护的信号量释放其实也是一个宏,真正调用的函数是xQueueGiveFromISR (),宏定义如下:
#definexSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) \
xQueueGiveFromISR( \
( QueueHandle_t ) ( xSemaphore), \
( pxHigherPriorityTaskWoken ) )
BaseType_t xQueueGiveFromISR(
QueueHandle_t xQueue,
BaseType_t * constpxHigherPriorityTaskWoken )
{
BaseType_t xReturn;
UBaseType_t uxSavedInterruptStatus;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
uxSavedInterruptStatus =portSET_INTERRUPT_MASK_FROM_ISR();
{
/*当队列用于实现信号量时,永远不会有数据出入队列,但是任然要检查队列是否为空 */
if( pxQueue->uxMessagesWaiting < pxQueue->uxLength )
{
/* 一个任务可以获取多个互斥量,但是只能有一个继承优先级,如果任务是互斥量的持有者,则互斥量不允许在中断服务程序中释放.因此这里不需要判断是否要恢复任务的原始优先级值,只是简单更新队列项计数器. */
++( pxQueue->uxMessagesWaiting );
/* 如果列表上锁,不能改变队列的事件列表. */
if( pxQueue->xTxLock == queueUNLOCKED )
{
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive) ) == pdFALSE )
{
if(xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive) ) != pdFALSE )
{
/* 解除阻塞的任务有更高优先级,因此记录上下文切换请求*/
if(pxHigherPriorityTaskWoken != NULL )
{
*pxHigherPriorityTaskWoken= pdTRUE;
}
}
}
}
else
{
/* Increment thelock count so the task that unlocks the queue
knows that data wasposted while it was locked. */
++( pxQueue->xTxLock );
}
xReturn = pdPASS;
}
else
{
xReturn = errQUEUE_FULL;
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR(uxSavedInterruptStatus );
return xReturn;
}
因为不涉及互斥量,不涉及阻塞,函数xQueueGiveFromISR()异常简单,如果队列满,直接返回错误代码(err_QUEUE_FULL);如果队列未满,则将队列结构体成员uxMessageWaiting加1,然后视队列是否上锁而决定是否解除任务阻塞(如果有得话)。如果你觉得难以理解,则需要先看看《FreeRTOS高级篇5---FreeRTOS队列分析》。
无论二进制信号量、计数信号量还是互斥量,它们都使用相同的获取和释放API函数。释获取信号量会消耗信号量,如果获取信号量失败,任务可能会阻塞,阻塞时间由函数参数xBlockTime指定,如果为0,则直接返回,不阻塞。获取信号量分为不带中断保护和带中断保护两个版本。
用于获取信号量,不带中断保护。获取的信号量可以是二进制信号量、计数信号量和互斥量。注意递归互斥量并不能使用这个API函数获取。其实获取信号量是一个宏,真正调用的函数是xQueueGenericReceive (),宏定义如下:
#definexSemaphoreTake( xSemaphore, xBlockTime ) \
xQueueGenericReceive( \
( QueueHandle_t ) ( xSemaphore ), \
NULL, \
( xBlockTime ), \
pdFALSE )
通过上面的宏定义可以看出,获取信号量实际上是执行出队操作。
对于二进制信号量和计数信号量,可以简化为三种情况:第一,如果队列不为空,队列结构体成员uxMessageWaiting减1,判断是否有因入队而阻塞的任务,有的话解除阻塞,然后返回成功信息(pdPASS);第二,如果队列为空并且阻塞时间为0,则直接返回错误码(errQUEUE_EMPTY),表示队列为空;第三,如果队列为空并且阻塞时间不为0,则任务会因为等待信号量而进入阻塞状态,任务会被挂接到延时列表中。
对于互斥量,也可以简化为三种情况,但是过程要复杂一些:第一,如果队列不为空,队列结构体成员uxMessageWaiting减1、将当前任务TCB结构体成员uxMutexesHeld加1,表示任务获取互斥量的个数、将队列结构体成员指针pxMutexHolder指向任务TCB、判断是否有因入队而阻塞的任务,有的话解除阻塞,然后返回成功信息(pdPASS);第二,如果队列为空并且阻塞时间为0,则直接返回错误码(errQUEUE_EMPTY),表示队列为空;第三,如果队列为空并且阻塞时间不为0,则任务会因为等待信号量而进入阻塞状态,在将任务挂接到延时列表之前,会判断当前任务和拥有互斥量的任务优先级哪个高,如果当前任务优先级高,则拥有互斥量的任务继承当前任务优先级。
用于获取信号量,带中断保护。获取的信号量可以是二进制信号量和计数信号量。和普通版本的获取信号量API函数不同,它不能获取互斥量,这是因为互斥量不可以在中断中使用!互斥量的优先级继承机制只能在任务中起作用,在中断中毫无意义。带中断保护的获取信号量其实也是一个宏,真正调用的函数是xQueueReceiveFromISR (),宏定义如下:
#definexSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken ) \
xQueueReceiveFromISR( \
( QueueHandle_t ) ( xSemaphore ), \
NULL, \
( pxHigherPriorityTaskWoken ) )
同样因为不涉及互斥量,不涉及阻塞,函数xQueueReceiveFromISR ()同样异常简单:如果队列为空,直接返回错误代码(pdFAIL);如果队列非空,则将队列结构体成员uxMessageWaiting减1,然后视队列是否上锁而决定是否解除任务阻塞(如果有得话)。
函数xSemaphoreGiveRecursive()用于释放一个递归互斥量。已经获取递归互斥量的任务可以重复获取该递归互斥量。使用xSemaphoreTakeRecursive() 函数成功获取几次递归互斥量,就要使用xSemaphoreGiveRecursive()函数返还几次,在此之前递归互斥量都处于无效状态。比如,某个任务成功获取5次递归互斥量,那么在它没有返还5次该递归互斥量之前,这个互斥量对别的任务无效。
像其它信号量一样,xSemaphoreGiveRecursive()也是一个宏定义,它最终使用现有的队列机制,实际执行的函数是xQueueGiveMutexRecursive(),这个宏定义如下所示:
#definexSemaphoreGiveRecursive( xMutex ) \
xQueueGiveMutexRecursive( (xMutex ) )
我们重点来看函数xQueueGiveMutexRecursive()的实现过程。经过整理后(去除跟踪调试语句)的源码如下所示:
#if ( configUSE_RECURSIVE_MUTEXES == 1 )
BaseType_txQueueGiveMutexRecursive( QueueHandle_t xMutex )
{
BaseType_t xReturn;
Queue_t * const pxMutex = ( Queue_t * ) xMutex;
/* 互斥量和递归互斥量要在同一个任务中获取和释放,当获取互斥量或递归互斥量时,队列结构体成员指针pxMutexHolder指向获取互斥量或递归互斥量的任务TCB,所以在释放递归互斥量时需要检查这个指针指向的TCB是否是和当前任务TCB相同,如果不相同是不能释放这个递归互斥量的! 注:释放互斥量时,这个检查不是必须的,FreeRTOS的作者将这个检查放在了断言中(configASSERT( pxTCB == pxCurrentTCB);).*/
if( pxMutex->pxMutexHolder == ( void * )xTaskGetCurrentTaskHandle() )
{
/* 每当任务获取递归互斥量时,队列结构体成员u.uxRecursiveCallCount会加1,互斥量不会使用这个变量,它用来保存递归次数.所以,在释放递归互斥量的时候要将它减1*/
( pxMutex->u.uxRecursiveCallCount)--;
/* 递归计数器为0? */
if( pxMutex->u.uxRecursiveCallCount == ( UBaseType_t ) 0 )
{
/* 调用入队函数释放一个互斥量,注意阻塞时间(由宏queueMUTEX_GIVE_BLOCK_TIME定义)为0 */
( void ) xQueueGenericSend( pxMutex, NULL,queueMUTEX_GIVE_BLOCK_TIME, queueSEND_TO_BACK );
}
xReturn = pdPASS;
}
else
{
/* 如果不是本任务拥有这个互斥量,则直接返回错误码 */
xReturn = pdFAIL;
}
return xReturn;
}
#endif /* configUSE_RECURSIVE_MUTEXES */
这个函数是带条件编译的,只有将宏configUSE_RECURSIVE_MUTEXES定义为1才会编译这个函数。
互斥量和递归互斥量的最大区别在于一个递归互斥量可以被已经获取这个递归互斥量的任务重复获取,这个递归调用功能是通过队列结构体成员u.uxRecursiveCallCount实现的。这个变量用于存储递归调用的次数,每次获取递归互斥量后,这个变量加1,在释放递归互斥量后,这个变量减1。只有这个变量减到0,即释放和获取的次数相等时,互斥量才能再次有效,使用入队函数释放一个递归互斥量。
函数xSemaphoreTakeRecursive()用于获取一个递归互斥量。像其它信号量一样,xSemaphoreTakeRecursive()也是一个宏定义,它最终使用现有的队列机制,实际执行的函数是xQueueTakeMutexRecursive(),这个宏定义如下所示:
#definexSemaphoreTakeRecursive( xMutex, xBlockTime ) \
xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )
获取递归互斥量具有阻塞超时参数,如果互斥量正被别的任务使用,可以阻塞设定的时间。我们重点来看函数xQueueTakeMutexRecursive()的实现过程。经过整理后(去除跟踪调试语句)的源码如下所示:
#if ( configUSE_RECURSIVE_MUTEXES == 1 )
BaseType_txQueueTakeMutexRecursive( QueueHandle_t xMutex, TickType_txTicksToWait )
{
BaseType_t xReturn;
Queue_t * const pxMutex = ( Queue_t * ) xMutex;
/*互斥量和递归互斥量要在同一个任务中获取和释放,递归互斥量可以在一个任务中多次获取,当第一次获取递归互斥量时,队列结构体成员指针pxMutexHolder指向获取递归互斥量的任务TCB,在此获取这个递归互斥量时,如果这个指针指向的TCB和当前任务TCB相同,只需要将递归次数计数器u.uxRecursiveCallCount加1即可,不需要再操作队列.*/
if( pxMutex->pxMutexHolder == ( void * )xTaskGetCurrentTaskHandle() )
{
( pxMutex->u.uxRecursiveCallCount)++;
xReturn = pdPASS;
}
else
{
/*调用出队函数*/
xReturn =xQueueGenericReceive( pxMutex, NULL, xTicksToWait, pdFALSE );
/* 成功获取递归互斥量后,要将递归次数计数器加1*/
if( xReturn != pdFAIL )
{
( pxMutex->u.uxRecursiveCallCount)++;
}
}
return xReturn;
}
#endif /* configUSE_RECURSIVE_MUTEXES */
这个函数是带条件编译的,只有将宏configUSE_RECURSIVE_MUTEXES定义为1才会编译这个函数。
程序逻辑比较简单,如果是第一次获取这个递归互斥量,直接使用出队函数,成功后将递归次数计数器加1;如果是第二次或者更多次获取这个递归互斥量,则只需要将递归次数计数器加1即可。