目录
一、信号量的特性
信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,其实信号量主要的功能就是实现任务之间的同步与互斥,实现的方式主要就是依靠队列(信号量是特殊的队列)的任务阻塞机制。
1、信号量跟队列的对比
差异列表如下:
队列 | 信号量 |
---|---|
可以容纳多个数据,创建队列时有两部分内存: 队列结构体、存储数据的空间 | 只有计数值,无法容纳其他数据。 创建信号量时,只需要分配信号量结构体 |
生产者:没有空间存入数据时可以阻塞 | 生产者:用于不阻塞,计数值已经达到最大时返回失败 |
消费者:没有数据时可以阻塞 | 消费者:没有资源时可以阻塞 |
由上面的表格可以看出:信号量相比队列更节省空间,因为实现同步与互斥不需要传递数据,所以信号量没有队列后面的环形存储区,信号量主要就是依靠计数值 uxMessagesWaiting
(在队列中表示队列现有消息个数,在信号量中表示有效信号量个数)。
其实,创建信号量就对应创建特殊队列,获取信号量就对应队列出队,释放信号量就对应队列入队,学好了队列就基本学好了信号量。
2、两种信号量的对比
信号量的计数值都有限制:限定了最大值。如果最大值被限定为 1,那么它就是二值信号量;如果最大值不是 1,它就是计数型信号量。
二值信号量 | 计算型信号量 |
---|---|
被创建时初始值为 0 | 被创建时初始值可以设定 |
其他操作是一样的 | 其他操作是一样的 |
二、信号量
首先来看一下信号的种类,相关的信号量函数放在第三章。
1、二值信号量
所谓二值信号量其实就是一个队列长度为1,没有数据存储器的队列,而二值则表示计数值uxMessagesWaiting只有0和1两种状态(就是队列空与队列满两种情况),uxMessagesWaiting在队列中表示队列中现有消息数量,而在信号量中则表示信号量的数量。
uxMessagesWaiting
为 0 表示:信号量资源被获取了.uxMessagesWaiting
为 1 表示:信号量资源被释放了
把这种只有 0 和 1 两种情况的信号量称之为二值信号量。
由于二值信号量就是特殊的队列,其实它的运转机制就是利用了队列的阻塞机,从而达到实现任务之间的同步与互斥(有优先级反转的缺陷)。
1.1 二值信号量用于同步
在多任务系统中,经常会使用二值信号量来实现任务之间或者任务与中断之间的同步,比如,某个任务需要等待一个标记,那么任务可以在轮询中查询这个标记有没有被置位,则任务在等待的过程也会消耗 CPU 的资源,如下所示:
// 任务一
void Task1Function(void *param)
{
volatile int i = 0;
while (1) {
for (i = 0; i < 10000000; ++i) {
sumj++;
}
flagCalcEnd = 1;
vTaskDelete(NULL);
}
}
// 任务二
void Task2Function(void *param)
{
while (1) {
if (flagCalcEnd)
printf("sum = %d\r\n", sum);
}
}
总体工作流程如下:任务二等待任务一(等待 flagCalcEnd
置一),计算完(sum 的值累加一百万次)。然后进行数据处理(这里简单打印 sum 的值)。
上面的代码看似没问题,其实存在有两个问题:
- 使用了全局变量
flagCalcEnd
,(如果同时读写flagCalcEnd
则会出问题)。 - 任务二在等待任务一计算完 sum 的值的过程中,任务二也会参与任务调度消耗 CPU 资源(假设只有这两个任务,优先级相同,且支持时间片轮转,则在任务一在计算 sum 值的过程中,任务一与任务二轮流执行相同时间片,只不过任务二就一直判断
flagCalcEnd
的值是否为1,相当于就是浪费 CPU 的资源)
所以二值信号量就可以解决这个问题,在任务一计算 sum 的值的过程中,任务二应该进入阻塞态让出 CPU 的使用权,在任务二阻塞期间任务一就可以独占 CPU 全速计算 sum 的值,代码如下所示:
// 任务一
void Task1Function(void *param)
{
volatile int i = 0;
while (1) {
for (i = 0; i < 10000000; ++i) {
sumj++;
}
// 等待 sum 计算完成释放信号量,信号量计数值 uxMessagesWaiting 加 1
xSemaphoreGive(xSemcalc);
vTaskDelete(NULL);
}
}
// 任务二
void Task2Function(void *param)
{
while (1) {
flagCalcEnd = 0;
// 若 sum 未计算完成,则获取信号量失败,任务会进入阻塞状态,其他任务得以调度
// 若 sum 计算完成(信号量为 1),则任务被唤醒 sum 得以打印
xSemaphoreTake(xSemcalc, portMAX_DELAY);
flagCalcEnd = 1;
printf("sum = %d\r\n", sum);
}
}
1.2 二值信号量用于互斥
我们在串口接收中,我们并不知道什么时候有数据发送过来(等数据过来标记一次),还有一个处理串口接收到的数据,在任务系统中不可能时时刻刻去判断是否有串口有数据过来(判断标志位),所以在这种情况下使用二值信号量是很好的办法,当没有数据到来的时候,任务就进入阻塞态,不参与任务的调度,等到数据到来了,释放一个二值信号量,任务就立即从阻塞态中解除,进入就绪态,然后运行的时候处理数据,这样子系统的资源就会很好的被利用起来。
二值信号量一般不用于任务之间的互斥(任务之间互斥的访问一个临界资源,同一时间只能一个任务可以使用),因为它有优先级反转的缺点,解决互斥的方式就是使用互斥信号量(具有优先级继承的机制能减少优先级反转的影响),关于优先级反转,优先级继承等下一讲讲互斥量的时候在讲。
2、计数信号量
计数值信号量也与二值信号量一样也是特殊的队列,二值信号量是长度为 1 的队列,而计数值信号量是长度大于 0 的队列,他们本质的区别就是应用场景不同:二值信号量常用于同步,计数值信号量常用于事件计数、资源管理,其实如果限定计数值信号量计数值最大值只能为 1 则就等同于二值信号量。
计数值信号量的应用场景:
- 事件计数
在这种场合下,每次事件发生后,在事件处理函数中释放计数型信号量(计数型信号量的资源数加 1),其他等待事件发生的任务获取计数型信号量(计数型信号量的资源数减 1),这种场景下,计数型信号量的资源数一般在创建时设置为 0。 - 资源管理
在这种场合下,计数型信号量的资源数代表着共享资源的可用数量,一个任务想要访问共享资源,就必须先获取这个共享资源的计数型信号量,之后在成功获取了计数型信号量之后,才可以对这个共享资源进行访问操作,当然,在使用完共享资源后也要释放这个共享资源的计数型信号量。在这种场合下,计数型信号量的资源数一般在创建时设置为受其管理的共享资源的最大可用数量。
三、信号量函数
使用信号量时,先创建、然后去添加资源、获得资源。使用句柄来表示一个信号量。
1、创建
使用信号量之前,要先创建,得到一个句柄;使用信号量时,要使用句柄来表明使用哪个信号量。对于二值信号量、计数型信号量,它们的创建函数不一样:
二值信号量 | 计数型信号量 | |
---|---|---|
动态创建 | xSemaphoreCreateBinary 计数值初始值为 0 | xSemaphoreCreateCounting |
vSemaphoreCreateBinary (过时了)计数值初始值为 1 | ||
静态创建 | xSemaphoreCreateBinaryStatic | xSemaphoreCreateCountingStatic |
创建二值信号量的函数原型如下:
/* 创建一个二值信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinary( void );
/* 创建一个二值信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t
*pxSemaphoreBuffer );
创建计数型信号量的函数原型如下:
/* 创建一个计数型信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* uxMaxCount: 最大计数值
* uxInitialCount: 初始计数值
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t
uxInitialCount);
/* 创建一个计数型信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* uxMaxCount: 最大计数值
* uxInitialCount: 初始计数值
* pxSemaphoreBuffer: StaticSemaphore_t结构体指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t
*pxSemaphoreBuffer );
2、删除
对于动态创建的信号量,不再需要它们时,可以删除它们以回收内存。vSemaphoreDelete
可以用来删除二值信号量、计数型信号量,函数原型如下:
/*
* xSemaphore: 信号量句柄,你要删除哪个信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
3、give/take
二值信号量、计数型信号量的 give、take 操作函数是一样的。这些函数也分为 2 个版本:给任务使用,给 ISR 使用。列表如下:
在任务中使用 | 在 ISR 中使用 | |
---|---|---|
give | xSemaphoreGive | xSemaphoreGiveFromISR |
take | xSemaphoreTake | xSemaphoreTakeFromISR |
xSemaphoreGive
的函数原型如下:
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
参数 | 说明 |
---|---|
xSemaphore | 信号量句柄,释放哪个信号量 |
返回值 | pdTRUE 表示成功, 如果二值信号量的计数值已经是 1,再次调用此函数则返回失败; 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败 |
xSemaphoreGiveFromISR
的函数原型如下:
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
参数 | 说明 |
---|---|
xSemaphore | 信号量句柄,释放哪个信号量 |
pxHigherPriorityTaskWoken | 如果释放信号量导致更高优先级的任务变为了就绪态, 则 *pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE 表示成功, 如果二值信号量的计数值已经是 1,再次调用此函数则返回失 败; 如果计数型信号量的计数值已经是最大值,再次调用此函数则返 回失败 |
xSemaphoreTake
的函数原型如下:
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
参数 | 说明 |
---|---|
xSemaphore | 信号量句柄,获取哪个信号量 |
xTicksToWait | 如果无法马上获得信号量,阻塞一会: 0:不阻塞,马上返回 portMAX_DELAY: 一直阻塞直到成功 其他值: 阻塞的 Tick 个数,可以使用 pdMS_TO_TICKS() 来指定阻塞时间为若干 ms |
返回值 | pdTRUE 表示成功 |
xSemaphoreTakeFromISR
的函数原型如下:
BaseType_t xSemaphoreTakeFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
参数 | 说明 |
---|---|
xSemaphore | 信号量句柄,获取哪个信号量 |
pxHigherPriorityTaskWoken | 如果获取信号量导致更高优先级的任务变为了就绪态, 则 *pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE 表示成功 |