14.信号量
信号量是一种实现任务间通信的机制,可以实现恩物之间同步或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。
限号量是一个非负整数,所有获取它的任务都会将该整数减1,当整数值为0时,所有试图获取它的任务都将处于阻塞状态。通常一个信号量的数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。
14.1 基本概念
14.1.1 二值信号量
二值信号量可以用于临界资源的访问也可以用于同步功能,与互斥信号量相比互斥信号量更适合用于临界资源的访问,而二值信号量则更适合于同步功能(任务与任务键的同步,或任务和中断间的同步)。
用作同步时信号量在创建后应被设置为空,任务1获取信号量而进入阻塞状态,任务2(或者中断)在某种条件发生后释放信号量,于是任务1获得信号量得以进入就绪状态。
可以将二值信号量看做只有一个消息的队列,因此这个队列只能为空或满,在运用时只需要知道队列中是否有消息即可,而无需关注消息是什么
应用场景:
在多任务系统中,我们经常会用到二值信号量,比如某个任务需要等待一个标记,那么任务可以在轮询中查询这个标记有没有被置位,但是这样会很消耗CPU资源并且妨碍其他任务执行。更好的做法是使任务的大部分时间处于阻塞状态(允许其他任务执行),直到某些事件发生,该任务才被唤醒去执行。这样CPU的效率可以大大提高,而且实时响应也是最快的。
假设有一个温湿度传感器,每1s采集一次数据,那么让它在液晶屏中显示出数据,这个周期也是1s,如果液晶屏刷新的周期是100ms那么此时的温湿度数据还没更新,液晶屏根本无需刷新,只需要在1s后温湿度数据更新是刷新即可,否则就是白白做了多次无效的数据更新操作,cpu的资源被刷新数据这个任务占用了大半,造成cpu资源浪费。如果液晶屏刷新的周期是10s,那么温湿度数据都变化了10次液晶屏才来更新数据,那么这个产品测得的结果就是不准确的,所以还是需要同步协调工作。在温湿度采集完毕之后进行液晶屏数据的刷新,这样得到的结果是最准确的并且不会浪费CPU的资源。
二值信号量运作机制:
创建信号量是系统会为创建的信号量对象分配内存,并且把可用信号量初始化为用户定义的个数,二值信号量的最大可用信号量个数是1。
任何任务都可以从创建的二值信号量资源中获取一个二值信号量,获取成功则返回正确信息,否则任务会根据用户指定额阻塞超时时间来等待其他任务/中断释放信号量。
14.1.2 计数信号量
计数信号量是用于计数的,也可以被看做是一个长度大于1的队列,信号量的使用者不用关心存储在队列中的消息,只需要关心队列中是否有消息即可。我们长江计数信号量用于事件计数与资源管理。信号量的计数值可以表示系统中可用的资源数目。
计数信号量允许多个任务对其进行操作,但限制了任务的数量。比如有一个停车场,里面只有100个车位,那么只能听100辆车,相当于我们的信号量有一百个。每进去一辆车就消耗一个停车位,当停满的100辆车时,此时的停车位数量为0,再来的车就不能再进去了,除非等有车出来(有任务再释放信号量)才能再进去。
14.1.3 互斥信号量
互斥量是一种特殊的二值信号量,互斥量支持互斥量所有权以及防止优先级反转的特性。用于实现对于临界资源的独占式处理。
互斥量的状态只有两种——开锁或闭锁。当互斥量被任务持有时,该互斥量处于闭锁状态,任务获得互斥量的所有权。当任务释放互斥量是,该互斥量处于开锁状态,任务失去对互斥量的所有权。当一个任务持有互斥量是,其他任务不能再对该互斥量进行开锁或持有。互斥量的特性与二值信号量几乎相同,但是互斥量的出现是为了解决一个问题:任务优先级翻转。互斥量可以通过优先级继承算法降低优先级翻转问题产生的影响。所以用于临界资源的保护时一般建议使用互斥量。
14.1.3.1优先级翻转
任务的优先级在创建时是已经设置好的,高优先级的任务可以打断低优先级的任务,抢占CPU的使用权。但是在很多场合中,某些资源只有一个,当低优先级任务正在占用该资源时,即便是高优先级任务,也只能等待低优先级任务使用完该资源后释放资源。这种高优先级任务无法运行而低优先级任务可以运行的现象称为“优先级翻转”。
优先级翻转会导致系统的高优先级任务阻塞时间过长。
举例: 现在有3个任务从,分别为H任务、M任务、L任务,三个任务的优先级顺序为H>M>L。假设系统中有一个资源被保护了,此时该资源被L任务使用,某一时刻H任务需要使用该资源,但是L任务还没使用完,H任务则因为申请不到资源而进入阻塞态,L任务继续使用该资源,此时就已经出现了优先级翻转的现象。但是可怕的还在后面。
如果L任务执行时刚好M任务被唤醒了,由于M任务优先级比L任务优先级高,那么会打断L任务,抢占CPU的使用权,直到M任务执行完,再把CPU使用权归还给L任务,L任务继续执行,等到执行完毕之后释放该资源,H任务此时才能解除阻塞态使用该资源。
**这个过程中优先级最高的H任务在等待优先级更低的L任务与M任务,其阻塞是的时间是M任务运行时间+L任务运行时间。**这只是只有3个任务的情况,如果有很多个这样的任务打断最低优先级任务,那么这个系统最高优先级任务就崩溃了。
14.1.3.2 优先级继承机制
为了减少优先级翻转的问题,使用了优先级继承算法。优先级继承算法是指,**暂时提高某个占有某种资源的低优先级任务的优先级,使之与所有等待该资源的任务中优先级最高的那个任务的优先级相等,而当这个低优先级任务执行完毕释放该资源时,优先级重新回到初始设定值。**这个优先级提升的过程叫做优先级继承。
FreeRTOS的优先级继承机制不能解决优先级翻转问题(因为低优先级还是会堵住高优先级),只能将这种情况的影响降到最低。因此我们需要在获得互斥量后就尽快释放互斥量
14.1.4 递归互斥量
当一个任务持有互斥量时持有该互斥量的任务也能够再次获得这个锁而不被挂起 ,这就是递归访问,也就是递归互斥量的特性。
14.2 信号量控制块
信号量的API函数实际上都是宏,使用现有的队列机制,这些宏在semphr.h文件中定义,如果使用信号量或者互斥量,则需要包含semphr.h头文件。
FreeRTOS信号量控制块结构体与消息队列结构体是一样的,只不过结构体中某些成员变量代表的含义不同
typedef struct QueueDefinition
{
int8_t *pcHead; /* 指向队列消息存储区起始位置,即第一个消息空间 */
int8_t *pcTail; /* 指向队列消息存储区结束位置地址 */
int8_t *pcWriteTo; /* 指向队列消息存储区下一个可用消息空间 */
union /* pcReadFrom与uxRecursiveCallCount是一对互斥变量,使用联合体来确保两个互斥的结构体成员不会同时出现。当结构体用于队列式,pcReadFrom指向出队消息空间的最后一个,也就是读取消息时是从pcReadFrom指向的空间读取消息内容 */
{
int8_t *pcReadFrom; /*< Points to the last place that a queued item was read from when the structure is used as a queue. */
UBaseType_t uxRecursiveCallCount; /* 当结构体用于互斥量时,uxRecursiveCallCount用于计数,记录递归互斥量被调用的次数 */
} u;
List_t xTasksWaitingToSend; /* 发送消息阻塞列表,用于保存阻塞在此队列的任务,任务按照优先级进行排序。 */
List_t xTasksWaitingToReceive; /* 获取消息阻塞列表,用于保存阻塞在此的队列任务,任务按照优先级进行排序。 */
volatile UBaseType_t uxMessagesWaiting; /* 如果控制块结构体是用于消息队列,则uxMessagesWaiting用来记录当前消 息队列的消息个数;如果控制块结构体被用于信号量时,则这个值表示有效信号量 的个数*/
UBaseType_t uxLength; /* 如果控制块结构体是用于消息队列表示队列的长度,如果控制块结构体被用于信号量时表 示最大的信号量可用个数 */
UBaseType_t uxItemSize; /* 如果用于消息队列则表示单个消息的大小,如果被用于信号量时,则无须分配储存空间, 为0即可 */
volatile int8_t cRxLock; /* 队列上锁后,存储从队列收到的列表项数目,也就是出队的数量;如果队列没有上锁,则设置为queueUNLOCKED */
volatile int8_t cTxLock; /* 队列上锁后,存储发送到队列的列表项数目,也就是入队的数量;如果队列没有上锁,则设置为queueUNLOCKED*/
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */
#endif
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition *pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
14.3 相关函数
14.3.1 二值信号量
14.3.1.1 xSemaphoreCreateBinary() 创建二值信号量
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, \ semSEMAPHORE_QUEUE_ITEM_LENGTH, \
queueQUEUE_TYPE_BINARY_SEMAPHORE )
#endif
注意事项
- xSemaphoreCreateBinary用于创建一个二值信号量,并返回一个句柄 句柄类型为:SemaphoreHandle_t
- 用该函数创建的二值信号量是空的,在使用xSemaphoreTake()函数获取之前必须得有调用xSemaphoreGive()释放信号量才能获取到。 如果使用老式的函数vSemphoreCreateBinary()创建的二值信号量为1.
- 要想使用该函数要将FreeRTOSConfig.h中的宏configSUPPORT_DYNAMIC_ALLOCATION定义为1
- 二值信号量的创建就是简介调用xQueueGenericCreate函数创建了一个长度为1的队列
14.3.1.2 vSemaphoreDelete() 删除信号量
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore) /* 信号量句柄 */
无返回值
14.3.1.3 xSemaphoreGive() 任务释放信号量
#define xSemaphoreGive( xSemaphore ) /* 参数为SemaphoreHandle_t类型,即要释放的信号量的句柄 */
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ),
NULL,
semGIVE_BLOCK_TIME,
queueSEND_TO_BACK )
返回值: 释放成功:返回pdPASS
释放失败:返回err_QUEUE_FULL
注意事项:
- 从宏定义可以看出,释放信号量实际上是一次入队操作,并且不允许入队阻塞,因为阻塞时间为semGIVE_BLOCK_TIME,该宏的值为0.
14.3.1.4 xSemaphoreGiveFromISR() 中断释放信号量
#define xSemaphoreGiveFromISR( xSemaphore, \ /* 信号量句柄 */
pxHigherPriorityTaskWoken\ /* 是否呼叫任务切换参数量,与之前的中断保护机制作 用相同 变量类型为BaseType_t */
) \
xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ),\
( pxHigherPriorityTaskWoken ) )
返回值: 释放成功:返回pdPASS
释放失败:返回err_QUEUE_FULL
注意事项:
- 一个或多个任务有可能阻塞在同一个信号量上,调用函数xSemaphoreGiveFromISR可能会唤醒阻塞在该信号量上的任务。如果被唤醒的任务的优先级大于当前任务的优先级,那么形参pxHigherPriorityTaskWoken就会被设置为pdTRUE,然后在中断退出前我们人为的执行一次上下文切换
14.3.1.4.1 使用实例
void vTestISR()
{
BaseType_t pxHigherPriorityTaskWoken;
uint32_t ulReturn;
/* 进入临界段,临界段可以嵌套 */
ulReturn = taskENTER_CRITICAL_FROM_ISR();
/* 判断是否产生中断 */
{
/* 如果产生中断,清除中断标志位 */
//释放二值信号量,发送接受到新数据标志,供前台程序查询
xSemaphoreGiveFromISR(BinarySem_Handle,&pxHigherPriorityTaskWoken);
//系统会判断是否需要进行任务 切换
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
}
}
我们之前是自己手动判断pxHigherPriorityTaskWoken的值,然后调用tskYIELD()进行切换,也可以用portYIELD_FROM_ISR来自动判断是否切换上下文,更加地方便。
14.3.1.5 xSemaphoreTake() 任务获取信号量
#define xSemaphoreTake( xSemaphore, \ /* 信号量句柄 类型为SemaphoreHandle_t */
xBlockTime )\ /* 等待信号量可用的最大超时时间,单位为系统节拍周期。如果宏 INCLUDE_vTaskSuspend定义为1且形参xBlockTime被设置为 portMAX_DELAY,则任务将一直阻塞在该信号量上 */
xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), \
NULL, ( xBlockTime ), \
pdFALSE )
返回值: 在指定超时时间中获取成功则返回pdTRUE,没有获取成功则返回errQUEUE_EMPTY
注意事项:
- 从该宏定义可以看出释放信号量实际上是一次消息出队操作,阻塞时间xBlockTime由用户指定
14.3.1.6 xSemaphoreTakeFromISR() 中断获取信号量
xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, /* 信号量句柄 */
signed BaseType_t * pxHigherPriorityTaskWoken
)
返回值:获取成功则返回pdTRUE,没有成功则返回errQUEUE_EMPTY。
注意事项: 总结了这么多次的中断中使用的函数,套路基本上都是那样的用法,pxHigherPriorityTaskWoken就用来判断是否来执行上下文切换。可参考上面中断释放任务量的pxHigherPriorityTaskWoken的使用
14.3.2 计数信号量
14.3.2.1 xSemaphoreCreateCounting()创建计数信号量
SemaphoreHandle_t xSemaphoreCreate(UBaseType_t uxMaxCount, /* 计数信号量的最大值,当达到这个值时,信号 量不能再被释放 */
UBaseType_t uxInitialCount /* 创建计数信号量的初始值 */
)
返回值: 如果创建成功,则返回一个计数信号量的句柄,用于访问创建的计数信号量
如果创建不成功,返回NULL
14.3.2.2 vSemaphoreDelete() 删除信号量
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore) /* 信号量句柄 */
无返回值
14.3.2.3 xSemaphoreGive() 任务释放信号量
#define xSemaphoreGive( xSemaphore ) /* 参数为SemaphoreHandle_t类型,即要释放的信号量的句柄 */
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ),
NULL,
semGIVE_BLOCK_TIME,
queueSEND_TO_BACK )
返回值: 释放成功:返回pdPASS
释放失败:返回err_QUEUE_FULL
注意事项:
- 从宏定义可以看出,释放信号量实际上是一次入队操作,并且不允许入队阻塞,因为阻塞时间为semGIVE_BLOCK_TIME,该宏的值为0.
14.3.2.4 xSemaphoreGiveFromISR() 中断释放信号量
#define xSemaphoreGiveFromISR( xSemaphore, \ /* 信号量句柄 */
pxHigherPriorityTaskWoken\ /* 是否呼叫任务切换参数量,与之前的中断保护机制作 用相同 变量类型为BaseType_t */
) \
xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ),\
( pxHigherPriorityTaskWoken ) )
返回值: 释放成功:返回pdPASS
释放失败:返回err_QUEUE_FULL
注意事项:
- 一个或多个任务有可能阻塞在同一个信号量上,调用函数xSemaphoreGiveFromISR可能会唤醒阻塞在该信号量上的任务。如果被唤醒的任务的优先级大于当前任务的优先级,那么形参pxHigherPriorityTaskWoken就会被设置为pdTRUE,然后在中断退出前我们人为的执行一次上下文切换
14.3.2.5 xSemaphoreTake() 任务获取信号量
#define xSemaphoreTake( xSemaphore, \ /* 信号量句柄 类型为SemaphoreHandle_t */
xBlockTime )\ /* 等待信号量可用的最大超时时间,单位为系统节拍周期。如果宏 INCLUDE_vTaskSuspend定义为1且形参xBlockTime被设置为 portMAX_DELAY,则任务将一直阻塞在该信号量上 */
xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), \
NULL, ( xBlockTime ), \
pdFALSE )
返回值: 在指定超时时间中获取成功则返回pdTRUE,没有获取成功则返回errQUEUE_EMPTY
注意事项:
- 从该宏定义可以看出释放信号量实际上是一次消息出队操作,阻塞时间xBlockTime由用户指定
14.3.2.6 xSemaphoreTakeFromISR() 中断获取信号量
xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, /* 信号量句柄 */
signed BaseType_t * pxHigherPriorityTaskWoken
)
返回值:获取成功则返回pdTRUE,没有成功则返回errQUEUE_EMPTY。
注意事项: 总结了这么多次的中断中使用的函数,套路基本上都是那样的用法,pxHigherPriorityTaskWoken就用来判断是否来执行上下文切换。可参考上面中断释放任务量的pxHigherPriorityTaskWoken的使用
14.3.3 互斥信号量
14.3.3.1 xSemaphoreCreateMutex() 创建互斥量
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
#endif
返回值:创建成功返回类型为SemaphoreHandle_t的信号量句柄
创建失败返回NULL
注意事项:
- 要使用该函数的时候需要将FreeRTOSConfig.h中的 configSUPPORT_DYNAMIC_ALLOCATION定义为1(一般默认都是1)
- 要将FreeRTOSConfig中的configUSE_MUTEXES宏定义打开
14.3.3.2 vSemaphoreDelete() 删除信号量
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore) /* 信号量句柄 */
无返回值
14.3.3.3 xSemaphoreGive() 任务释放信号量
#define xSemaphoreGive( xSemaphore ) /* 参数为SemaphoreHandle_t类型,即要释放的信号量的句柄 */
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ),
NULL,
semGIVE_BLOCK_TIME,
queueSEND_TO_BACK )
返回值: 释放成功:返回pdPASS
释放失败:返回err_QUEUE_FULL
注意事项:
- 从宏定义可以看出,释放信号量实际上是一次入队操作,并且不允许入队阻塞,因为阻塞时间为semGIVE_BLOCK_TIME,该宏的值为0.
- 该函数用来释放互斥量时需要将FreeRTOSConfig.h中的宏定义configUSE_RECURSIVE_MUTEXES定义为1
14.3.3.4 xSemaphoreTake() 任务获取信号量
#define xSemaphoreTake( xSemaphore, \ /* 信号量句柄 类型为SemaphoreHandle_t */
xBlockTime )\ /* 等待信号量可用的最大超时时间,单位为系统节拍周期。如果宏 INCLUDE_vTaskSuspend定义为1且形参xBlockTime被设置为 portMAX_DELAY,则任务将一直阻塞在该信号量上 */
xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), \
NULL, ( xBlockTime ), \
pdFALSE )
返回值: 在指定超时时间中获取成功则返回pdTRUE,没有获取成功则返回errQUEUE_EMPTY
注意事项:
- 从该宏定义可以看出释放信号量实际上是一次消息出队操作,阻塞时间xBlockTime由用户指定
14.3.4 递归互斥量
14.3.4.1 xSemaphoreCreateRecursiveMutex() 创建递归互斥量
#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configUSE_RECURSIVE_MUTEXES == 1 ) )
#define xSemaphoreCreateRecursiveMutex() xQueueCreateMutex( queueQUEUE_TYPE_RECURSIVE_MUTEX )
#endif
返回值:创建成功返回类型为SemaphoreHandle_t的信号量句柄
创建失败返回NULL
注意事项:
- 要使用该函数的时候需要将FreeRTOSConfig.h中的 configSUPPORT_DYNAMIC_ALLOCATION定义为1(一般默认都是1)
- 要将FreeRTOSConfig中的configUSE_MUTEXES宏定义打开
14.3.4.2 vSemaphoreDelete() 删除信号量
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore) /* 信号量句柄 */
无返回值
14.3.4.3 xSemaphoreGiveRecursive() 释放递归信号量
#if( configUSE_RECURSIVE_MUTEXES == 1 )
#define xSemaphoreGiveRecursive( xMutex ) /* 信号量句柄 */
xQueueGiveMutexRecursive( ( xMutex ) )
#endif
返回值: 释放成功:返回pdPASS
释放失败:返回err_QUEUE_FULL
注意事项:
- 使用xSemaphoreTakeRecursive()函数成功获取几次递归互斥量就需要使用xSemaphoreGiveRecursive函数返回几次,在此之前队规互斥量都处于无效状态,其他任务无法获取该递归互斥量
14.3.4.4 xSemaphoreTakeRecursive() 获取递归互斥量
#if( configUSE_RECURSIVE_MUTEXES == 1 )
#define xSemaphoreTakeRecursive( xMutex,\ /* 信号量句柄,变量类型为SemaphoreHandle_t */
xBlockTime )\ /* 如果不是持有互斥量额任务去获取无效的互斥量,那么任务将等待用户指定超时时间,单位为tick,如果宏定义INCLUDE_vTaskSuspend定义为1且形参设置为portMAX_DELAY,则任务将一直阻塞在该递归互斥量上 */
xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )
#endif
返回值: 在超市之前如果获取成功则返回pdTRUE 没有获取成功则返回errQUEUE_EMPTY
注意事项:
- 该函数不能用于获取由函数xSemphoreCreateMutex()创建的互斥量
- 如果想要使用该函数需要将FreeRTOSConfig.h中的宏定义configUSE_RECURSIVE_MUTEXES定义为1
14.4 总结
信号量实际上就是队列,各种不同的信号量就是简介调用队列相关的函数来实现不同信号量的相关功能。
在相关任务那一栏,可以发现,二值信号量和计数信号量的操作使用的函数唯一的差别就是创建信号量使用的函数不同,实际上四种信号量每种信号量都有自己专属的创建信号量函数。
信号量的删除都是通过vSemaphoreDelete函数。
互斥信号量的释放与获取同二值信号量、计数信号量调用的函数一样。但是递归信号量的释放与获取都有专门的函数来实现。但是要注意的是只要使用互斥量就要将FreeRTOSConfig.h中的宏定义configUSE_RECURSIVE_MUTEXES定义为1,这样释放与获取才能正常使用。
互斥量只能在任务中释放与获取,在中断中不能使用。