前面介绍过,队列(queue)可以用于传输数据:在任务之间,任务和中断之间。
消息队列用于传输多个数据,但是有时候我们只需要传递一个状态,这个状态值需要用一个数值表示,比如:
卖家:做好了1个包子,做好了2个包子,做好了3个包子!
买家:买了一个包子,包子数量减1
这个停车位我占了,停车位减1
我开车走了,停车位加1
在这些情况下,我们只需要维护一个数值,使用信号量效率更高,更节省内存。
FreeRTOS信号量简介
信号量是一种解决同步问题的机制,可以实现对共享资源的有序访问。其中,“同步”指的是任务之间的同步,即信号量可以使得一个任务等待另一个任务完成某件事后,才继续执行;而“有序访问”指的是对被多任务或中断访问的共享资源(如全局变量)的管理,当一个任务在访问(读取或写入)一个共享资源时,信号量可以防止其他任务或中断在这期间访问(读取或写入)这个共享资源。
FreeRTOS二值信号量
前面说过,信号量是基于队列实现的,二值信号量也不例外,二值信号量实际上就是一个队列长度为1的队列,在这种情况下,队列就只有空和满两种情况,这就是二值信号量,二值信号量通常用于互斥访问或任务同步,与互斥信号量类似,但是二值信号量有优先级反转的问题:优先级反转就是指,当一个高优先级任务因获取一个低优先级任务获取而处于没有资源状态的二值信号量时,这个高优先级的任务就将被阻塞,直到低优先级任务释放二值信号量,在此之前,如果有一个优先级介于高优先级和低优先级之间的中等优先级任务就绪,那么这个中等优先级任务就会抢占低优先级任务,那么,这三个任务中,高优先级任务反而要最后执行,这就是二值信号量带来的优先级反转问题。
FreeRTOS计数型信号量
计数型信号量和二值信号量类似,二值信号量相当于队列长度为1 的队列,因此二值信号量只能容纳一个资源,这也是为什么命名为二值信号量,而计数型信号量相当于队列长度大于0的队列,因此计数型信号零能容纳多个资源,这是在计数型信号量被创建的时候确定的。
计数型信号量适用于下面两种场合:
事件计数
在这种场合下,每次事件发生后,在事件处理函数中释放计数型信号量(计数型信号量的资源数加1),其他等待事件发生的任务 获取计数型信号量(计数型信号量的资源数减1),这么以来等待事件发生的任务就可以在成功获取到计数型信号量之后执行相应的操作,在这种场合下,计数型信号量的资源数一般在创建时设置为0。
资源管理
在这种场合下,计数型信号量的资源数代表着共享资源的可用数量,例如前面举例中的停车场中的空车位。一个任务想要访问共享资源,就必须先获取这个共享资源的计数型信号量,之后在成功获取了计数型信号量之后,才可以对这个共享资源进行操作,当然,在使用完共享资源之后也要释放这个共享资源的计数型信号量。在这种场合下,计数型信号量的资源数一般在创建时设置为受其管理的共享资源的最大可用数量。
信号量的特性
信号量的常规操作
信号量这个名字起的很恰当:
信号:起通知作用
量:还可以表示资源的数量
当“量”没有限制时,它就是“计数型信号量”(Counting Semaphores)
当“量”只有0、1两个取值时,它就是“二值信号量”(Binary Seamphores)
支持的动作:“give”给出资源,计数值加1,“take”获得资源,计数值减1
计数信号量的典型场景是:
事件计数:事件产生时“give"信号量,让计数值加1;处理事件时要先”take“信号量,也就是获得信号量,让计数值减1
资源管理:要想访问资源需要先”take“信号量,让计数值减1,用完资源后”give“信号量,让计数值加1
信号量的”give“、”take“双方并不需要相同,可以用于生产者-消费者场合;
生产者为任务A,B,消费者为任务C,D
一开始信号量的计数值为0,如果任务C,D想获得信号量,会有两种结果:
阻塞:买不到东西咱就等等吧,可以定个闹钟(超时时间)
即可返回失败:不等
任务A,B可以生产资源,就是让信号量的计数值增加1,并且把等待这个资源的顾客唤醒
唤醒谁?谁优先级高就唤醒谁,如果大家优先级都一样,就唤醒等待时间最长的人
二值信号量和计数型信号量的唯一差别:就是计数值的最大值被限定为1
信号量跟队列的对比
队列 | 信号量 |
可以容纳多个数据,创建队列时有2部分内存:队列结构体、存储数据的空间 | 只有计数值,无法容纳其他数据 创建信号量时,只需要分配信号量结构体 |
生产者:没有空间存入数据时可以阻塞 | 生产者:用于不阻塞,计数值已经达到最大值时返回失败 |
消费者:没有数据时可以阻塞 | 消费者:没有资源时可以阻塞 |
两种信号量对比
二进制信号量 | 计数型信号量 |
被创建时初始值为0 | 被创建时初始值可以设定 |
其他操作是一样的 | 其他操作是一样的 |
二值信号量
二值信号量的本质是一个队列长度为1的队列,该队列就只有空和满两种情况,这就是二值。
二值信号量通常用于互斥访问或任务同步,与互斥信号量比较类似,但是二值信号量有可能会导致优先级翻转的问题,所以二值信号量更适合用于同步。
二值信号量相关API函数
使用二值信号量的过程:创建二值信号量->释放二值信号量->获取二值信号量
使用信号量时,先创建,然后去添加资源、获得资源,使用句柄来表示一个信号量
函数 | 描述 |
xSemaphoreCreateBinary() | 使用动态方式创建二值信号量 |
xSemaphoreCreateBinaryStatic() | 使用静态方式创建二值信号量 |
xSemaphoreGive() | 释放二值信号量 |
xSemaphoreGiveFromISR() | 在中断中释放信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreTakeFromISR() | 在中断中获取信号量 |
这些函数其实都可以在semphr.h头文件中找到,它们都是一些宏定义,本质上用的其实都是Queue的函数
创建二值信号量
/*
创建一个二进制信号量,返回它的句柄
此函数内部会分配信号量结构体
返回值:返回句柄,非NULL表示成功
可以看出,动态创建二值信号量实际上是调用了函数xQueueGenericCreate()
创建了一个队列长度为1且队列项目大小为信号量队列项目大小的二值信号量类型队列
*/
#define xSemaphoreCreateBinary()
xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
/*
创建一个二进制信号量,返回它的句柄
此函数无需动态分配内存,所以需要现有一个StaticSemaphore_t结构体,并传入它的指针
返回值:返回句柄,非NULL表示成功
从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreCreateStatic() 实 际 上 是 调 用 了 函 数
xQueueGenericCreateStatic()创建了一个队列长度为 1 且队列项目大小为信号量队列项目大小的
二值信号量类型队列,需要用户手动分配提供创建二值信号量的内存
*/
#define xSemaphoreCreateBinaryStatic( pxStaticSemaphore )
xQueueGenericCreateStatic( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, NULL, pxStaticSemaphore, queueQUEUE_TYPE_BINARY_SEMAPHORE )
获取信号量
/*
此函数用于获取信号量,如果信号量处于没有资源的状态,那么此函数可以选择将任务进
行阻塞,如果成功获取了信号量,那信号量的资源数将会减 1。该函数实际上是一个宏定义
*/
#define xSemaphoreTake( xSemaphore, xBlockTime )
xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )
BaseType_t xQueueSemaphoreTake( QueueHandle_t xQueue,
TickType_t xTicksToWait )
{
BaseType_t xEntryTimeSet = pdFALSE;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = xQueue;
#if ( configUSE_MUTEXES == 1 )
BaseType_t xInheritanceOccurred = pdFALSE;
#endif
/* Check the queue pointer is not NULL. */
configASSERT( ( pxQueue ) );
/* 信号量类型队列的项目大小为0 */
configASSERT( pxQueue->uxItemSize == 0 );
/* Cannot block if the scheduler is suspended. */
#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
{
configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
}
#endif
/*lint -save -e904 This function relaxes the coding standard somewhat to allow return
* statements within the function itself. This is done in the interest
* of execution time efficiency. */
for( ; ; )
{ /*进入临界区*/
taskENTER_CRITICAL();
{
/* 获取信号量的资源数 */
const UBaseType_t uxSemaphoreCount = pxQueue->uxMessagesWaiting;
/* 判断信号量是否有资源 */
if( uxSemaphoreCount > ( UBaseType_t ) 0 )
{
traceQUEUE_RECEIVE( pxQueue );
/* 更新信号量的资源数 */
pxQueue->uxMessagesWaiting = uxSemaphoreCount - ( UBaseType_t ) 1;
#if ( configUSE_MUTEXES == 1 )
{
if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
{
/* 设置互斥信号量的持有者并更新互斥信号量的持有次数 */
pxQueue->u.xSemaphore.xMutexHolder = pvTaskIncrementMutexHeldCount();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_MUTEXES */
/* 判断信号量的获取阻塞任务列表中是否有任务 */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
/*将阻塞任务从信号量获取阻塞任务列表中移除*/
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
/*根据需要进行任务切换*/
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
taskEXIT_CRITICAL();
return pdPASS;
}
/*信号量没有资源*/
else
{
/*判断是否不选择阻塞等待信号量*/
if( xTicksToWait == ( TickType_t ) 0 )
{
/* 此宏用于启用互斥信号量 */
#if ( configUSE_MUTEXES == 1 )
{
configASSERT( xInheritanceOccurred == pdFALSE );
}
#endif /* configUSE_MUTEXES */
/* 退出临界区*/
taskEXIT_CRITICAL();
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
/*选择阻塞等待信号量*/
else if( xEntryTimeSet == pdFALSE )
{
/* 队列满,任务需要阻塞
记录此时系统节拍计数器 值和溢出次数
用于下面对阻塞时间进行补偿
*/
vTaskInternalSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
else
{
/* Entry time was already set. */
mtCOVERAGE_TEST_MARKER();
}
}
}
taskEXIT_CRITICAL();
/* 挂起任务调度器 */
vTaskSuspendAll();
/*信号量队列上锁*/
prvLockQueue( pxQueue );
/* 判断阻塞时间补偿后,是否还需要阻塞 */
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
{
/* 判断队列是否为空 */
if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
{
traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );
#if ( configUSE_MUTEXES == 1 )
{
if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
{
taskENTER_CRITICAL();
{
/*进行优先级继承,这是互斥信号量用于解决优先级翻转问题的*/
xInheritanceOccurred = xTaskPriorityInherit( pxQueue->u.xSemaphore.xMutexHolder );
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* if ( configUSE_MUTEXES == 1 ) */
/*将任务添加到队列写入阻塞任务列表中进行阻塞*/
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
prvUnlockQueue( pxQueue );
/*恢复任务调度器*/
if( xTaskResumeAll() == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/*队列不为空*/
else
{
/* There was no timeout and the semaphore count was not 0, so
* attempt to take the semaphore again. */
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
}
}
else
{
/* Timed out. */
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
/* If the semaphore count is 0 exit now as the timeout has
* expired. Otherwise return to attempt to take the semaphore that is
* known to be available. As semaphores are implemented by queues the
* queue being empty is equivalent to the semaphore count being 0. */
if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
{
#if ( configUSE_MUTEXES == 1 )
{
/* xInheritanceOccurred could only have be set if
* pxQueue->uxQueueType == queueQUEUE_IS_MUTEX so no need to
* test the mutex type again to check it is actually a mutex. */
if( xInheritanceOccurred != pdFALSE )
{
taskENTER_CRITICAL();
{
UBaseType_t uxHighestWaitingPriority;
/* This task blocking on the mutex caused another
* task to inherit this task's priority. Now this task
* has timed out the priority should be disinherited
* again, but only as low as the next highest priority
* task that is waiting for the same mutex. */
uxHighestWaitingPriority = prvGetDisinheritPriorityAfterTimeout( pxQueue );
vTaskPriorityDisinheritAfterTimeout( pxQueue->u.xSemaphore.xMutexHolder, uxHighestWaitingPriority );
}
taskEXIT_CRITICAL();
}
}
#endif /* configUSE_MUTEXES */
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
} /*lint -restore */
}
释放信号量
#define xSemaphoreGive( xSemaphore )
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
这里其实和上面的获取是类似的,碍于篇幅有限,不给出源码。
在任务中使用 | 在ISR中使用 | |
give | xSemaphoreGive | xSemaphoreGiveFromISR |
take | xSemaphoreTake | xSemaphoreTakeFromISR |
删除
对于动态创建的信号量,不再需要使用它们时,可以删除它们以回收内存
vSemaphoreDelete可以删除二值信号量,计数型信号量,函数原型如下:
#define vSemaphoreDelete( xSemaphore )
vQueueDelete( ( QueueHandle_t ) ( xSemaphore ) )
计数型信号量
计数型信号量相当于队列长度大于1的队列,因此计数型信号量能够容纳多个资源,这在计数型信号量被创建的时候确定。
计数型信号量相关API函数
函数 | 描述 |
xSemaphoreCreateCounting() | 使用动态方法创建计数型信号量 |
xSemaphoreCreateCountingStatic() | 使用静态方法创建计数型信号量 |
uxSemaphoreGetCount() | 获取信号量的计数值 |
计数值信号量的获取和释放和二值信号量的相同,这里不再赘述。
计数型信号量创建
/*
uxMaxCount:计数值的最大值限定
uxInitialCount:计数值的初始值
NULL:创建失败
其他值:创建成功返回计数型信号量的句柄
*/
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount )
xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
获取信号量当前计数值大小
/*
xSemaphore:信号量句柄
返回值:整数,当前信号量的计数值大小
*/
#define uxSemaphoreGetCount( xSemaphore )
uxQueueMessagesWaiting( ( QueueHandle_t ) ( xSemaphore ) )
个人总结
信号量是一种特殊的队列,其中二值信号量是长度为1的队列,计数型信号量是长度大于1的队列
使用信号量,只是传递一个状态,而不是像队列一样可以缓存数据,然后进行数据交流,信号量相比于队列更加轻量化,只需要创建信号量结构体,队列不仅需要申请创建队列的结构体还包括存储数据的缓存区。
二值信号量其实是一种特殊的计数型信号量,只有0和1两个计数值,常常用于互斥访问和任务间同步,但是二值信号量会导致优先级翻转的问题,互斥量被引申出解决该问题,所以二值信号量最合适用于任务同步。
其实,不管是信号量,队列,互斥量,事件组等IPC(在线程通信)都是这样,都要先创建一个中间结构体,任务之间通过该结构体传输信息,进行交流,这是一种间接的任务之间通信方式。