【FreeRTOS】二值信号量与互斥量核心区别与使用详解
FreeRTOS中二值信号量与互斥量的核心区别在于:二值信号量用于任务同步(无所有权概念,任何任务都可释放),适合事件通知场景;互斥量用于资源保护(具有所有权特性,必须由获取者释放),能防止优先级反转。关键差异体现在互斥量支持优先级继承机制,当高优先级任务等待时,持有互斥量的低优先级任务会临时提升优先级,避免被中优先级任务阻塞。使用原则:任务间同步选二值信号量,共享资源保护用互斥量,注意互斥量不可
目录
对于学习FreeRTOS 来说,二值信号量和互斥量确实容易混淆,因为它们底层机制相似,但用途和行为有本质区别。我们可以从 “功能用途”“核心特性”“使用场景” 三个维度来理解它们的区别和联系。
一、先搞懂基本概念:什么是二值信号量?什么是互斥量?
1. 二值信号量(Binary Semaphore)
可以理解为一个 “开关”,只有两种状态:“有信号”(值为 1)和“无信号”(值为 0)。
-
当一个任务调用
xSemaphoreGive()时,信号量变为 “有信号”(值 1); -
当一个任务调用
xSemaphoreTake()时,信号量变为 “无信号”(值 0); -
核心作用:“同步”(让一个任务等待另一个任务 / 事件的发生)。
2. 互斥量(Mutex)
可以理解为一个 “钥匙”,用于保护共享资源 (比如一个传感器、一段内存、一个外设)。
-
当一个任务需要访问共享资源时,先 “拿钥匙”(
xSemaphoreTake()); -
用完后必须 “还钥匙”(
xSemaphoreGive()); -
核心作用:“互斥”(确保同一时间只有一个任务能访问共享资源)。
二、核心区别:从 “行为特性” 看差异

