下一个内容,信号量。
先包含头文件。
#include "freertos/semphr.h"
我们通过队列可以进行任务间的数据传递,也可以通过队列来控制任务间的同步。如果我只需要控制任务而不需要传递数据,那么我们完全可以用信号量来代替队列。
简单介绍一下信号量,它约等于是没有容量的队列,或者把它当成是一个计数器。我们对信号量的操作有加一和减一。
如果信号量当前的值为0并且我需要进行减一操作,那么会阻塞,直到其他任务对信号量进行加一操作。由此我们可以发现信号量和队列的共性,信号量的加一操作就类似于队列的塞数据,信号量的减一操作就类似于队列的取数据,区别在于信号量没存数据,我们只是用它控制任务的同步。
可能有小伙伴会好奇,我直接整个全局变量去控制不就好了嘛,为什么非得用信号量。
全局变量来当信号量看起来好像没什么问题,但是确实是有问题的。
我们使用全局变量++或是全局变量--的时候,我们敲的代码是一行的,但是我们的代码终究是要转成汇编代码然后再转成机器码的,转成汇编代码的时候,我们原本一行代码的全局变量++就会变成三行,如果我这三行代码执行到一半的时候,任务切换了,导致我们未能成功将全局变量++,往小了说影响我们功能的实现,往大了说可能直接导致程序报错。
而我们的信号量属于原子操作,原子操作顾名思义就类似于原子,不能再分割了(也许吧,这属于物理范畴我不了解),也就是说当我们进行原子操作的时候,一定会执行完再切换任务,我们对信号量的操作要么一点不动,要么一定完成。
信号量我们先介绍两种,一种是计数信号量,一种是二进制信号量。差别就在于计数信号量的上限我们可以指定(最多计到多少数),而二进制信号量的上限就是1,因此二进制信号量只有两种状态,一个是空另一个是满。
我们使用下面这个宏去创建计数信号量。
xSemaphoreCreateCounting(uxMaxCount, uxInitialCount)
传入的是信号量最大值和初始值。
我们再细看一下这个宏可以发现,本质上创建信号量和创建队列是一样的。
所以理论上我们拿一个队列句柄类型的变量去接收创建得到的信号量也是可以的,并且队列集中也是可以存放信号量的,因为它们根本就是一个东西嘛。
但我们最好还是用SemaphoreHandle_t这个来接收信号量,这样见名知意。
删除信号量使用下面这个宏。
vSemaphoreDelete(xSemaphore)
宏里调用的也是删除队列的函数。
那剩下就是对信号量进行加一或减一操作了,或者换个大家比较熟悉的说法——PV操作。
先是加一操作,这里有个更形象的名字,give给也就是加一。
xSemaphoreGive(xSemaphore)
接下来是减一操作。名字也很形象,take拿也就是减一。
xSemaphoreTake(xSemaphore, xBlockTime)
上面无论是take还是give都有中断版本,也就是在函数后面加上FromISR,这边就不再介绍了。
另外还要提一下信号量和队列的另一个差别,那就是通用是塞东西,如果队列满了暂时塞不了,那么会有等待时间(进入阻塞),等待塞进去。而信号量加一超过了上限,那么直接返回错误,而不会进入阻塞等待能够加一。
信号量比较简单就不演示了,下一个介绍一下互斥锁。
头文件跟信号量是同一个,它们也有很多相似的地方,使用起来基本一样,创建——加一(上锁)——减一(开锁)——销毁。
甚至在一般情况下,我们可以把互斥锁看作是二进制信号量来使用。
互斥锁的开锁(give)和上锁(take)以及销毁用的函数都和信号量是一样的,因此我们在额外介绍一下互斥锁的创建以及互斥锁和二进制信号量的差别即可。
下面这个宏创建互斥锁。
xSemaphoreCreateMutex()
从下面这个官方示例可以得知互斥锁的句柄类型和信号量是一样的。
点开这个宏往上翻一翻可以发现,互斥锁,信号量,队列本质上是一个玩意,不过在创建的时候给我们默认加了个参数来指定类型为互斥锁,差别就在这边了。
在应用层上我们有个规定,那就是谁上锁谁开锁,而不能任务A上锁,然后让任务B开锁,这也是互斥锁和信号量的差别。
信号量更适用于控制数量有多个的资源,而互斥锁更适用于控制只有一个的核心资源。
假设我们只有一个核心资源,而任务A和任务B都需要用到,我们使用互斥锁来保证核心资源的使用,那么我们应该在任务中做到在使用前上锁,保证在使用过程中不会被其他任务干扰,在使用后开锁,保证之后能让其他任务使用。
互斥锁在使用上take和give是在同一个任务中使用的,而我们之前使用信号量,基本上take和give是分任务使用的。
一个简单的小例子大家就懂了。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
SemaphoreHandle_t mutex;
void test1(void*) {
while (1) {
xSemaphoreTake(mutex,0);
// 假设在这边使用核心资源
xSemaphoreGive(mutex);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void test2(void*) {
while (1) {
xSemaphoreTake(mutex,0);
// 假设在这边使用核心资源
xSemaphoreGive(mutex);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void) {
TaskHandle_t test1_handle;
TickType_t curTime = xTaskGetTickCount();
mutex = xSemaphoreCreateMutex();
xTaskCreate(test1, "test1", 1024 * 2, NULL, configMAX_PRIORITIES /2,&test1_handle);
xTaskCreate(test2, "test2", 1024 * 2, NULL, configMAX_PRIORITIES /2,NULL);
while (1) {
xTaskDelayUntil(&curTime, pdMS_TO_TICKS(1000));
}
}
那么大家或许会有疑问,既然信号量和互斥锁是一个东西(句柄类型一样,不过其实在底层有不同),只是在使用层面上不同,那为什么要有互斥锁呢?我用二进制信号量貌似也能代替互斥锁。
那我们想象一个场景,我们创建了很多优先级不一样的任务,优先级高的任务更容易拿到时间片,也就更容易执行,如果高优先级任务和低优先级任务共用一个核心资源,并且我们使用二进制信号量来控制,当低优先级任务使用核心资源的时候,将信号量减一,高优先级任务想要使用这个核心资源的时候就只能等着,但是由于低优先级任务执行的概率比较低,因此尽管高优先级的优先级比较高,但还是得不到执行,这就导致了“饥饿”现象。
而互斥锁有个优先级继承机制,当高优先级任务请求一个被低优先级任务持有的互斥锁时,系统会将持有锁的低优先级任务的优先级临时提升到与高优先级任务相同的级别。这样,持有锁的任务能够更快地完成其执行并释放锁,从而使高优先级任务能够继续执行。
这就是互斥锁和信号量在本质上的差别。
互斥锁更适合用于需要严格互斥访问共享资源的场景。由于具有优先级继承机制,它能够在一定程度上减少因任务优先级不同而导致的死锁或延迟问题。
而二进制信号量更适用于任务间或任务与中断间的同步。例如,当一个中断发生时,可以通过释放一个二进制信号量来通知一个或多个任务进行相应处理。由于二进制信号量相对简单且没有优先级继承机制的开销,因此它在某些同步场景中可能更高效。