第四篇FreeRTOS同步机制-信号量、互斥量与优先级反转
- 未分类
- 9小时前
- 17热度
- 0评论
第四篇 FreeRTOS 同步机制:信号量、互斥量与优先级反转
掌握信号量与互斥量的原理和使用场景,深入理解优先级反转问题。这是 RTOS 面试中出现频率最高的知识点之一。本文从"为什么需要同步"讲起,带你彻底搞清楚二值信号量、计数信号量、互斥量的区别和实战用法。
一、为什么需要同步机制?
1.1 多任务并发问题
任务A: 读取温度 → [计算] → 写入全局变量 temp
任务B: 读取 temp → [显示到LCD]
问题:如果任务A写到一半被任务B抢占,B读到的就是"半成品"数据!
三大经典问题:
- 竞态条件(Race Condition):多任务同时访问共享资源
- 数据不一致:读写被打断导致数据错乱
- 执行顺序:任务间需要协调先后顺序
二、信号量(Semaphore)
2.1 二值信号量(Binary Semaphore)
就像一把只有一把钥匙的锁,只有 0 和 1 两种状态。
中断(Give) 二值信号量 任务(Take)
│ 初始值=0 │
│── xSemaphoreGiveFromISR() ──→│ 值=1 │
│ │── xSemaphoreTake() 成功 ──→│
│ │ 值=0 │ 执行处理逻辑
典型用途:中断与任务同步(中断 Give,任务 Take)
SemaphoreHandle_t xBinarySem;
// 初始化
xBinarySem = xSemaphoreCreateBinary(); // 初始值为0(空)
// 中断服务函数中
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xBinarySem, &xHigherPriorityTaskWoken);
// 如果唤醒了更高优先级的任务,触发上下文切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 任务中
void vProcessTask(void *pvParameters)
{
for(;;)
{
// 等待信号量(阻塞直到中断Give)
if(xSemaphoreTake(xBinarySem, portMAX_DELAY) == pdTRUE)
{
// 处理中断触发的事件
ProcessData();
}
}
}
xSemaphoreCreateBinary() 创建后初始值为 0(空),任务会立即阻塞在 xSemaphoreTake(),直到中断调用 xSemaphoreGiveFromISR() 释放信号量。
2.2 计数信号量(Counting Semaphore)
就像一个停车场计数器,记录可用资源数量。
// 创建:最大值10,初始值0
SemaphoreHandle_t xCountSem = xSemaphoreCreateCounting(10, 0);
// 生产者:释放资源
xSemaphoreGive(xCountSem); // 计数+1
// 消费者:获取资源
xSemaphoreTake(xCountSem, portMAX_DELAY); // 计数-1,为0时阻塞
典型用途:
- 事件计数:记录中断发生次数(防止丢失)
- 资源管理:管理有限资源池(如 DMA 通道、连接池)
2.3 二值信号量 vs 计数信号量
| 特性 | 二值信号量 | 计数信号量 |
|---|---|---|
| 值范围 | 0 或 1 | 0 ~ 最大值 |
| 用途 | 同步(通知) | 资源管理 / 事件计数 |
| 中断中多次 Give | 只记住一次 | 每次都计数 |
三、互斥量(Mutex)
3.1 为什么不能用二值信号量保护临界区?
任务A(低优先级): Take → 进入临界区 → [被抢占]
任务C(高优先级): Take → 阻塞等待...
任务B(中优先级): 一直在运行(不需要信号量)
结果:高优先级C被中优先级B间接阻塞了!→ 优先级反转!
3.2 互斥量 = 二值信号量 + 优先级继承
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
void vTaskA(void *p) // 低优先级
{
for(;;)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
// 访问共享资源(临界区)
SharedResource++;
xSemaphoreGive(xMutex);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void vTaskC(void *p) // 高优先级
{
for(;;)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
// 访问共享资源
ReadSharedResource();
xSemaphoreGive(xMutex);
vTaskDelay(pdMS_TO_TICKS(50));
}
}
3.3 优先级继承机制(核心重点)
低优先级任务A 中优先级任务B 高优先级任务C
│ │ │
持有 Mutex │ │
在临界区执行──────→ │ 尝试 Take Mutex
│ │ → 阻塞
┌────────────────────────────────────────────────┐
│ A 的优先级被临时提升到与 C 相同! │
│ B 无法抢占 A(A 现在优先级 = C) │
└────────────────────────────────────────────────┘
│ │ │
释放 Mutex ─────────────────────────────→ 获得 Mutex
优先级恢复原值 │ 开始执行
3.4 信号量 vs 互斥量(高频面试题)
| 特性 | 二值信号量 | 互斥量 |
|---|---|---|
| 优先级继承 | ❌ 无 | ✅ 有 |
| 主要用途 | 同步(通知) | 互斥(保护资源) |
| 谁 Give/Take | 不同任务(A Give, B Take) | 同一任务(谁 Take 谁 Give) |
| 中断中使用 | ✅ 可以 GiveFromISR | ❌ 不可以 |
| 优先级反转 | 会发生 | 通过继承缓解 |
3.5 递归互斥量
// 同一任务可以多次 Take,不会死锁
SemaphoreHandle_t xRecMutex = xSemaphoreCreateRecursiveMutex();
void vTask(void *p)
{
xSemaphoreTakeRecursive(xRecMutex, portMAX_DELAY); // Take 1次
FuncA(); // FuncA 内部也 Take 了一次
xSemaphoreGiveRecursive(xRecMutex); // Give 1次
}
void FuncA(void)
{
xSemaphoreTakeRecursive(xRecMutex, portMAX_DELAY); // Take 第2次(不会死锁)
// ...
xSemaphoreGiveRecursive(xRecMutex); // Give 第2次
}
四、临界区保护的其他方式
4.1 关中断
taskENTER_CRITICAL(); // 关中断(允许嵌套)
// 临界区代码(尽量短!)
taskEXIT_CRITICAL(); // 开中断
// ISR版本
UBaseType_t uxSaved = taskENTER_CRITICAL_FROM_ISR();
// ...
taskEXIT_CRITICAL_FROM_ISR(uxSaved);
4.2 挂起调度器
vTaskSuspendAll(); // 暂停调度器(中断仍然开启)
// 临界区代码
xTaskResumeAll(); // 恢复调度器
4.3 三种临界区保护方式对比
| 方式 | 关中断 | 挂起调度器 | 互斥量 |
|---|---|---|---|
| 中断响应 | ❌ 中断被屏蔽 | ✅ 中断正常 | ✅ 中断正常 |
| 任务切换 | ❌ 不会切换 | ❌ 不会切换 | ✅ 会切换(阻塞等待) |
| 适用时长 | 极短(几us) | 短(不宜太长) | 可以较长 |
| 适用场景 | ISR 与任务共享 | 纯任务间共享 | 任务间共享(推荐) |
- 临界区代码极短(几行赋值) →
taskENTER_CRITICAL() - 临界区代码较长或涉及阻塞操作 → Mutex
- 需要和 ISR 共享数据 → 关中断
- 纯任务间共享 → Mutex(不影响中断响应)
五、实战练习
练习 1:中断同步 — 按键触发任务
SemaphoreHandle_t xButtonSem;
void vButtonTask(void *p)
{
xButtonSem = xSemaphoreCreateBinary();
for(;;)
{
if(xSemaphoreTake(xButtonSem, portMAX_DELAY) == pdTRUE)
{
printf("Button pressed! Processing...\r\n");
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
}
// EXTI中断回调
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
BaseType_t xWoken = pdFALSE;
if(GPIO_Pin == BUTTON_Pin)
{
xSemaphoreGiveFromISR(xButtonSem, &xWoken);
portYIELD_FROM_ISR(xWoken);
}
}
练习 2:Mutex 保护共享资源
SemaphoreHandle_t xMutex;
uint32_t ulSharedCounter = 0;
void vIncrementTask(void *p)
{
for(;;)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
for(int i = 0; i < 1000; i++)
ulSharedCounter++;
printf("Task%d: counter = %lu\r\n", (int)p, ulSharedCounter);
xSemaphoreGive(xMutex);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 创建两个同优先级任务,观察 counter 是否正确递增
xTaskCreate(vIncrementTask, "Inc1", 256, (void*)1, 2, NULL);
xTaskCreate(vIncrementTask, "Inc2", 256, (void*)2, 2, NULL);
ulSharedCounter 做 1000 次自增,最终值会比预期少(因为 ++ 操作不是原子的,读-改-写会被打断)。加了 Mutex 后,counter 应精确递增。
六、面试高频问答
1. 什么是优先级反转?怎么解决?
**优先级反转**:高优先级任务等待低优先级任务持有的资源,而中间优先级任务抢占了低优先级任务,导致高优先级任务被间接阻塞。
**解决方案**:
- **优先级继承**(FreeRTOS Mutex 自带):临时提升持锁任务的优先级
- **优先级天花板**(FreeRTOS 不支持):将 Mutex 设为所有使用者的最高优先级
- 经典案例:**火星探路者号**(1997年)因优先级反转导致系统重启
2. Mutex 能在中断中使用吗?为什么?
**不能**。因为:
1. Mutex 有优先级继承机制,中断没有"优先级"概念(中断优先级和任务优先级是两套体系)
2. Mutex 的 Take 可能导致阻塞,**中断中绝对不能阻塞**
3. 中断中应使用**二值信号量**(xSemaphoreGiveFromISR)来通知任务
3. 什么是死锁?FreeRTOS 怎么避免?
**死锁**:两个任务互相等待对方持有的资源。
- 任务A持有MutexA,等待MutexB
- 任务B持有MutexB,等待MutexA
**避免方法**:
1. 所有任务按相同顺序获取多个 Mutex
2. 使用超时参数(不用 portMAX_DELAY)
3. 尽量减少同时持有多个 Mutex 的情况
4. 使用**递归互斥量**防止自死锁
4. taskENTER_CRITICAL() 和 Mutex 怎么选?
- 临界区代码**极短**(几行赋值) → 关中断 taskENTER_CRITICAL()
- 临界区代码**较长**或涉及阻塞操作 → 用 Mutex
- 需要和 **ISR 共享**数据 → 关中断
- 纯**任务间**共享 → Mutex(不影响中断响应)
核心原则:关中断时间越短越好,长时间关中断会严重影响系统实时性。
七、巩固练习题
选择题
1. 以下哪个说法正确?
A. 二值信号量有优先级继承 B. 互斥量可以在中断中使用
C. 互斥量有优先级继承机制 D. 计数信号量只能计数到1
**答案:C** ✅
💡 **解析**:互斥量 = 二值信号量 + 优先级继承,这是它与二值信号量的本质区别。A 错(二值信号量无优先级继承),B 错(Mutex 不能在中断中使用),D 错(计数信号量可计数到设定的最大值)。
2. 中断服务函数中应该使用哪个 API 释放信号量?
A. xSemaphoreGive() B. xSemaphoreGiveFromISR()
C. xSemaphoreTake() D. xSemaphoreCreateBinary()
**答案:B** ✅
💡 **解析**:中断中必须使用带 FromISR 后缀的 API。普通版本可能导致任务调度操作在中断上下文中执行,引发系统崩溃。
3. 以下哪种情况会导致优先级反转?
A. 高优先级任务使用 Mutex 保护资源
B. 低优先级任务持有二值信号量,高优先级任务等待,中优先级任务抢占
C. 两个同优先级任务使用 Mutex
D. 任务使用 taskENTER_CRITICAL 保护临界区
**答案:B** ✅
💡 **解析**:优先级反转的经典场景:低优先级持有资源 → 高优先级等待 → 中优先级抢占低优先级 → 高优先级被间接阻塞。使用 Mutex 的优先级继承可以缓解(A选项)。关中断(D选项)不存在此问题。
4. xSemaphoreCreateBinary() 创建的信号量初始值是?
A. 1 B. 0 C. -1 D. 未定义
**答案:B. 0** ✅
💡 **解析**:二值信号量创建后初始值为 0(空),任务调用 xSemaphoreTake() 会立即阻塞,需要等待其他地方 Give。注意:xSemaphoreCreateMutex() 创建的 Mutex 初始值为 1(可用)。
5. 关于递归互斥量,以下说法错误的是?
A. 同一任务可以多次 Take 不会死锁
B. Take 几次就要 Give 几次
C. 不同任务可以对同一个递归互斥量交叉 Take/Give
D. 使用 xSemaphoreCreateRecursiveMutex() 创建
**答案:C** ✅
💡 **解析**:递归互斥量的"递归"是指同一任务可以多次 Take。不同任务不能交叉 Take/Give,Mutex 的基本原则始终是"谁 Take 谁 Give"。
填空题
6. FreeRTOS 中,信号量主要用于____,互斥量主要用于____
信号量主要用于同步(通知/事件触发),互斥量主要用于互斥(保护共享资源)。
💡 信号量是"通知机制"(A发信号给B),互斥量是"锁机制"(谁锁谁开)。
7. 互斥量不能在中断中使用的两个原因是:____ 和 ____
1. Mutex 有优先级继承机制,中断没有任务优先级概念(两套体系)
2. Mutex 的 Take 可能导致阻塞,中断中绝对不能阻塞
💡 中断中需要同步时,应使用 xSemaphoreGiveFromISR() 释放二值信号量来通知任务。
8. 优先级继承是指:当____优先级任务等待____优先级任务持有的 Mutex 时,____优先级任务的优先级被临时提升到与____优先级任务相同
当高优先级任务等待低优先级任务持有的 Mutex 时,低优先级任务的优先级被临时提升到与高优先级任务相同。
💡 Mutex 释放后,低优先级任务的优先级会自动恢复原值。
9. 避免死锁的三种方法:____、____、____
1. 所有任务按相同顺序获取多个 Mutex
2. 使用超时参数(不用 portMAX_DELAY)
3. 尽量减少同时持有多个 Mutex 的情况
💡 还可以使用递归互斥量防止"自死锁"(同一任务对同一 Mutex 重复 Take)。
简答题
10. 面试官问:"请详细说明优先级反转的过程,以及 FreeRTOS 如何解决?"
**优先级反转过程**:
1. 低优先级任务 A 获取了信号量(或 Mutex),进入临界区执行
2. 高优先级任务 C 就绪,抢占 A,然后尝试获取同一个信号量 → 阻塞等待
3. 中优先级任务 B 就绪,由于 A 的优先级低于 B,B 抢占了 A
4. 结果:A 无法运行所以无法释放信号量,C 只能一直等。实际执行顺序变成 B > C,中优先级反而比高优先级先执行
**FreeRTOS 的解决方案(优先级继承)**:
- 使用 xSemaphoreCreateMutex() 代替 xSemaphoreCreateBinary()
- 当 C 等待 A 持有的 Mutex 时,A 的优先级被临时提升到 C 的优先级
- B 无法抢占 A(因为 A 此时优先级 = C > B)
- A 尽快执行完临界区并释放 Mutex,优先级恢复
- C 获得 Mutex,开始执行
11. 面试官问:"如果按键中断频繁触发,用二值信号量可能丢事件,怎么办?"
**问题分析**:
二值信号量只有 0 和 1 两个状态,连续多次 Give 只会将值保持在 1,任务只 Take 到一次。
**解决方案**:
1. **计数信号量**:xSemaphoreCreateCounting(MAX, 0),每次 Give 都会计数 +1,任务每次 Take 消费一个计数,不会丢失事件
2. **队列**:用 xQueueSendFromISR() 发送按键信息(带时间戳或键值),不仅不丢事件,还能携带额外数据
3. **任务通知**:xTaskNotifyFromISR() + eIncrement 模式,效率最高
实际项目中推荐方案 2(队列),因为既不丢事件又能传数据。
12. 面试官问:"vTaskSuspendAll() 和 taskENTER_CRITICAL() 都能保护临界区,区别是什么?"
| 对比 | taskENTER_CRITICAL() | vTaskSuspendAll() |
| :--- | :--- | :--- |
| 中断 | 关闭中断,中断不响应 | 中断正常响应 |
| 任务调度 | 不会发生 | 不会发生 |
| 适用场景 | ISR 和任务共享的变量 | 纯任务间共享的变量 |
| 时长要求 | 必须极短(us 级) | 可以稍长但不宜太长 |
**核心区别**:taskENTER_CRITICAL() 关中断,所有中断不响应,影响系统实时性;vTaskSuspendAll() 只暂停调度器,中断仍能响应,对实时性影响小。
**选型原则**:如果需要防止 ISR 访问共享数据,必须关中断;如果只是防止其他任务抢占,用挂起调度器即可。
八、总结 Checklist
- [ ] 理解信号量的 "Give / Take" 模型
- [ ] 区分二值信号量、计数信号量、互斥量的使用场景
- [ ] 能解释优先级反转及其解决方案
- [ ] 掌握 Mutex 的优先级继承机制
- [ ] 知道三种临界区保护方式的适用场景
- [ ] 完成中断同步实验
- [ ] 完成 Mutex 保护共享资源实验