信号量和互斥锁
信号量是一种多任务操作系统中用于实现任务同步和互斥的机制,通过控制任务对共享资源的访问,避免资源冲突并保证任务按预期顺序执行。
和队列类似,信号量也用于任务间的同步与通信,但信号量主要传递资源状态或事件通知,用于控制访问权限;而队列则用于传递具体数据,实现任务间的数据交换。
本文档仅涵盖信号量中部分常用 API,更多内容请参考 FreeRTOS 官方文档。
在 ESP-IDF 中调用队列相关 API 时,需要在文件中包含以下头文件:
#include "freertos/semphr.h"
创建二值信号量
API 原型
SemaphoreHandle_t xSemaphoreCreateBinary( void );
xSemaphoreCreateBinary()
用于创建一个二值信号量,初始状态为空(不可用),适用于任务间事件通知或简单的同步控制。
返回值 |
说明 |
---|---|
|
由于 FreeRTOS 可用堆空间不足,信号量创建失败。 |
信号量句柄 |
创建成功,返回的句柄可用于后续操作该信号量。 |
示例 1:创建二值信号量
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
// 定义句柄,用于接收返回值
SemaphoreHandle_t xHandle = NULL;
void app_main(void)
{
// 创建信号量
xHandle = xSemaphoreCreateBinary();
if (xHandle != NULL) {
printf("Create SUCCESS\n");
}
else {
printf("Create FALSE\n");
}
}
以上代码执行结果如下:
Create SUCCESS
该示例展示了如何使用 xSemaphoreCreateBinary()
创建二值信号量:定义句柄用于接收返回值,创建信号量后根据返回值判断是否创建成功。
创建计数信号量
API 原型
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount);
xSemaphoreCreateCounting()
用于创建一个计数信号量,支持指定最大计数值和初始计数值,适用于管理有限数量的资源或控制多个事件的同步。
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
最大计数值 |
当信号量计数达到该值时,无法继续调用“给予”操作( |
|
初始计数值 |
创建时设定的起始计数值,不能大于 |
备注
计数信号量的当前计数值反映可用资源数量,每次获取时递减,释放时递增。 最大计数值表示信号量可拥有的最大资源数,初始计数值表示创建时的资源数量。
返回值 |
说明 |
---|---|
|
由于保留信号量所需的 RAM 无法分配而无法创建信号量,信号量创建失败。 |
信号量句柄 |
创建成功,返回的句柄可用于后续操作该信号量。 |
示例 2:创建计数信号量
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
// 定义句柄,用于接收返回值
SemaphoreHandle_t xHandle = NULL;
void app_main(void)
{
// 创建信号量
xHandle = xSemaphoreCreateCounting(10, 0);
if (xHandle != NULL) {
printf("Create SUCCESS\n");
}
else {
printf("Create FALSE\n");
}
}
以上代码执行结果如下:
Create SUCCESS
该示例展示了如何使用 xSemaphoreCreateCounting()
创建一个计数信号量,其中最大计数值为 10,初始计数值为 0。
程序首先定义信号量句柄,并通过函数返回值判断信号量是否创建成功。
计数信号量适用于事件计数和资源管理等场景。
在事件计数中,每次事件发生由中断或任务调用 xSemaphoreGive()
增加计数值,处理任务通过调用 xSemaphoreTake()
等待并获取信号量,从而触发事件的处理,在这种情况下,初始值通常设为 0。
资源管理场景中,信号量计数表示可用资源数,任务获取资源时递减计数,释放资源时递增计数,初始值通常等于最大值,表示资源初始全部可用。
相比二值信号量只能表示“有”或“无”的状态,计数信号量可表示多个单位,更适合处理可累积事件或管理多个资源。 二值信号量结构简单、资源开销小,适用于基本同步和互斥;计数信号量功能更强,适用于更复杂的同步需求。 具体使用哪种信号量应根据应用场景进行选择。
创建互斥信号量
API 原型
SemaphoreHandle_t xSemaphoreCreateMutex( void )
xSemaphoreCreateMutex()
用于创建一个互斥信号量,实现任务间对共享资源的互斥访问,确保同一时刻仅有一个任务访问资源。
该互斥锁在创建后初始状态为“可用”,即未被任何任务持有,可立即被任务获取。
返回值 |
说明 |
---|---|
|
由于无法分配保存互斥锁所需的内存而未创建互斥锁,信号量创建失败。 |
信号量句柄 |
创建成功,返回的句柄可用于后续操作该信号量。 |
由于互斥锁的创建与二值信号量类似,仅需更换 API,故此处不再提供示例。
互斥锁与二值信号量结构相近,但互斥锁支持优先级继承机制,而二值信号量不具备该特性。 因此,二值信号量更适合任务间或任务与中断间的同步,也是实现简单互斥的更优选择。
备注
优先级继承机制指当高优先级任务尝试获取被低优先级任务持有的互斥锁时,低优先级任务会临时提升为高优先级,防止优先级反转。 需注意,持有互斥锁的任务必须及时释放,否则高优先级任务将一直被阻塞,且低优先级任务无法恢复原优先级。
创建递归互斥信号量
API 原型
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void )
xSemaphoreCreateRecursiveMutex()
用于创建一个递归互斥锁,允许同一任务多次递归获取该锁而不会导致死锁,适合嵌套调用场景下的资源保护。
该互斥锁在创建后初始状态为“可用”,即未被任何任务持有,可立即被任务获取。
返回值 |
说明 |
---|---|
|
由于无法分配保存互斥锁所需的内存,信号量创建失败。 |
信号量句柄 |
创建成功,返回的句柄可用于后续操作该信号量。 |
由于递归互斥锁的创建与非递归互斥锁类似,仅需更换 API,因此此处不提供示例。
非递归互斥锁只能被同一任务获取一次,若该任务在未释放的情况下再次尝试获取,将会失败或进入阻塞状态。只有在释放后,互斥锁才重新变为可用。
递归互斥锁允许同一任务在已持有锁的情况下重复获取,每次获取都会增加内部计数,任务释放锁时必须调用相同次数的释放操作,只有当释放次数与获取次数相等时,锁才会真正释放,恢复为可用状态。
两者均采用优先级继承机制,防止优先级反转问题。
删除信号量
API 原型
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
vSemaphoreDelete()
用于删除一个已创建的信号量,释放其占用的系统资源,删除后信号量句柄将失效,不应再使用。
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
需要删除的信号量的句柄 |
由信号量创建 API 返回的有效句柄,用于指定要删除的信号量。 |
信号量删除后,信号量句柄本身的值不会自动变成 NULL
或其他特殊值,它仍然保持原来的数值。
但该句柄已失效,不能再用于任何信号量操作,否则会导致未定义行为。
因此,调用 vSemaphoreDelete()
后,建议手动将句柄置为 NULL
,防止误用。
获取资源
API 原型
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait );
xSemaphoreTake()
用于获取信号量,表示任务尝试访问受保护的资源。调用时可指定等待时间,若信号量可用则立即获得并使计数值减少;若不可用,则根据等待时间阻塞任务或立即返回失败。该函数常用于任务同步和资源互斥访问。
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
正在获取信号量的句柄 |
由信号量创建 API 返回的有效句柄,用于指定需要获取的信号量。 |
|
最长等待时间 |
等待信号量可用的最长时间,以滴答周期(tick)为单位定义;为 0 时立即返回。 |
返回值 |
说明 |
---|---|
|
成功获取信号量。 |
|
等待信号量超时,获取失败。 |
备注
信号量可用意为当前信号量满足获取条件,不需要等待即可成功获取。对于不同信号量类型:
二值信号量:只有两种状态(0 或 1)。当值为 1 时,表示可用,任务可以获取;为 0 时,表示不可用,任务需等待或失败。
计数信号量:当当前计数值大于 0,表示有可用“资源”或“事件”,可以获取;为 0 时表示暂时不可用。
互斥锁/递归互斥锁:未被其他任务持有时即为“可用”,可被当前任务成功获取。
示例 3:获取计数信号量资源
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
SemaphoreHandle_t xHandle = NULL;
void app_main(void)
{
// 用于接收获取资源返回值
BaseType_t xReture;
// 创建信号量
xHandle = xSemaphoreCreateCounting(5, 3);
int count = 0;
if (xHandle != NULL) {
printf("Create SUCCESS\n");
}
else {
printf("Create FALSE\n");
}
while (1) {
xReture = xSemaphoreTake(xHandle, 0);
if (xReture == pdTRUE) {
count++;
printf("Take SUCCESS %d\n", count);
}
else {
printf("Take FALSE\n");
}
}
}
以上代码执行结果如下:
Create SUCCESS
Take SUCCESS 1
Take SUCCESS 2
Take SUCCESS 3
Take FALSE
Take FALSE
Take FALSE
该示例演示了如何使用 xSemaphoreTake()
获取信号量资源。
示例中创建了一个最大计数为 5、初始计数为 3 的计数信号量,表示初始时信号量的“资源”数量为 3。
在无限循环中,任务以非阻塞方式(等待时间为 0)调用 xSemaphoreTake()
尝试获取信号量。
由于初始计数为 3,前 3 次调用能够成功获取信号量,每次成功后计数值减 1,表示资源被占用,资源数量减 1。
当信号量计数递减到 0 时,表示当前无可用资源,后续调用 xSemaphoreTake()
将立即返回失败,任务无法继续获取资源。
需要注意:
该示例中未调用
xSemaphoreGive()
,因此资源不会被释放,信号量计数持续递减直至耗尽。在实际应用中,应确保对信号量的获取和释放操作成对进行,以维持信号量状态与实际资源状态一致,避免资源泄漏或死锁。该示例使用非阻塞获取方式,适合用于轮询检测信号量状态的场景。当需要等待信号量可用时,
xSemaphoreTake()
的等待时间参数可设置为非零值,使任务在信号量不可用时阻塞等待,提升系统效率。
释放资源
API 原型
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
xSemaphoreGive()
用于释放一个信号量,表示任务或中断完成了对资源的使用。
对于二值信号量和计数信号量,调用该函数会增加信号量的计数值,表明可用资源数量增加;对于非递归互斥锁,该 API 用于释放锁,使其恢复为可用状态。
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
正在释放信号量的句柄 |
由信号量创建 API 返回的有效句柄,用于指定需要释放的信号量。 |
返回值 |
说明 |
---|---|
|
对于计数信号量和二值信号量,表示计数值已成功递增;对于互斥锁,表示锁已成功释放。 |
|
释放失败。可能由于信号量未被正确获取、信号量已满或释放操作不合法等原因导致。 |
示例 4:释放二值、计数信号量资源
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
SemaphoreHandle_t xHandleBinary = NULL;
SemaphoreHandle_t xHandleCounting = NULL;
static void vTakeTask( void *pvParameters )
{
const char *NameTakeTask = pcTaskGetName(NULL);
SemaphoreHandle_t xHandle = (SemaphoreHandle_t)pvParameters;
// 用于接收获取资源返回值
BaseType_t xRetureForCount;
BaseType_t xRetureForTake;
int count = 0;
while (1) {
xRetureForTake = xSemaphoreTake(xHandle, 0);
if (xRetureForTake == pdTRUE) {
count++;
xRetureForCount = uxSemaphoreGetCount(xHandle);
printf("%d: Count after %s is %d\n", count, NameTakeTask, xRetureForCount);
}
else {
printf("%s FALSE\n", NameTakeTask);
printf("%s Task DELETED\n", NameTakeTask);
vTaskDelete(NULL);
}
if (count == 3) {
printf("%s Task DELETED\n", NameTakeTask);
vTaskDelete(NULL);
}
vTaskDelay(1);
}
}
static void vGiveTask( void *pvParameters )
{
const char *NameGiveTask = pcTaskGetName(NULL);
SemaphoreHandle_t xHandle = (SemaphoreHandle_t)pvParameters;
// 用于接收信号量创建返回值
BaseType_t xRetureForCount;
BaseType_t xRetureForGive;
int count = 1;
xRetureForCount = uxSemaphoreGetCount(xHandle);
printf("Initial Count for %s is %d\n", NameGiveTask, xRetureForCount);
while (1) {
xRetureForGive = xSemaphoreGive(xHandle);
if (xRetureForGive == pdTRUE) {
xRetureForCount = uxSemaphoreGetCount(xHandle);
printf("%d: Count after %s is %d\n", count, NameGiveTask, xRetureForCount);
count++;
}
if (count == 3) {
printf("%s Task DELETED\n", NameGiveTask);
vTaskDelete(NULL);
}
vTaskDelay(1);
}
}
void app_main(void)
{
// 创建信号量
xHandleBinary = xSemaphoreCreateBinary();
xHandleCounting = xSemaphoreCreateCounting(5, 3);
xHandleMutex = xSemaphoreCreateMutex();
if (xHandleBinary != NULL) {
xTaskCreate(vGiveTask, "Binary Give", 2048, (void *)xHandleBinary, 1, NULL);
xTaskCreate(vTakeTask, "Binary Take", 2048, (void *)xHandleBinary, 1, NULL);
}
if (xHandleCounting != NULL) {
xTaskCreate(vGiveTask, "Counting Give", 2048, (void *)xHandleCounting, 1, NULL);
xTaskCreate(vTakeTask, "Counting Take", 2048, (void *)xHandleCounting, 1, NULL);
}
}
void app_main(void)
{
// 创建信号量
xHandleBinary = xSemaphoreCreateBinary();
xHandleCounting = xSemaphoreCreateCounting(5, 3);
if (xHandleBinary != NULL) {
xTaskCreate(vGiveTask, "Binary Give", 2048, (void *)xHandleBinary, 1, NULL);
xTaskCreate(vTakeTask, "Binary Take", 2048, (void *)xHandleBinary, 1, NULL);
}
if (xHandleCounting != NULL) {
xTaskCreate(vGiveTask, "Counting Give", 2048, (void *)xHandleCounting, 1, NULL);
xTaskCreate(vTakeTask, "Counting Take", 2048, (void *)xHandleCounting, 1, NULL);
}
}
以上代码执行结果如下:
Initial Count for Binary Give is 0
1: Count after Binary Give is 1
1: Count after Binary Take is 0
Initial Count for Counting Give is 3
1: Count after Counting Give is 4
2: Count after Binary Give is 1
Binary Give Task DELETED
2: Count after Binary Take is 0
1: Count after Counting Take is 3
2: Count after Counting Give is 4
Counting Give Task DELETED
2: Count after Counting Take is 3
Binary Take FALSE
Binary Take Task DELETED
3: Count after Counting Take is 2
Counting Take Task DELETED
整理后可得:
Initial Count for Binary Give is 0
1: Count after Binary Give is 1
1: Count after Binary Take is 0
2: Count after Binary Give is 1
Binary Give Task DELETED
2: Count after Binary Take is 0
Binary Take FALSE
Binary Take Task DELETED
Initial Count for Counting Give is 3
1: Count after Counting Give is 4
1: Count after Counting Take is 3
2: Count after Counting Give is 4
Counting Give Task DELETED
2: Count after Counting Take is 3
3: Count after Counting Take is 2
Counting Take Task DELETED
该示例通过创建一个二值信号量和一个计数信号量,分别交由成对的任务进行资源的释放与获取,
学习 xSemaphoreGive()
和 xSemaphoreTake()
的用法,并对比观察不同类型信号量在释放资源时的行为差异。
示例中,每种信号量成功创建后都会创建两个任务,分别用于释放资源和获取资源。 每次资源释放或获取成功后,任务会打印当前信号量的计数值,并通过自增计数器记录执行轮次,便于跟踪信号量状态的动态变化。
对于二值信号量,执行结果显示其初始计数值为 0。释放一次后,计数值变为 1;获取任务成功获取资源后,计数值回落至 0。 当释放任务执行两次释放操作后退出,未进行第三次释放,导致获取任务第三次获取失败。
备注
该结果受任务调度顺序影响,不具备绝对普适性。
由此可以看出,二值信号量的释放和获取必须严格成对,只有释放后才可成功获取,且释放不会累加。
备注
在本示例中,二值信号量的取值始终在 0 和 1 之间变化,并非是程序频繁获取和释放造成的,而是因为二值信号量本身只具备两个状态:0 表示未释放(不可用),1 表示已释放(可用)。 即使连续调用释放函数,其计数值也不会超过 1。因此,释放与获取必须严格配对使用,且信号量不会累计。
对于计数信号量,执行结果显示其初始计数值为 3。释放任务运行一次后,计数值变为 4;随后获取任务获取一次资源,计数值又变回 3。 释放任务最多只释放了两次后退出,未执行第三次释放。但由于初始值为 3,即使释放操作不再继续,获取任务仍然可以最多再成功获取三次资源。
这表明计数信号量的可用资源数量不仅由释放操作决定,也受到初始值的影响,获取次数总计不应超过初始值与释放次数之和。
示例 4:释放非递增互斥信号量资源
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
SemaphoreHandle_t xHandleMutex = NULL;
// 用于接收获取资源返回值
BaseType_t xRetureForTake;
BaseType_t xRetureForGive;
static int count = 1;
static void vTakeTask ( void )
{
xRetureForTake = xSemaphoreTake(xHandleMutex, 0);
if (xRetureForTake == pdTRUE) {
printf("%d Take SUCCESS\n", count);
}
else {
printf("Take FALSE\n\n");
}
}
static void vGiveTask ( void )
{
xRetureForGive = xSemaphoreGive(xHandleMutex);
if (xRetureForGive == pdTRUE) {
printf("%d Give SUCCESS\n", count);
}
else {
printf("Give FALSE\n\n");
}
}
static void vMutexTask( void *pvParameters )
{
printf(">>>>> Give after initialize\n");
vGiveTask();
printf(">>>>> General Case\n");
while (1) {
if (count < 3) {
vTakeTask();
vGiveTask();
count++;
}
if (count == 3) {
printf("\n>>>>> Take twice after one give\n");
vTakeTask();
vTakeTask();
printf("Task DELETED\n");
vTaskDelete(NULL);
}
vTaskDelay(1);
}
}
void app_main(void)
{
// 创建信号量
xHandleMutex = xSemaphoreCreateMutex();
if (xHandleMutex != NULL) {
xTaskCreate(vMutexTask, "Mutex Take", 2048, NULL, 1, NULL);
}
}
以上代码执行结果如下:
>>>>> Give after initialize
Give FALSE
>>>>> General Case
1 Take SUCCESS
1 Give SUCCESS
2 Take SUCCESS
2 Give SUCCESS
>>>>> Take twice after one give
3 Take SUCCESS
Take FALSE
Task DELETED
本示例展示了如何通过 xSemaphoreTake()
和 xSemaphoreGive()
获取和释放非递增互斥信号量,并通过执行结果验证其使用限制。
示例中创建了一个任务,在达到三轮信号量操作后自行删除。
任务中依次测试了三种情况:
信号量创建后立即释放
获取与释放配对使用
多次获取同一信号量
通过执行结果可以总结出以下几点:
互斥信号量创建后立即释放会失败,因为此时信号量尚未被任何任务持有,仅能由实际持有者释放。
互斥信号量必须获取和释放严格配对使用,并且获取和释放必须在同一任务中完成。
非递归互斥信号量不支持同一任务多次获取,未释放前再次获取将失败。
递归互斥信号量的获取与释放
API 原型
// 获取资源
xSemaphoreTakeRecursive( SemaphoreHandle_t xMutex,
TickType_t xTicksToWait );
// 释放资源
xSemaphoreGiveRecursive( SemaphoreHandle_t xMutex );
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
正在获取/释放信号量的句柄 |
由 |
|
最长等待时间 |
等待信号量可用的最长时间,以滴答周期(tick)为单位定义;为 0 时立即返回。 |
返回值 |
说明 |
---|---|
|
成功获取信号量。 |
|
等待信号量超时,获取失败。 |
递归互斥量使用 xSemaphoreTakeRecursive()
和 xSemaphoreGiveRecursive()
来进行获取与释放。
该类型信号量会记录任务的获取次数,只有当释放次数与获取次数一致时,信号量才真正被释放,适用于函数嵌套或重复进入临界区的情况。
其使用方法与 xSemaphoreTake()
和 xSemaphoreGive()
相同,故此处不再提供示例,区别在于递归互斥量允许同一任务多次获取,而普通互斥量不支持多次获取。
信号量计数
API 原型
UBaseType_t uxSemaphoreGetCount( SemaphoreHandle_t xSemaphore );
uxSemaphoreGetCount()
用于获取信号量当前的计数值,表示信号量中可用资源的数量。该 API 主要用于调试和状态监测,只能作用于计数信号量和二值信号量,不可用于互斥信号量。
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
需要计数的信号量的句柄 |
由信号量创建 API 返回的有效句柄,用于指定需要计数的信号量。 |
返回值 |
说明 |
---|---|
信号量的当前计数值 |
当信号量是计数信号量时。 |
|
当信号量是二值信号量时,表示信号量可用。 |
|
当信号量是二值信号量时,表示信号量不可用。 |
有关该 API 的使用方法,可参考示例 4。