关键区别详解:
a.“所有权” 是核心差异
-
二值信号量:比如任务 A 调用
xSemaphoreGive()使信号量变为 1,任务 B 可以调用xSemaphoreTake()拿走(变为 0),之后任务 C 也可以调用xSemaphoreGive()再次使它变为 1—— 谁都可以 “给”,谁都可以 “拿”,没有归属。 -
互斥量:比如任务 A 调用
xSemaphoreTake()拿到互斥量(“拿到钥匙”),那么只有任务 A能调用xSemaphoreGive()释放它。如果任务 B 试图释放任务 A 持有的互斥量,会导致错误(行为未定义)。
b.优先级反转问题的处理(重点)
这是互斥量最关键的特性,也是最难理解的点,举个例子:
-
假设有高优先级任务 H、中优先级任务 M、低优先级任务 L。
三个任务在 “竞争访问同一个共享资源” 时,采用了不同的 “资源保护工具”—— 要么用二值信号量,要么用互斥量。
1.使用二值信号量
这里的逻辑是:
为了防止多个任务同时访问共享资源(比如一块内存、一个传感器),三个任务(L、M、H)约定用二值信号量作为 “资源占用的标志”:
-
谁要访问资源,先调用
xSemaphoreTake()拿信号量(拿到了就表示 “资源可用”,没拿到就等); -
用完资源后,调用
xSemaphoreGive()释放信号量(告诉其他任务 “资源空闲了”)。
但因为二值信号量没有 “优先级继承” 特性,就会出现问题:
-
低优先级任务 L 先拿到二值信号量,开始访问资源;
-
此时高优先级任务 H 就绪,也想拿信号量访问资源,但信号量被 L 占用,H 只能等待;
-
中优先级任务 M(优先级比 L 高、比 H 低)也就绪了 —— 因为 M 的优先级比 L 高,会直接抢占 L 的 CPU(L 被打断,暂停访问资源);
-
M 运行时,H 依然在等信号量,但 L 被 M 打断后迟迟不能释放信号量,导致 H(最高优先级)反而被 M(中优先级)和 L(低优先级)“拖累”,这就是优先级反转。
一句话:三个任务都用二值信号量来 “抢资源”,但二值信号量保护不了优先级,导致反转。
2.使用互斥量
这里的逻辑是:
三个任务(L、M、H)约定改用互斥量作为 “资源占用的标志”,规则和信号量类似(拿锁 -> 用资源 -> 放锁),但互斥量有 “优先级继承” 特性,能解决问题:
-
低优先级任务 L 先拿到互斥量,开始访问资源;
-
高优先级任务 H 就绪,想拿互斥量但被 L 占用,H 开始等待;
-
此时 FreeRTOS 检测到 “高优先级任务 H 在等低优先级任务 L 持有的互斥量”,会临时把 L 的优先级提升到和 H 一样高;
-
中优先级任务 M 就绪后,因为 M 的优先级低于 “被临时提升后的 L”,所以 M 无法抢占 L,L 能继续不受干扰地访问资源,用完后释放互斥量;
-
互斥量释放后,H 就能立刻拿到互斥量访问资源,避免了优先级反转。
一句话:三个任务都用互斥量来 “抢资源”,互斥量通过 “临时提升低优先级任务 L 的优先级”,阻止了 M 抢占 L,让 H 能及时拿到资源。
三、使用示例
二值信号量和互斥量在 FreeRTOS 底层都基于信号量机制实现,它们的核心操作(xSemaphoreTake() 获取、xSemaphoreGive() 释放)的函数接口完全相同。
可以理解为:
-
二值信号量是 “最简单的信号量”(计数只能是 0 或 1);
-
互斥量是 “增强版的二值信号量”,增加了 “所有权” 和 “优先级继承” 特性,专门用于互斥场景。
3.1二值信号量的使用方法
二值信号量核心用于同步(如 “事件通知”“任务唤醒”),使用流程分为 3 步:创建、获取(等待信号)、释放(发送信号)。
1.创建二值信号量
使用 xSemaphoreCreateBinary() 函数创建,返回信号量句柄(SemaphoreHandle_t)。
#include "FreeRTOS.h"
#include "semphr.h"
// 定义二值信号量句柄(全局或任务间可见)
SemaphoreHandle_t xBinarySemaphore;
void vInitSemaphores(void) {
// 创建二值信号量(初始状态为0:无信号)
xBinarySemaphore = xSemaphoreCreateBinary();
// 检查创建是否成功(内存不足可能失败)
if (xBinarySemaphore == NULL) {
// 错误处理:如日志输出、系统复位等
}
}
注意:二值信号量创建后默认状态为 “无信号”(值为 0),需通过xSemaphoreGive()手动设置为 “有信号”(值为 1)。
2.1 任务中获取信号量
使用 xSemaphoreTake() 函数等待信号量变为 “有信号”(值 1),获取后信号量自动变为 “无信号”(值 0)。
FreeRTOS源代码:
/**
* semphr. h
* <pre>xSemaphoreTake(
* SemaphoreHandle_t xSemaphore,
* TickType_t xBlockTime
* )</pre>
*
* <i>Macro</i> to obtain a semaphore. The semaphore must have previously been
* created with a call to xSemaphoreCreateBinary(), xSemaphoreCreateMutex() or
* xSemaphoreCreateCounting().
*
* @param xSemaphore A handle to the semaphore being taken - obtained when
* the semaphore was created.
*
* @param xBlockTime The time in ticks to wait for the semaphore to become
* available. The macro portTICK_PERIOD_MS can be used to convert this to a
* real time. A block time of zero can be used to poll the semaphore. A block
* time of portMAX_DELAY can be used to block indefinitely (provided
* INCLUDE_vTaskSuspend is set to 1 in FreeRTOSConfig.h).
*
* @return pdTRUE if the semaphore was obtained. pdFALSE
* if xBlockTime expired without the semaphore becoming available.
*
* Example usage:
<pre>
SemaphoreHandle_t xSemaphore = NULL;
// A task that creates a semaphore.
void vATask( void * pvParameters )
{
// Create the semaphore to guard a shared resource.
xSemaphore = xSemaphoreCreateBinary();
}
// A task that uses the semaphore.
void vAnotherTask( void * pvParameters )
{
// ... Do other things.
if( xSemaphore != NULL )
{
// See if we can obtain the semaphore. If the semaphore is not available
// wait 10 ticks to see if it becomes free.
if( xSemaphoreTake( xSemaphore, ( TickType_t ) 10 ) == pdTRUE )
{
// We were able to obtain the semaphore and can now access the
// shared resource.
// ...
// We have finished accessing the shared resource. Release the
// semaphore.
xSemaphoreGive( xSemaphore );
}
else
{
// We could not obtain the semaphore and can therefore not access
// the shared resource safely.
}
}
}
</pre>
* \defgroup xSemaphoreTake xSemaphoreTake
* \ingroup Semaphores
*/
#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )
示例:任务等待中断通知(如按键按下)
// 等待信号量的任务
void vWaitTask(void *pvParameters) {
for (;;) {
// 等待信号量(无限期等待,直到有信号)
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdPASS) {
// 信号量获取成功:处理事件(如按键逻辑)
printf("收到信号,执行操作...\n");
}
}
}
2.2 中断中获取信号量
FreeRTOS源代码:
/**
* semphr. h
* <pre>
xSemaphoreTakeFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
)</pre>
*
* <i>Macro</i> to take a semaphore from an ISR. The semaphore must have
* previously been created with a call to xSemaphoreCreateBinary() or
* xSemaphoreCreateCounting().
*
* Mutex type semaphores (those created using a call to xSemaphoreCreateMutex())
* must not be used with this macro.
*
* This macro can be used from an ISR, however taking a semaphore from an ISR
* is not a common operation. It is likely to only be useful when taking a
* counting semaphore when an interrupt is obtaining an object from a resource
* pool (when the semaphore count indicates the number of resources available).
*
* @param xSemaphore A handle to the semaphore being taken. This is the
* handle returned when the semaphore was created.
*
* @param pxHigherPriorityTaskWoken xSemaphoreTakeFromISR() will set
* *pxHigherPriorityTaskWoken to pdTRUE if taking the semaphore caused a task
* to unblock, and the unblocked task has a priority higher than the currently
* running task. If xSemaphoreTakeFromISR() sets this value to pdTRUE then
* a context switch should be requested before the interrupt is exited.
*
* @return pdTRUE if the semaphore was successfully taken, otherwise
* pdFALSE
*/
#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )
3.1 任务中释放信号量
-
任务中释放:
xSemaphoreGive() -
中断中释放:必须用
xSemaphoreGiveFromISR()(ISR 安全版本)
FreeRTOS源代码:
/**
* semphr. h
* <pre>xSemaphoreGive( SemaphoreHandle_t xSemaphore )</pre>
*
* <i>Macro</i> to release a semaphore. The semaphore must have previously been
* created with a call to xSemaphoreCreateBinary(), xSemaphoreCreateMutex() or
* xSemaphoreCreateCounting(). and obtained using sSemaphoreTake().
*
* This macro must not be used from an ISR. See xSemaphoreGiveFromISR () for
* an alternative which can be used from an ISR.
*
* This macro must also not be used on semaphores created using
* xSemaphoreCreateRecursiveMutex().
*
* @param xSemaphore A handle to the semaphore being released. This is the
* handle returned when the semaphore was created.
*
* @return pdTRUE if the semaphore was released. pdFALSE if an error occurred.
* Semaphores are implemented using queues. An error can occur if there is
* no space on the queue to post a message - indicating that the
* semaphore was not first obtained correctly.
*
* Example usage:
<pre>
SemaphoreHandle_t xSemaphore = NULL;
void vATask( void * pvParameters )
{
// Create the semaphore to guard a shared resource.
xSemaphore = vSemaphoreCreateBinary();
if( xSemaphore != NULL )
{
if( xSemaphoreGive( xSemaphore ) != pdTRUE )
{
// We would expect this call to fail because we cannot give
// a semaphore without first "taking" it!
}
// Obtain the semaphore - don't block if the semaphore is not
// immediately available.
if( xSemaphoreTake( xSemaphore, ( TickType_t ) 0 ) )
{
// We now have the semaphore and can access the shared resource.
// ...
// We have finished accessing the shared resource so can free the
// semaphore.
if( xSemaphoreGive( xSemaphore ) != pdTRUE )
{
// We would not expect this call to fail because we must have
// obtained the semaphore to get here.
}
}
}
}
</pre>
* \defgroup xSemaphoreGive xSemaphoreGive
* \ingroup Semaphores
*/
#define xSemaphoreGive( xSemaphore ) xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
3.2 中断中释放信号量
使用 xSemaphoreGive() 函数将信号量设置为 “有信号”(值 1),唤醒等待的任务。
FreeRTOS源代码:
/**
* semphr. h
* <pre>
xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
)</pre>
*
* <i>Macro</i> to release a semaphore. The semaphore must have previously been
* created with a call to xSemaphoreCreateBinary() or xSemaphoreCreateCounting().
*
* Mutex type semaphores (those created using a call to xSemaphoreCreateMutex())
* must not be used with this macro.
*
* This macro can be used from an ISR.
*
* @param xSemaphore A handle to the semaphore being released. This is the
* handle returned when the semaphore was created.
*
* @param pxHigherPriorityTaskWoken xSemaphoreGiveFromISR() will set
* *pxHigherPriorityTaskWoken to pdTRUE if giving the semaphore caused a task
* to unblock, and the unblocked task has a priority higher than the currently
* running task. If xSemaphoreGiveFromISR() sets this value to pdTRUE then
* a context switch should be requested before the interrupt is exited.
*
* @return pdTRUE if the semaphore was successfully given, otherwise errQUEUE_FULL.
*
* Example usage:
<pre>
\#define LONG_TIME 0xffff
\#define TICKS_TO_WAIT 10
SemaphoreHandle_t xSemaphore = NULL;
// Repetitive task.
void vATask( void * pvParameters )
{
for( ;; )
{
// We want this task to run every 10 ticks of a timer. The semaphore
// was created before this task was started.
// Block waiting for the semaphore to become available.
if( xSemaphoreTake( xSemaphore, LONG_TIME ) == pdTRUE )
{
// It is time to execute.
// ...
// We have finished our task. Return to the top of the loop where
// we will block on the semaphore until it is time to execute
// again. Note when using the semaphore for synchronisation with an
// ISR in this manner there is no need to 'give' the semaphore back.
}
}
}
// Timer ISR
void vTimerISR( void * pvParameters )
{
static uint8_t ucLocalTickCount = 0;
static BaseType_t xHigherPriorityTaskWoken;
// A timer tick has occurred.
// ... Do other time functions.
// Is it time for vATask () to run?
xHigherPriorityTaskWoken = pdFALSE;
ucLocalTickCount++;
if( ucLocalTickCount >= TICKS_TO_WAIT )
{
// Unblock the task by releasing the semaphore.
xSemaphoreGiveFromISR( xSemaphore, &xHigherPriorityTaskWoken );
// Reset the count so we release the semaphore again in 10 ticks time.
ucLocalTickCount = 0;
}
if( xHigherPriorityTaskWoken != pdFALSE )
{
// We can force a context switch here. Context switching from an
// ISR uses port specific syntax. Check the demo task for your port
// to find the syntax required.
}
}
</pre>
* \defgroup xSemaphoreGiveFromISR xSemaphoreGiveFromISR
* \ingroup Semaphores
*/
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )
示例:中断中释放信号量(通知任务)
// 按键中断服务程序(ISR)
void EXTI0_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 检查中断标志(硬件相关)
if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
// 释放二值信号量(通知等待任务)
xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
// 清理中断标志
EXTI_ClearITPendingBit(EXTI_Line0);
}
// 若需要,触发任务切换(高优先级任务被唤醒时)
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
3.2互斥量的使用方法
互斥量核心用于保护共享资源(确保同一时间只有一个任务访问),使用流程也是 3 步:创建、获取(加锁)、释放(解锁)。
1.创建互斥量
使用 xSemaphoreCreateMutex() 函数创建,返回互斥量句柄(SemaphoreHandle_t)。
// 定义互斥量句柄(保护共享资源,如串口)
SemaphoreHandle_t xMutex;
void vInitMutex(void) {
// 创建互斥量(初始状态为“有信号”:可被获取)
xMutex = xSemaphoreCreateMutex();
// 检查创建是否成功
if (xMutex == NULL) {
// 错误处理
}
}
注意:互斥量创建后默认状态为 “有信号”(值 1),表示 “资源可用”。
2.获取互斥量(加锁)
同样使用 xSemaphoreTake() 函数,获取后互斥量变为 “无信号”(值 0),表示 “资源被占用”。 示例:任务访问共享资源(如串口打印)前加锁
// 任务1:使用串口打印
void vTask1(void *pvParameters) {
for (;;) {
// 获取互斥量(最多等待100ms,超时则放弃)
if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdPASS) {
// 成功获取:安全访问共享资源(串口)
printf("任务1:正在使用串口...\n");
vTaskDelay(pdMS_TO_TICKS(50)); // 模拟使用资源
// 用完后释放互斥量(解锁)
xSemaphoreGive(xMutex);
} else {
// 获取失败(超时):处理错误(如重试或放弃)
printf("任务1:获取互斥量超时!\n");
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 任务2:也需要使用串口(与任务1互斥)
void vTask2(void *pvParameters) {
// 逻辑与任务1相同,省略...
}
3.释放互斥量(解锁)
使用 xSemaphoreGive() 函数,释放后互斥量变为 “有信号”(值 1),表示 “资源可用”。 核心规则:只有获取互斥量的任务才能释放它(所有权特性)。 如果任务 A 获取互斥量后,任务 B 试图释放,会导致未定义行为(可能系统崩溃)。
3.3使用时的核心注意事项
1.二值信号量的注意事项
-
初始化状态:创建后默认 “无信号”(值 0),需首次调用
xSemaphoreGive()才能让xSemaphoreTake()获取成功。 -
中断中必须用
FromISR版本:释放信号量时,ISR 中必须用xSemaphoreGiveFromISR(),不能用普通xSemaphoreGive()(否则可能导致调度错误)。 -
不适合保护共享资源:二值信号量没有 “所有权” 和 “优先级继承”,若用于互斥,可能导致优先级反转(高优先级任务被低优先级任务阻塞)。
-
避免长期持有:
xSemaphoreTake()获取后,应尽快用xSemaphoreGive()释放,避免其他等待任务长期阻塞。
2.互斥量的注意事项
-
严格的所有权:必须由获取互斥量的任务释放,其他任务释放会导致错误(FreeRTOS 不检查,可能引发系统异常)。
-
禁止在 ISR 中使用:互斥量的
xSemaphoreTake()可能阻塞(xTicksToWait非 0),而 ISR 不能阻塞;且互斥量的优先级继承机制在 ISR 中无效。 -
避免嵌套死锁:同一任务多次获取同一个互斥量会导致死锁(第一次获取成功,第二次获取时因互斥量已被自己占用而阻塞,永远无法返回)。
// 错误示例:同一任务嵌套获取互斥量
void vBadTask(void *param) {
xSemaphoreTake(xMutex, portMAX_DELAY); // 第一次获取成功
xSemaphoreTake(xMutex, portMAX_DELAY); // 第二次获取:阻塞,死锁!
}
-
持有时间要短:获取互斥量后,应仅执行 “访问共享资源” 的必要操作,尽快释放,避免其他任务长时间等待。
-
优先级继承的限制:互斥量的优先级继承只能 “减轻” 优先级反转,不能完全消除。例如,若低优先级任务 L 持有互斥量,高优先级任务 H 等待,L 会被提升到 H 的优先级,但如果此时有中优先级任务 M,M 仍可能被 L(临时高优先级)阻塞。
3.两者共有的注意事项
-
必须检查返回值:
xSemaphoreTake()和xSemaphoreGive()的返回值需判断(如超时、释放失败),否则可能因资源未获取 / 未释放导致逻辑错误。 -
句柄有效性:使用前必须确保信号量 / 互斥量创建成功(句柄非 NULL),否则调用相关函数会导致崩溃。
-
内存管理:信号量 / 互斥量依赖 FreeRTOS 堆内存,若堆内存不足,创建会失败(返回 NULL),需确保堆大小足够(
FreeRTOSConfig.h中的configTOTAL_HEAP_SIZE)。
总结
-
二值信号量:用于 “同步”(事件通知),无所有权,可在 ISR 中用
FromISR版本,注意初始化和及时释放。 -
互斥量:用于 “互斥”(保护共享资源),有所有权和优先级继承,禁止在 ISR 中使用,严格避免嵌套死锁。
四、怎么选?用场景判断
-
用二值信号量:当你需要 “通知” 或 “同步” 时。 例 1:按键中断触发后,用
xSemaphoreGiveFromISR()释放信号量,唤醒等待的任务处理按键事件。 例 2:任务 A 完成数据采集后,释放信号量,通知任务 B 开始处理数据。 -
用互斥量:当你需要 “保护共享资源” 时。 例 1:多个任务需要向同一个串口打印日志,用互斥量确保同一时间只有一个任务能操作串口。 例 2:多个任务需要读写同一块内存中的配置参数,用互斥量避免数据错乱。
总结
-
二值信号量:像 “门铃”,谁都可以按(释放),谁都可以听(获取),用于 “通知对方做事”。
-
互斥量:像 “厕所钥匙”,谁拿到谁用,用完必须自己还,用于 “保证只有一个人用厕所”,还能避免 “急事的人(高优先级)被慢腾腾的人(低优先级)耽误”。
记住核心原则:同步用信号量,互斥用互斥量,并始终检查返回值,就能避免大部分使用错误。
更多推荐



所有评论(0)