【FreeRTOS】信号量
FreeRTOS 信号量
- 一、二值信号量 (Binary Semaphore)
- 二、计数信号量 (Counting Semaphore)
- 三、互斥量 (Mutex - Mutual Exclusion Semaphore):
- 3.1 互斥量(Mutex):
- 3.2 递归互斥量 (Recursive Mutex):
- 3.3 互斥量和递归互斥量的区别
- 四、信号量的获取和释放
- 4.1 释放(提供)信号量
- 4.2 获取信号量
- 五、优先级翻转问题
FreeRTOS 中的信号量是一种核心的同步机制,用于在任务之间、任务与中断服务程序(ISR)之间协调对共享资源的访问、实现任务同步以及管理事件。它们本质上是一种任务通知机制,允许任务等待某个事件发生或资源变得可用。
FreeRTOS 主要提供三种类型的信号量: 二值信号量,计数信号量和互斥量。
一、二值信号量 (Binary Semaphore)
-
行为: 像一个开关,只有两种状态:可用(通常为 1)或不可用(通常为 0)。
-
用途:
- 任务同步: 最常用。例如,一个任务(或 ISR)完成某个操作(如数据采集)后 “释放” 信号量,另一个任务 “获取” 该信号量以知道操作已完成并开始后续处理(如数据处理)。
- 互斥锁的轻量级替代(需谨慎): 在资源极其简单且不会发生优先级反转的情况下,可以代替互斥量。但通常不推荐,因为它不解决优先级反转问题。
-
创建API:
动态创建:
SemaphoreHandle_t xSemaphoreCreateBinary(void);
静态创建:
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *pxSemaphoreBuffer );
静态创建示例:
SemaphoreHandle_t xSemaphore = NULL; StaticSemaphore_t xSemaphoreBuffer; xSemaphore = xSemaphoreCreateBinaryStatic( &xSemaphoreBuffer );
-
初始状态: 创建后为不可用状态(0)。通常需要一个任务或 ISR 先“给出”它,其他任务才能获取,中断中只能调用
xSemaphoreGiveFromISR
。xSemaphoreGive( xSemaphore );
二、计数信号量 (Counting Semaphore)
启用计数信号量时,configUSE_COUNTING_SEMAPHORES
需要置 1
-
行为: 像一个计数器,其值可以大于 1。每次“获取”操作使计数值减 1(如果值大于 0),每次“给出”操作使计数值加 1。
-
用途:
- 管理多个相同资源: 经典用途。例如,管理一个包含 N 个缓冲区的池。计数值初始化为 N(表示所有缓冲区可用)。任务要使用缓冲区时“获取”信号量(计数值减 1),用完归还时“给出”信号量(计数值加 1)。如果计数值为 0,试图获取的任务将阻塞(如果指定了阻塞时间)。
- 事件计数: 记录事件发生的次数。一个任务(或 ISR)每次事件发生时“给出”信号量(计数加 1),另一个任务可以“获取”信号量(计数减 1)来处理事件。任务可以一次性处理多个累积的事件(多次获取)。
-
动态创建:
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
uxMaxCount
: 信号量能达到的最大计数值。uxInitialCount
: 信号量创建时的初始计数值。
-
静态创建:
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,UBaseType_t uxInitialCountStaticSemaphore_t *pxSemaphoreBuffer );
-
uxMaxCount
可以达到的最大计数值。当信号量达到此值时,它不能再 被“给出”。
-
uxInitialCount
创建信号量时分配给信号量的计数值。
-
pxSemaphoreBuffer
必须指向一个 StaticSemaphore_t 类型的变量,然后用它来保存信号量的数据结构体。
返回值:如果已成功创建信号量,则将返回该信号量的句柄。如果
pxSemaphoreBuffer
为 NULL, 则返回 NULL。 -
三、互斥量 (Mutex - Mutual Exclusion Semaphore):
启用互斥信号量时,configUSE_MUTEXES
需要置 1。
与信号量不同的是,互斥量具有所有权、递归访问以及防止优先级翻转的特性,用于实现对临界资源的独占式处理。
互斥量只能由同一个任务加锁和解锁。
当互斥量被任务持有时,该互斥量处于闭锁状态,这个任务获得互斥量的所有权。释放互斥量时,则处于开锁状态。
当一个任务持有互斥量时,其他任务不能再对该互斥量进行开锁或持有。持有该互斥量的任务也能够再次获得这个锁而不被挂起,即递归访问。
3.1 互斥量(Mutex):
-
行为: 一种特殊的二值信号量,专为互斥访问共享资源而设计,关键特性是优先级继承。
-
用途:
- 保护共享资源: 确保在任何时候只有一个任务可以访问临界资源(如全局变量、外设、文件等)。当一个低优先级任务持有互斥量时,如果高优先级任务尝试获取,低优先级任务的临时优先级会被提升到与高优先级任务相同(优先级继承),以防止中等优先级任务抢占低优先级任务而导致高优先级任务被无限期阻塞(优先级反转问题)。
-
创建:
SemaphoreHandle_t xSemaphoreCreateMutex(void);
-
初始状态: 创建后为可用状态(未锁定)。
对应的信号量获取和释放API为:
获取互斥量
xSemaphoreTake( SemaphoreHandle_t xSemaphore,TickType_t xTicksToWait )
释放互斥量
xSemaphoreGive( SemaphoreHandle_t xSemaphore )
互斥量是为了对资源的互斥访问而设计,不同于使用二值信号量来保护临界资源外, 互斥锁采用优先级继承机制。这意味着如果高优先级任务进入阻塞状态,同时 尝试获取当前由低优先级任务持有的互斥锁(令牌), 则持有令牌的任务的优先级会暂时提高到阻塞任务的优先级。这项机制 旨在确保较高优先级的任务保持阻塞状态的时间尽可能短, 从而最大限度减少已经发生的“优先级反转”现象。
不能在中断中使用互斥锁是:
-
互斥锁使用的优先级继承机制要求 从任务中(而不是从中断中)拿走和放入互斥锁。
-
中断无法保持阻塞来等待一个被互斥锁保护的资源 变为可用。
3.2 递归互斥量 (Recursive Mutex):
要使用递归互斥量,需要将 configUSE_RECURSIVE_MUTEXES 置1 。
用户可对一把递归互斥锁重复加锁。只有用户 为每个成功的 xSemaphoreTakeRecursive()
请求调用 xSemaphoreGiveRecursive()
后,互斥锁才会重新变为可用。例如,如果一个任务成功“加锁”相同的互斥锁 5 次, 那么任何其他任务都无法使用此互斥锁,直到任务也把这个互斥锁“解锁”5 次。
-
行为: 一种特殊的互斥量,允许同一个任务多次获取(锁定)它,而不会导致自身死锁。该任务必须释放(解锁)相同的次数,互斥量才会真正变为可用状态。
-
用途: 保护可能被同一个任务嵌套调用的函数或代码块中访问的共享资源。
-
创建:
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);
-
初始状态: 创建后为可用状态(未锁定)。
对于递归互斥量对应着一组专门的函数:
⚠️
xSemaphoreTakeRecursive()
和xSemaphoreGiveRecursive()
只能在使用xSemaphoreCreateRecursiveMutex()
创建的互斥锁上使用。
3.3 互斥量和递归互斥量的区别
互斥量(Mutex)和递归互斥量(Recursive Mutex)在 FreeRTOS 中都是用于保护共享资源的同步机制,但它们在设计目标和行为上有关键区别,主要围绕同一个任务能否多次获取锁这一问题展开。
以下是详细对比:
1. 核心区别:是否允许同一任务重复加锁
特性 | 互斥量 (Mutex) | 递归互斥量 (Recursive Mutex) |
---|---|---|
同一任务重复获取 | ❌ 不允许 | ✅ 允许 |
行为 | 任务首次获取成功,后续再获取会死锁 | 任务可多次获取,需等量释放才能解锁 |
内部计数器 | 无 | 有(记录当前任务的加锁次数) |
2. 适用场景
互斥量 (Mutex)
-
典型场景:保护非嵌套访问的共享资源(如全局变量、硬件外设)。
void TaskA() {xSemaphoreTake(xMutex, portMAX_DELAY); // 第一次获取(成功)// 访问共享资源xSemaphoreGive(xMutex); // 释放 }void TaskB() {xSemaphoreTake(xMutex, portMAX_DELAY); // 其他任务需等待// ... }
-
致命问题:若任务尝试嵌套获取,会立即死锁:
void TaskA() {xSemaphoreTake(xMutex, portMAX_DELAY); // 第一次获取(成功)// ... 调用某个函数 ...xSemaphoreTake(xMutex, portMAX_DELAY); // ❌ 第二次获取:任务永久阻塞!xSemaphoreGive(xMutex);xSemaphoreGive(xMutex); // 永远执行不到这里 }
递归互斥量 (Recursive Mutex)
-
典型场景:保护可能被同一任务嵌套调用的代码(如递归函数、模块化代码)。
void RecursiveFunction(int n) {xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY); // 第 n 次获取if (n > 0) {RecursiveFunction(n - 1); // 嵌套调用,再次获取锁}xSemaphoreGiveRecursive(xRecursiveMutex); // 第 n 次释放 }void Task() {// 安全进入嵌套函数xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);RecursiveFunction(3);xSemaphoreGiveRecursive(xRecursiveMutex); // 最终释放 }
-
关键优势:
同一任务可多次加锁,内部计数器跟踪加锁次数。只有释放次数 = 获取次数时,锁才真正释放。
3. API 区别
操作 | 互斥量 (Mutex) | 递归互斥量 (Recursive Mutex) |
---|---|---|
创建 | xSemaphoreCreateMutex() | xSemaphoreCreateRecursiveMutex() |
获取 (Take) | xSemaphoreTake() | xSemaphoreTakeRecursive() |
释放 (Give) | xSemaphoreGive() | xSemaphoreGiveRecursive() |
错误使用后果 | 死锁 | 资源未释放(计数器未清零) |
⚠️ 重要:递归互斥量必须使用
TakeRecursive
/GiveRecursive
配套 API!
4. 何时选择?
- 用互斥量 (Mutex):
资源共享逻辑简单,同一任务不会重复请求锁(如:单个函数内访问硬件寄存器)。 - 用递归互斥量 (Recursive Mutex):
代码存在嵌套调用路径(如:函数A → 函数B → 两者均需访问同一资源),或资源可能被同一任务多次进入。
四、信号量的获取和释放
4.1 释放(提供)信号量
任务释放信号量:
xSemaphoreGive( SemaphoreHandle_t xSemaphore );
参数:
-
xSemaphore
要释放的信号量的句柄。这是创建信号量时返回的句柄。 |
返回:
- 如果信号量被释放,则返回 pdTRUE。
- 如果发生错误,则返回 pdFALSE。信号量是使用队列实现的,发布消息时,如果队列上没有空间, 那么可能会发生错误,这表明最初未能正确获取信号量。
此函数不支持使用 xSemaphoreCreateRecursiveMutex()创建的信号量。
中断中释放信号量
xSemaphoreGiveFromISR ( SemaphoreHandle_t xSemaphore,BaseType_t *pxHigherPriorityTaskWoken );
互斥信号量(调用 xSemaphoreCreateMutex() 创建的信号量) 不得与此宏一起使用。
参数:
-
xSemaphore
要释放的信号量的句柄,这是创建信号量时返回的句柄。
-
pxHigherPriorityTaskWoken
如果给出信号量会导致任务解除阻塞,并且解除阻塞的任务的优先级高于当前正在运行的任务, 则 xSemaphoreGiveFromISR() 会将 *pxHigherPriorityTaskWoken 设置为 pdTRUE。 如果 xSemaphoreGiveFromISR() 将此值设置为 pdTRUE,则应在退出中断之前请求上下文切换。 从 FreeRTOS V7.3.0 开始,pxHigherPriorityTaskWoken 为可选参数, 可设置为 NULL。
返回:
如果成功给出信号量,则返回 pdTRUE,否则 errQUEUE_FULL。
在中断中,使用示例:
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();
void timer_isr(void)
{BaseType_t xHigherPriorityTaskWoken = pdFALSE;xSemaphoreGiveFromISR (xSemaphore,&xHigherPriorityTaskWoken );/* 如果 xHigherPriorityTaskWoken 为 true,则请求上下文切换,执行优先级最高的任务. */portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
4.2 获取信号量
任务获取信号量:
xSemaphoreTake( SemaphoreHandle_t xSemaphore,TickType_t xTicksToWait );
参数:
-
xSemaphore
正在获取的信号量的句柄——在创建信号量时获得。
-
xTicksToWait
等待信号量变为可用的时间(以滴答为单位)。宏
portTICK_PERIOD_MS
可以 将其转换为实际时间。可以用一个为零的阻塞时间来轮询信号量。 如果把 FreeRTOSConfig.h 中
INCLUDE_vTaskSuspend
设置为 1,则将阻塞时间指定为portMAX_DELAY
会导致任务无限期地阻塞(没有超时限制)。
返回:
- 如果获得信号量,则返回 pdTRUE。
- 如果
xTicksToWait
超时过后,还没有获得信号量,则返回 pdFALSE。
中断中获取信号量
同样的,在中断中也可以获取信号量,与任务中不同的是,中断函数中不能阻塞,因此在中断中获取信号量 API 不能阻塞。
xSemaphoreTakeFromISR ( SemaphoreHandle_t xSemaphore,BaseType_t *pxHigherPriorityTaskWoken )
与 xSemaphoreTake() 不同,xSemaphoreTakeFromISR() 不允许指定阻塞时间。
从 ISR 获取信号量并不是一种常见的操作,它可能仅在获取计数信号量时有用,当中断需要从资源池中获取对象时(当信号量计数指示可用资源的数量时)会用到,其他情况下,很少在中断中获取信号量。
五、优先级翻转问题
优先级翻转,如果没有优先级继承机制的话,会出现以下的现象:高优先级任务因为无法获取信号量而阻塞,持有信号量的低优先级任务被中等优先级任务抢占,导致其无法释放信号量,这就导致了高优先级任务长时间等待信号量而无法及时执行,严重影响了系统的实时性要求。
示例图示如下:

避免出现优先级翻转问题的方式就是使用互斥量,它提供了优先级继承机制。
当高优先级任务 Task H 执行过程中需要调用互斥资源时, 但是发现任务 Task L 正在持有该互斥量, 此时RTOS系统会将任务 Task L 的优先级会被提升到与 Task H 同一个优先级, 这个就是所谓的优先级继承( Priority inheritance) ,当任务 Task L释放互斥量后,高优先级的任务H 可以获取互斥量,并且及时运行,这样就有效地防止了优先级翻转问题。
FreeRTOS 实现了基本的优先级继承机制,旨在优化 空间和执行周期。完全的优先级继承机制需要多得多的数据和处理器 周期来确定任何时刻的继承优先级,特别是在任务同时占用超过一个互斥锁时 。
优先级继承机制的这些特定行为:
- 如果一个任务在占用一个互斥锁时没有先释放它已占用的互斥锁, 则可以进一步提升其继承优先级。
- 任务在释放其占有的所有互斥锁之前,一直保持最高继承优先级。 这与释放互斥锁的顺序无关。
- 如果多个互斥锁被占用,无论在任何一个被占用的互斥锁上等待的任务是否完成等待(超时), 则任务将保持最高继承优先级 。