目录
一、简介
FreeRTOS中的信号量(Semaphore)是一种实现任务间通信的机制,用于任务同步和资源管理。信号量可以防止任务之间发生竞态条件(race condition),并确保系统中多个任务或中断安全地共享资源。
信号量主要可以分为:
- 二值信号量
- 计数信号量
- 互斥信号量
- 递归信号量
每种信号量都有不同的使用场景,本文将详细介绍这几种信号量。
二、相关API
与信号量相关的操作主要有:
- 创建信号量
- 获取信号量
- 释放信号量
- 删除信号量
1.创建二值信号量
- xSemaphoreGreateBinary()
- 该函数返回一个句柄SemaphoreHandle_t ,无参数,使用该函数创建的二值信号量是空的(为0,不可用)。
- 二值信号量、计数信号量和互斥信号量都使用同一类型的句柄SemaphoreHandle_t ,该句柄的原型是一个 void 型的指针。在使用函数 xSemaphoreTake()获取之前必须先调用函数 xSemaphoreGive()释放后才可以获取,因为调用该函数所得的二值信号量初始化为0了。
2.创建计数信号量
- 参数有计数信号量的最大值和初始值,创建成功则返回信号量句柄。
3.创建互斥信号量
- xSemaphoreCreateMutex()
- 无参数,返回一个信号量句柄。
4.释放信号量
- xSemaphoreGive(xSemaphore)
- 参数为信号量句柄,若信号量未满,返回释放成功信息(pdPASS);如果信号量已满,则返回错误代码(err_QUEUE_FULL)。
- 该函数可以用于二值信号量、计数信号量、互斥量的释放。
5.带中断保护的释放信号量
- xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken)
- 参数为信号量句柄,pxHigherPriorityTaskWoken可以设置为NULL。
- 被释放的信号量可以是二进制信号量和计数信号量。和普通版本的释放信号量 API 函数有些许不同,它不能释放互斥信号量,这是因为互斥信号量不可以在中断中使用。
6.获取信号量
- 参数有信号量句柄和阻塞时间(0~portMAX_DELAY)。
- 获取的信号量对象可以是二值信号量、计数信号量和互斥信号量。
7.带中断保护的获取信号量
- 信号量类型可以是二值信号量和计数信号量,它与xSemaphoreTake()函数不同,它不能用于获取互斥信号量,因为互斥信号量不可以在中断中使用 。
8.删除信号量
- vSemaphoreDelete()用于删除一个信号量,包括二值信号量,计数信号量和互斥信号量。如果有任务阻塞在该信号量上,那么不要删除该信号量。
三、信号量控制块
信号量 API 函数实际上都是宏,它使用现有的队列机制,这些宏定义在semphr.h 文件中,如果使用信号量或者互斥量,需要包含semphr.h 头文件。所以 FreeRTOS 的信号量控制块结构体与消息队列结构体是一模一样的,只不过结构体中某些成员变量代表的含义不一样而已。
四、 二值信号量
FreeRTOS 中的二值信号量(Binary Semaphore)是一种非常常用的同步机制,主要用于任务之间的同步或者任务与中断之间的同步,并且在某些场景下可以作为一个简单的资源保护机制。它只有两种状态:0和1,因此被称为“二值信号量”。
二值信号量和互斥信号量非常相似,但是有一些细微差别:互斥信号量有优先级继承机制,二值信号量则没有这个机制。这使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步),而互斥量更偏向应用于临界资源的访问。
二值信号量可以应用于:任务与任务之间的同步、任务与中断之间的同步以及资源保护。
下面是任务与任务之间通信、实现同步的一个示例:
SemaphoreHandle_t xBinarySemaphore; //定义一个信号量句柄
xBinarySemaphore = xSemaphoreCreateBinary(); //创建一个二值信号量,初值为0
void vInputTask(void *pvParameters) //任务1,用于处理输入
{
while (1)
{
// 处理输入
// ...
// 通知输出任务数据已准备好
xSemaphoreGive(xBinarySemaphore); //释放二值信号量,值+1,表示输入数据已处理完毕
}
}
void vOutputTask(void *pvParameters) //任务2,用于处理输出
{
while (1)
{
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE)
{
//已获取到二值信号量,二值信号量值-1,可以开始处理输出
}
}
}
五、计数信号量
FreeRTOS中的计数信号量(Counting Semaphore) 是一种强大而灵活的同步机制。与二值信号量不同,计数信号量可以在信号量内部维护一个计数器,这个计数器可以递增或递减,并且它的取值范围不是仅仅限制在0和1,而是可以任意配置。
计数信号量可以理解为一个允许多次释放(值+1)和获取(值-1)的信号量。它的核心是一个计数器,表示可用信号量的数量。 计数信号量通常用于管理多个资源、多个事件的计数以及控制多任务并发。
下面是一个控制多任务并发的示例:
SemaphoreHandle_t xCountingSemaphore; //定义一个信号量句柄
xCountingSemaphore = xSemaphoreCreateCounting(5, 5); //创建一个计数信号量
void vLimitedTask(void *pvParameters)
{
while (1)
{
if (xSemaphoreTake(xCountingSemaphore, portMAX_DELAY) == pdTRUE)
{ //等待获取信号量
//已获取信号量,可以访问某种资源
xSemaphoreGive(xCountingSemaphore); //任务完成,释放信号量
}
}
}
上述代码中创建了一个最多5个信号量、初始值为5的计数信号量,这意味着像上述访问资源的任务最多同时只能有5个,超过5个之后的任务想获取信号量以便能够访问资源时都将被阻塞。类似的,二值信号量最大值为1,同时只能有一个任务获取到信号量并访问资源。
六、互斥信号量
FreeRTOS中的互斥信号量(Mutex) 是一种用于实现任务之间互斥访问共享资源的同步机制。互斥信号量的主要目的是确保同一时刻只有一个任务可以访问某个共享资源,从而防止资源竞争和数据不一致的问题。它是 FreeRTOS 提供的一种非常常用的任务同步工具,尤其适用于需要保护共享数据或临界区的场景。互斥信号量常简称互斥量。
互斥信号量类似于二值信号量,但它专门设计用于管理资源的互斥访问。与普通二值信号量的主要区别在于,互斥信号量包含任务优先级继承机制,这使它更适合解决共享资源引发的优先级反转问题。
- 优先级反转问题:当一个低优先级的任务持有资源,而一个高优先级的任务被阻塞等待该资源时,这时就发生了优先级反转的问题,即高优先级任务无法运行而低优先级任务可以运行的情况。这个时候,如果又出现了一个中优先级的任务时,低优先级的任务被中优先级的任务给打断了,导致资源久久不能释放,使得高优先级的任务等待的时间更长了,这时,优先级反转问题的危害加重了。
- 优先级继承机制:当低优先级的任务持有互斥信号量时,如果有一个高优先级的任务也请求该信号量,系统将暂时提升低优先级任务的优先级到与高优先级任务的优先级相同,从而加快低优先级任务释放资源的速度。这时,中优先级的任务将不能再打断原本的低优先级任务的运行,从而降低了优先级反转问题的危害。
下面是一个实现任务之间互斥访问资源的示例:
SemaphoreHandle_t xMutex; //定义一个信号量句柄
xMutex = xSemaphoreCreateMutex(); //创建一个互斥信号量
void vTask1(void *pvParameters) //任务1
{
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE)
{
// 进入临界区
// 访问共享资源
// 离开临界区后释放互斥信号量
xSemaphoreGive(xMutex);
}
}
//当任务1获取了互斥量后,任务2将不能访问相应的共享资源
//等任务1释放了互斥量后,任务2才能获取信号量并访问共享资源
//这保证了共享资源同一时刻只能有一个任务在访问
void vTask2(void *pvParameters) //任务2
{
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE)
{
// 进入临界区
// 访问共享资源
// 离开临界区后释放互斥信号量
xSemaphoreGive(xMutex);
}
}
七、递归信号量
除了上述三种信号量外,FreeRTOS还有一种递归信号量,或者说递归互斥信号量,其也有优先级继承机制,相当于一个互斥量。只不过上面说到的互斥量不能被一个任务多次获取,但是一个递归互斥信号量可以被一个任务多次获取,注意获取了多少次就要释放多少次。
在某些情况下,一个任务可能会在一个函数中获取互斥信号量,然后调用另一个也需要获取该信号量的函数。普通的互斥信号量在这种情况下会导致死锁,因为同一个任务无法获取已经持有的互斥信号量。而递归互斥信号量允许任务多次获取同一个信号量,只要每次获取都在离开临界区之前进行相应的释放。
以下是递归互斥信号量的常用API:
- 递归互斥量创建函数xSemaphoreCreateRecursiveMutex(),无参数,返回信号量句柄。
- 递归互斥量获取函数xSemaphoreTakeRecursive(xMutex, xBlockTime),参数是递归互斥信号量句柄xMutex和阻塞时间xBlockTime,获取成功返回pdTRUE,失败返回errQUEUE_EMPTY.
- 递归互斥量释放函数xSemaphoreGiveRecursive(xMutex),参数是递归互斥信号量句柄xMutex。
- 删除函数如前所述。
以下是递归互斥信号量使用的一个示例:
SemaphoreHandle_t xRecursiveMutex; //定义一个信号量句柄
xRecursiveMutex = xSemaphoreCreateRecursiveMutex(); //创建递归互斥信号量
void vTask(void *pvParameters)
{
if (xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY) == pdTRUE)
{ //第一次获取递归互斥信号量
// 进入临界区
// 临界区代码
if (xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY) == pdTRUE)
{ //再次获取递归互斥信号量
// 进入嵌套临界区
// 临界区代码
xSemaphoreGiveRecursive(xRecursiveMutex); //释放第二次获取的信号量
}
xSemaphoreGiveRecursive(xRecursiveMutex); //释放第一次获取的信号量
}
}