互斥量(Mutex)与临界区保护:构建线程安全的RTOS系统¶
学习目标¶
完成本教程后,你将能够:
- 理解互斥量的概念和工作原理
- 掌握互斥量与信号量的区别
- 学会使用互斥量保护共享资源
- 理解优先级继承机制
- 掌握死锁的识别和避免方法
- 能够实现线程安全的临界区保护
- 解决常见的资源竞争问题
前置要求¶
知识要求¶
- 理解RTOS的基本概念
- 掌握任务创建和管理
- 了解任务调度和优先级
- 理解信号量的基本使用
技能要求¶
- 能够创建和管理RTOS任务
- 了解任务状态和状态转换
- 理解任务优先级的作用
- 掌握信号量的基本操作
环境要求¶
- STM32开发板(或其他支持FreeRTOS的开发板)
- STM32CubeIDE或Keil MDK开发环境
- FreeRTOS源码或HAL库
- 串口调试工具
准备工作¶
硬件准备¶
| 硬件 | 数量 | 说明 |
|---|---|---|
| STM32开发板 | 1 | 如STM32F407、STM32F103等 |
| USB数据线 | 1 | 用于下载和供电 |
| LED灯 | 2-3个 | 用于状态指示(可选) |
软件准备¶
- 安装开发环境
- STM32CubeIDE v1.10或更高版本
-
或Keil MDK v5.30或更高版本
-
配置FreeRTOS
- 在STM32CubeMX中启用FreeRTOS
-
或手动添加FreeRTOS源码到项目
-
配置串口
- 配置UART用于调试输出
- 波特率:115200
- 数据位:8,停止位:1,无校验
环境配置¶
// FreeRTOSConfig.h 关键配置
#define configUSE_PREEMPTION 1 // 启用抢占式调度
#define configUSE_MUTEXES 1 // 启用互斥量
#define configUSE_RECURSIVE_MUTEXES 1 // 启用递归互斥量
#define configMAX_PRIORITIES 5 // 最大优先级数
#define configTICK_RATE_HZ 1000 // 时钟节拍频率
概述¶
什么是互斥量?¶
**互斥量(Mutex, Mutual Exclusion)**是RTOS中用于保护共享资源的同步机制。它确保在任意时刻,只有一个任务可以访问被保护的资源。
生活中的类比:
想象一个公共卫生间: - 只有一把钥匙(互斥量) - 进入前必须拿到钥匙(获取互斥量) - 使用完毕后归还钥匙(释放互斥量) - 其他人必须等待钥匙归还(任务阻塞) - 只有拿钥匙的人才能归还(所有权)
为什么需要互斥量?¶
在多任务系统中,多个任务可能同时访问共享资源,导致数据不一致:
问题示例:共享变量竞争
// 全局共享变量
uint32_t shared_counter = 0;
// 任务1:增加计数器
void Task1(void *param) {
while(1) {
shared_counter++; // ❌ 不安全!
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// 任务2:增加计数器
void Task2(void *param) {
while(1) {
shared_counter++; // ❌ 不安全!
vTaskDelay(pdMS_TO_TICKS(10));
}
}
问题分析:
时刻1: Task1读取 shared_counter = 100
时刻2: Task1被抢占,切换到Task2
时刻3: Task2读取 shared_counter = 100
时刻4: Task2执行 shared_counter = 101
时刻5: 切换回Task1
时刻6: Task1执行 shared_counter = 101 ← 错误!应该是102
解决方案:使用互斥量
SemaphoreHandle_t counter_mutex;
void Task1(void *param) {
while(1) {
xSemaphoreTake(counter_mutex, portMAX_DELAY); // 获取互斥量
shared_counter++; // ✅ 安全访问
xSemaphoreGive(counter_mutex); // 释放互斥量
vTaskDelay(pdMS_TO_TICKS(10));
}
}
互斥量 vs 信号量¶
| 特性 | 互斥量 | 二值信号量 |
|---|---|---|
| 用途 | 资源互斥访问 | 任务同步、事件通知 |
| 所有权 | 有所有权,谁获取谁释放 | 无所有权概念 |
| 优先级继承 | 支持,防止优先级反转 | 不支持 |
| 递归获取 | 支持递归互斥量 | 不支持 |
| 中断使用 | 不能在中断中使用 | 可以在中断中Give |
| 典型场景 | 保护共享资源 | 中断→任务通信 |
选择建议: - 保护共享资源(如全局变量、外设) → 使用互斥量 - 任务同步和事件通知 → 使用信号量
步骤1:创建和使用互斥量¶
1.1 创建互斥量¶
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// 声明互斥量句柄
SemaphoreHandle_t resource_mutex;
int main(void) {
// 系统初始化
SystemInit();
// 创建互斥量
resource_mutex = xSemaphoreCreateMutex();
if(resource_mutex != NULL) {
printf("Mutex created successfully\n");
// 创建任务...
// 启动调度器
vTaskStartScheduler();
} else {
printf("Failed to create mutex\n");
}
while(1);
}
代码说明:
- xSemaphoreCreateMutex():创建互斥量
- 返回值:互斥量句柄,失败返回NULL
- 初始状态:互斥量可用(未被任何任务持有)
1.2 获取和释放互斥量¶
获取互斥量(Take)¶
// 获取互斥量(带超时)
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xMutex,
TickType_t xTicksToWait // 等待时间
);
// 参数说明:
// xTicksToWait = 0:不等待,立即返回
// xTicksToWait = portMAX_DELAY:永久等待
// xTicksToWait = pdMS_TO_TICKS(1000):等待1000ms
// 返回值:
// pdTRUE:成功获取互斥量
// pdFALSE:超时未获取到互斥量
释放互斥量(Give)¶
// 释放互斥量
BaseType_t xSemaphoreGive(SemaphoreHandle_t xMutex);
// 返回值:
// pdTRUE:成功释放互斥量
// pdFALSE:释放失败(通常不会发生)
重要规则: - ⚠️ 只有获取互斥量的任务才能释放它(所有权) - ⚠️ 不能在中断服务函数中使用互斥量 - ⚠️ 必须在同一个任务中获取和释放
1.3 完整示例:保护共享变量¶
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// 互斥量句柄
SemaphoreHandle_t counter_mutex;
// 共享变量
uint32_t shared_counter = 0;
// 任务1:增加计数器
void IncrementTask(void *param) {
uint32_t task_id = (uint32_t)param;
while(1) {
// 获取互斥量
if(xSemaphoreTake(counter_mutex, portMAX_DELAY) == pdTRUE) {
// 临界区开始
printf("[Task %d] Counter before: %d\n", task_id, shared_counter);
// 模拟复杂操作
uint32_t temp = shared_counter;
vTaskDelay(pdMS_TO_TICKS(10)); // 模拟处理时间
temp++;
shared_counter = temp;
printf("[Task %d] Counter after: %d\n", task_id, shared_counter);
// 临界区结束
// 释放互斥量
xSemaphoreGive(counter_mutex);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
int main(void) {
// 系统初始化
HAL_Init();
SystemClock_Config();
// 创建互斥量
counter_mutex = xSemaphoreCreateMutex();
if(counter_mutex != NULL) {
// 创建多个任务
xTaskCreate(IncrementTask, "Task1", 256, (void *)1, 2, NULL);
xTaskCreate(IncrementTask, "Task2", 256, (void *)2, 2, NULL);
xTaskCreate(IncrementTask, "Task3", 256, (void *)3, 2, NULL);
// 启动调度器
vTaskStartScheduler();
}
while(1);
}
运行结果:
[Task 1] Counter before: 0
[Task 1] Counter after: 1
[Task 2] Counter before: 1
[Task 2] Counter after: 2
[Task 3] Counter before: 2
[Task 3] Counter after: 3
[Task 1] Counter before: 3
[Task 1] Counter after: 4
代码说明: - 三个任务竞争访问共享计数器 - 互斥量确保同一时刻只有一个任务访问 - 计数器值正确递增,无数据竞争
步骤2:理解优先级继承¶
2.1 优先级反转问题¶
什么是优先级反转?
当高优先级任务等待低优先级任务释放资源时,如果中优先级任务抢占了低优先级任务,会导致高优先级任务长时间等待。
问题场景:
任务优先级:
- Task_High (优先级3)
- Task_Medium (优先级2)
- Task_Low (优先级1)
时间线:
t1: Task_Low 获取互斥量
t2: Task_Low 正在使用资源
t3: Task_High 就绪,抢占Task_Low
t4: Task_High 尝试获取互斥量,阻塞等待
t5: Task_Low 恢复运行
t6: Task_Medium 就绪,抢占Task_Low ← 问题!
t7: Task_Medium 长时间运行
t8: Task_Low 恢复,释放互斥量
t9: Task_High 获得互斥量,继续运行
结果:高优先级任务被中优先级任务间接阻塞!
2.2 优先级继承机制¶
FreeRTOS的解决方案:
当高优先级任务等待低优先级任务持有的互斥量时,临时提升低优先级任务的优先级,使其不会被中优先级任务抢占。
时间线(使用互斥量):
t1: Task_Low (优先级1) 获取互斥量
t2: Task_Low 正在使用资源
t3: Task_High (优先级3) 就绪,抢占Task_Low
t4: Task_High 尝试获取互斥量,阻塞等待
t5: Task_Low 优先级提升到3 ← 优先级继承!
t6: Task_Medium (优先级2) 就绪,但无法抢占Task_Low
t7: Task_Low 完成工作,释放互斥量
t8: Task_Low 优先级恢复到1
t9: Task_High 获得互斥量,继续运行
结果:高优先级任务快速获得资源!
2.3 优先级继承示例¶
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// 互斥量句柄
SemaphoreHandle_t resource_mutex;
// 低优先级任务
void LowPriorityTask(void *param) {
while(1) {
printf("[Low] Trying to acquire mutex\n");
if(xSemaphoreTake(resource_mutex, portMAX_DELAY) == pdTRUE) {
printf("[Low] Mutex acquired, priority: %d\n",
uxTaskPriorityGet(NULL));
// 模拟长时间操作
printf("[Low] Working...\n");
vTaskDelay(pdMS_TO_TICKS(2000));
printf("[Low] Releasing mutex\n");
xSemaphoreGive(resource_mutex);
}
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
// 中优先级任务
void MediumPriorityTask(void *param) {
vTaskDelay(pdMS_TO_TICKS(500)); // 延迟启动
while(1) {
printf("[Medium] Running (no mutex needed)\n");
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 高优先级任务
void HighPriorityTask(void *param) {
vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟启动
while(1) {
printf("[High] Trying to acquire mutex\n");
if(xSemaphoreTake(resource_mutex, portMAX_DELAY) == pdTRUE) {
printf("[High] Mutex acquired\n");
printf("[High] Working...\n");
vTaskDelay(pdMS_TO_TICKS(500));
printf("[High] Releasing mutex\n");
xSemaphoreGive(resource_mutex);
}
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
int main(void) {
// 系统初始化
HAL_Init();
SystemClock_Config();
// 创建互斥量(自动支持优先级继承)
resource_mutex = xSemaphoreCreateMutex();
// 创建任务(不同优先级)
xTaskCreate(LowPriorityTask, "Low", 256, NULL, 1, NULL);
xTaskCreate(MediumPriorityTask, "Medium", 256, NULL, 2, NULL);
xTaskCreate(HighPriorityTask, "High", 256, NULL, 3, NULL);
// 启动调度器
vTaskStartScheduler();
while(1);
}
运行结果:
[Low] Trying to acquire mutex
[Low] Mutex acquired, priority: 1
[Low] Working...
[Medium] Running (no mutex needed)
[High] Trying to acquire mutex
[Low] Priority inherited to: 3 ← 优先级提升
[Medium] Blocked by Low task ← 无法抢占
[Low] Releasing mutex
[Low] Priority restored to: 1
[High] Mutex acquired
[High] Working...
[High] Releasing mutex
代码说明: - 互斥量自动实现优先级继承 - 低优先级任务持有互斥量时,优先级临时提升 - 中优先级任务无法抢占持有互斥量的低优先级任务 - 释放互斥量后,优先级恢复
步骤3:递归互斥量¶
3.1 什么是递归互斥量?¶
**递归互斥量(Recursive Mutex)**允许同一个任务多次获取同一个互斥量,而不会造成死锁。
使用场景: - 递归函数需要保护 - 函数调用链中多次需要同一个互斥量 - 复杂的嵌套调用
3.2 创建递归互斥量¶
// 创建递归互斥量
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);
// 获取递归互斥量
BaseType_t xSemaphoreTakeRecursive(
SemaphoreHandle_t xMutex,
TickType_t xTicksToWait
);
// 释放递归互斥量
BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t xMutex);
3.3 递归互斥量示例¶
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// 递归互斥量句柄
SemaphoreHandle_t recursive_mutex;
// 递归函数示例
void RecursiveFunction(uint32_t level) {
if(level == 0) {
return;
}
// 获取递归互斥量
if(xSemaphoreTakeRecursive(recursive_mutex, portMAX_DELAY) == pdTRUE) {
printf("Level %d: Mutex acquired\n", level);
// 递归调用(再次获取同一个互斥量)
RecursiveFunction(level - 1);
printf("Level %d: Releasing mutex\n", level);
// 释放递归互斥量
xSemaphoreGiveRecursive(recursive_mutex);
}
}
// 任务函数
void RecursiveTask(void *param) {
while(1) {
printf("\n[Task] Starting recursive function\n");
// 调用递归函数
RecursiveFunction(3);
printf("[Task] Recursive function completed\n\n");
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
int main(void) {
// 系统初始化
HAL_Init();
SystemClock_Config();
// 创建递归互斥量
recursive_mutex = xSemaphoreCreateRecursiveMutex();
if(recursive_mutex != NULL) {
// 创建任务
xTaskCreate(RecursiveTask, "Recursive", 256, NULL, 2, NULL);
// 启动调度器
vTaskStartScheduler();
}
while(1);
}
运行结果:
[Task] Starting recursive function
Level 3: Mutex acquired
Level 2: Mutex acquired
Level 1: Mutex acquired
Level 1: Releasing mutex
Level 2: Releasing mutex
Level 3: Releasing mutex
[Task] Recursive function completed
代码说明: - 同一个任务可以多次获取递归互斥量 - 必须释放相同次数才能真正释放互斥量 - 适用于递归函数和嵌套调用
重要提示: - ⚠️ 获取和释放次数必须匹配 - ⚠️ 只能在同一个任务中使用 - ⚠️ 不要在不同任务间传递递归互斥量
步骤4:临界区保护¶
4.1 什么是临界区?¶
**临界区(Critical Section)**是访问共享资源的代码段,必须保证原子性执行(不被中断)。
保护方法: 1. 关闭中断(适用于短时间操作) 2. 使用互斥量(适用于任务级保护) 3. 调度器锁(暂停任务调度)
4.2 方法1:关闭中断¶
// 进入临界区(关闭中断)
taskENTER_CRITICAL();
// 临界区代码
shared_variable++;
// 退出临界区(恢复中断)
taskEXIT_CRITICAL();
特点: - ✅ 最快的保护方法 - ✅ 可以保护中断和任务 - ❌ 会影响系统实时性 - ❌ 只能用于极短时间操作(微秒级)
示例:
// 保护共享变量
void UpdateSharedData(void) {
taskENTER_CRITICAL();
// 临界区:快速操作
shared_counter++;
shared_flag = true;
taskEXIT_CRITICAL();
}
4.3 方法2:使用互斥量¶
// 进入临界区(获取互斥量)
xSemaphoreTake(mutex, portMAX_DELAY);
// 临界区代码(可以较长时间)
ProcessSharedData();
// 退出临界区(释放互斥量)
xSemaphoreGive(mutex);
特点: - ✅ 不影响中断响应 - ✅ 支持优先级继承 - ✅ 适用于较长时间操作 - ❌ 有任务切换开销 - ❌ 不能在中断中使用
4.4 方法3:调度器锁¶
特点: - ✅ 不关闭中断 - ✅ 防止任务切换 - ❌ 影响系统响应性 - ❌ 不保护中断访问
4.5 临界区保护对比¶
| 方法 | 保护范围 | 时间限制 | 中断响应 | 使用场景 |
|---|---|---|---|---|
| 关闭中断 | 中断+任务 | 极短(微秒) | 被阻塞 | 快速变量操作 |
| 互斥量 | 任务间 | 较长(毫秒) | 正常 | 复杂资源保护 |
| 调度器锁 | 任务间 | 短(毫秒) | 正常 | 防止任务切换 |
4.6 完整示例:多种保护方法¶
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// 互斥量
SemaphoreHandle_t data_mutex;
// 共享数据
typedef struct {
uint32_t counter;
uint32_t timestamp;
bool flag;
} SharedData_t;
SharedData_t shared_data = {0};
// 方法1:使用关闭中断(快速操作)
void FastUpdate(void) {
taskENTER_CRITICAL();
shared_data.counter++;
taskEXIT_CRITICAL();
}
// 方法2:使用互斥量(复杂操作)
void ComplexUpdate(void) {
if(xSemaphoreTake(data_mutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
// 临界区:可以进行复杂操作
shared_data.counter++;
shared_data.timestamp = xTaskGetTickCount();
shared_data.flag = !shared_data.flag;
// 模拟复杂处理
vTaskDelay(pdMS_TO_TICKS(10));
xSemaphoreGive(data_mutex);
}
}
// 方法3:使用调度器锁
void SchedulerLockUpdate(void) {
vTaskSuspendAll();
shared_data.counter++;
shared_data.timestamp = xTaskGetTickCount();
xTaskResumeAll();
}
// 任务:测试不同保护方法
void TestTask(void *param) {
uint32_t method = (uint32_t)param;
while(1) {
switch(method) {
case 1:
FastUpdate();
printf("[Task %d] Fast update\n", method);
break;
case 2:
ComplexUpdate();
printf("[Task %d] Complex update\n", method);
break;
case 3:
SchedulerLockUpdate();
printf("[Task %d] Scheduler lock update\n", method);
break;
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
int main(void) {
// 系统初始化
HAL_Init();
SystemClock_Config();
// 创建互斥量
data_mutex = xSemaphoreCreateMutex();
// 创建测试任务
xTaskCreate(TestTask, "Task1", 256, (void *)1, 2, NULL);
xTaskCreate(TestTask, "Task2", 256, (void *)2, 2, NULL);
xTaskCreate(TestTask, "Task3", 256, (void *)3, 2, NULL);
// 启动调度器
vTaskStartScheduler();
while(1);
}
步骤5:死锁避免¶
5.1 什么是死锁?¶
**死锁(Deadlock)**是指两个或多个任务互相等待对方释放资源,导致所有任务永久阻塞。
经典死锁场景:
5.2 死锁的四个必要条件¶
- 互斥条件:资源不能被共享
- 持有并等待:持有资源的同时等待其他资源
- 不可抢占:资源不能被强制释放
- 循环等待:存在资源等待的循环链
破坏任意一个条件即可避免死锁。
5.3 死锁避免策略¶
策略1:固定获取顺序¶
// ❌ 错误:可能死锁
void TaskA(void *param) {
xSemaphoreTake(mutex1, portMAX_DELAY);
xSemaphoreTake(mutex2, portMAX_DELAY); // 可能死锁
// 使用资源
xSemaphoreGive(mutex2);
xSemaphoreGive(mutex1);
}
void TaskB(void *param) {
xSemaphoreTake(mutex2, portMAX_DELAY);
xSemaphoreTake(mutex1, portMAX_DELAY); // 可能死锁
// 使用资源
xSemaphoreGive(mutex1);
xSemaphoreGive(mutex2);
}
// ✅ 正确:固定顺序
void TaskA(void *param) {
xSemaphoreTake(mutex1, portMAX_DELAY); // 先获取mutex1
xSemaphoreTake(mutex2, portMAX_DELAY); // 再获取mutex2
// 使用资源
xSemaphoreGive(mutex2);
xSemaphoreGive(mutex1);
}
void TaskB(void *param) {
xSemaphoreTake(mutex1, portMAX_DELAY); // 先获取mutex1
xSemaphoreTake(mutex2, portMAX_DELAY); // 再获取mutex2
// 使用资源
xSemaphoreGive(mutex2);
xSemaphoreGive(mutex1);
}
策略2:使用超时机制¶
// 使用超时避免永久等待
void SafeTask(void *param) {
while(1) {
// 尝试获取mutex1(超时1秒)
if(xSemaphoreTake(mutex1, pdMS_TO_TICKS(1000)) == pdTRUE) {
// 尝试获取mutex2(超时1秒)
if(xSemaphoreTake(mutex2, pdMS_TO_TICKS(1000)) == pdTRUE) {
// 成功获取两个互斥量
printf("Both mutexes acquired\n");
// 使用资源
ProcessData();
// 释放互斥量
xSemaphoreGive(mutex2);
xSemaphoreGive(mutex1);
} else {
// 获取mutex2超时,释放mutex1
printf("Timeout acquiring mutex2, releasing mutex1\n");
xSemaphoreGive(mutex1);
}
} else {
printf("Timeout acquiring mutex1\n");
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
策略3:一次性获取所有资源¶
// 使用一个互斥量保护多个资源
SemaphoreHandle_t resource_group_mutex;
void TaskWithGroupMutex(void *param) {
// 一次性获取所有资源的访问权
if(xSemaphoreTake(resource_group_mutex, portMAX_DELAY) == pdTRUE) {
// 现在可以安全访问所有资源
UseResource1();
UseResource2();
UseResource3();
// 释放所有资源
xSemaphoreGive(resource_group_mutex);
}
}
策略4:避免嵌套获取¶
// ❌ 避免这种嵌套
void BadNesting(void) {
xSemaphoreTake(mutex1, portMAX_DELAY);
// 在持有mutex1时获取mutex2
xSemaphoreTake(mutex2, portMAX_DELAY);
// 使用资源
xSemaphoreGive(mutex2);
xSemaphoreGive(mutex1);
}
// ✅ 更好的设计
void GoodDesign(void) {
// 分别保护不同的资源
xSemaphoreTake(mutex1, portMAX_DELAY);
UseResource1();
xSemaphoreGive(mutex1);
xSemaphoreTake(mutex2, portMAX_DELAY);
UseResource2();
xSemaphoreGive(mutex2);
}
5.4 死锁检测示例¶
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// 两个互斥量
SemaphoreHandle_t mutex_A;
SemaphoreHandle_t mutex_B;
// 任务A:按A→B顺序获取
void TaskA(void *param) {
while(1) {
printf("[TaskA] Trying to acquire Mutex A\n");
if(xSemaphoreTake(mutex_A, pdMS_TO_TICKS(2000)) == pdTRUE) {
printf("[TaskA] Mutex A acquired\n");
vTaskDelay(pdMS_TO_TICKS(100)); // 模拟处理
printf("[TaskA] Trying to acquire Mutex B\n");
if(xSemaphoreTake(mutex_B, pdMS_TO_TICKS(2000)) == pdTRUE) {
printf("[TaskA] Mutex B acquired\n");
printf("[TaskA] Working with both resources\n");
vTaskDelay(pdMS_TO_TICKS(500));
xSemaphoreGive(mutex_B);
printf("[TaskA] Mutex B released\n");
} else {
printf("[TaskA] Timeout acquiring Mutex B!\n");
}
xSemaphoreGive(mutex_A);
printf("[TaskA] Mutex A released\n");
} else {
printf("[TaskA] Timeout acquiring Mutex A!\n");
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 任务B:按A→B顺序获取(避免死锁)
void TaskB(void *param) {
vTaskDelay(pdMS_TO_TICKS(50)); // 稍微延迟启动
while(1) {
printf("[TaskB] Trying to acquire Mutex A\n");
if(xSemaphoreTake(mutex_A, pdMS_TO_TICKS(2000)) == pdTRUE) {
printf("[TaskB] Mutex A acquired\n");
vTaskDelay(pdMS_TO_TICKS(100));
printf("[TaskB] Trying to acquire Mutex B\n");
if(xSemaphoreTake(mutex_B, pdMS_TO_TICKS(2000)) == pdTRUE) {
printf("[TaskB] Mutex B acquired\n");
printf("[TaskB] Working with both resources\n");
vTaskDelay(pdMS_TO_TICKS(500));
xSemaphoreGive(mutex_B);
printf("[TaskB] Mutex B released\n");
} else {
printf("[TaskB] Timeout acquiring Mutex B!\n");
}
xSemaphoreGive(mutex_A);
printf("[TaskB] Mutex A released\n");
} else {
printf("[TaskB] Timeout acquiring Mutex A!\n");
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
int main(void) {
// 系统初始化
HAL_Init();
SystemClock_Config();
// 创建互斥量
mutex_A = xSemaphoreCreateMutex();
mutex_B = xSemaphoreCreateMutex();
// 创建任务(相同优先级)
xTaskCreate(TaskA, "TaskA", 256, NULL, 2, NULL);
xTaskCreate(TaskB, "TaskB", 256, NULL, 2, NULL);
// 启动调度器
vTaskStartScheduler();
while(1);
}
运行结果(无死锁):
[TaskA] Trying to acquire Mutex A
[TaskA] Mutex A acquired
[TaskB] Trying to acquire Mutex A
[TaskA] Trying to acquire Mutex B
[TaskA] Mutex B acquired
[TaskA] Working with both resources
[TaskA] Mutex B released
[TaskA] Mutex A released
[TaskB] Mutex A acquired
[TaskB] Trying to acquire Mutex B
[TaskB] Mutex B acquired
[TaskB] Working with both resources
验证¶
验证方法¶
-
编译项目
-
下载到开发板
- 连接开发板
- 点击"Debug"或"Run"按钮
-
程序自动下载并运行
-
查看串口输出
- 打开串口调试工具
- 配置:115200, 8N1
- 观察任务执行和互斥量操作的输出
预期结果¶
互斥量保护示例: - 多个任务正确访问共享变量 - 计数器值正确递增 - 无数据竞争现象
优先级继承示例: - 低优先级任务持有互斥量时优先级提升 - 中优先级任务无法抢占 - 高优先级任务快速获得资源
死锁避免示例: - 任务按固定顺序获取互斥量 - 使用超时机制避免永久等待 - 系统正常运行,无死锁
测试要点¶
- 功能测试
- 互斥量创建成功
- 任务能够获取和释放互斥量
- 共享资源访问正确
-
优先级继承工作正常
-
边界测试
- 多个任务竞争同一互斥量
- 超时机制工作正常
-
递归互斥量正确处理嵌套
-
安全测试
- 无数据竞争
- 无死锁现象
- 无优先级反转问题
故障排除¶
问题1:互斥量创建失败¶
现象:
可能原因: 1. 堆内存不足 2. FreeRTOS配置错误 3. 未启用互斥量功能
解决方法:
// 1. 增加堆大小(FreeRTOSConfig.h)
#define configTOTAL_HEAP_SIZE ((size_t)(20 * 1024))
// 2. 启用互斥量功能
#define configUSE_MUTEXES 1
// 3. 检查剩余堆空间
size_t free_heap = xPortGetFreeHeapSize();
printf("Free heap: %d bytes\n", free_heap);
问题2:任务永久阻塞¶
现象:
可能原因: 1. 互斥量未被释放 2. 死锁情况 3. 其他任务持有互斥量并崩溃
解决方法:
// 1. 使用超时等待
if(xSemaphoreTake(mutex, pdMS_TO_TICKS(5000)) == pdTRUE) {
// 成功获取
xSemaphoreGive(mutex);
} else {
printf("Timeout waiting for mutex\n");
// 错误处理
}
// 2. 添加调试输出
printf("Trying to acquire mutex...\n");
xSemaphoreTake(mutex, portMAX_DELAY);
printf("Mutex acquired\n");
问题3:数据仍然不一致¶
现象:
可能原因: 1. 不是所有访问都受保护 2. 使用了错误的互斥量 3. 中断中访问了共享数据
解决方法:
// 1. 确保所有访问都受保护
void ReadData(void) {
xSemaphoreTake(mutex, portMAX_DELAY);
uint32_t value = shared_data; // ✅ 受保护
xSemaphoreGive(mutex);
}
void WriteData(uint32_t value) {
xSemaphoreTake(mutex, portMAX_DELAY);
shared_data = value; // ✅ 受保护
xSemaphoreGive(mutex);
}
// 2. 中断中使用关闭中断保护
void ISR_Handler(void) {
taskENTER_CRITICAL_FROM_ISR();
shared_data++;
taskEXIT_CRITICAL_FROM_ISR();
}
问题4:优先级继承不工作¶
现象: - 高优先级任务长时间等待 - 中优先级任务抢占了低优先级任务
可能原因: 1. 使用了信号量而不是互斥量 2. FreeRTOS配置错误
解决方法:
// 1. 确保使用互斥量(不是信号量)
mutex = xSemaphoreCreateMutex(); // ✅ 互斥量
// 不要使用:
// sem = xSemaphoreCreateBinary(); // ❌ 信号量
// 2. 检查FreeRTOS配置
#define configUSE_MUTEXES 1 // 必须启用
问题5:死锁发生¶
现象: - 多个任务永久阻塞 - 系统停止响应
解决方法:
// 1. 使用固定获取顺序
// 所有任务按相同顺序获取互斥量
// 2. 使用超时机制
if(xSemaphoreTake(mutex1, pdMS_TO_TICKS(1000)) == pdTRUE) {
if(xSemaphoreTake(mutex2, pdMS_TO_TICKS(1000)) == pdTRUE) {
// 使用资源
xSemaphoreGive(mutex2);
}
xSemaphoreGive(mutex1);
}
// 3. 添加死锁检测
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
// 定期检查任务状态
void MonitorTask(void *param) {
char buffer[512];
while(1) {
vTaskList(buffer);
printf("Task Status:\n%s\n", buffer);
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
常见问题¶
Q1: 互斥量和二值信号量有什么本质区别?¶
A: 主要区别在于所有权和优先级继承:
互斥量: - 有所有权:只有获取互斥量的任务才能释放它 - 支持优先级继承:防止优先级反转 - 不能在中断中使用 - 用于保护共享资源
二值信号量: - 无所有权:任何任务都可以Give - 不支持优先级继承 - 可以在中断中Give - 用于任务同步和事件通知
示例:
// 互斥量:保护资源
xSemaphoreTake(mutex, portMAX_DELAY);
shared_resource++;
xSemaphoreGive(mutex); // 必须是同一个任务
// 信号量:事件通知
void ISR_Handler(void) {
xSemaphoreGiveFromISR(sem, NULL); // 中断中Give
}
void Task(void *param) {
xSemaphoreTake(sem, portMAX_DELAY); // 任务中Take
}
Q2: 什么时候应该使用递归互斥量?¶
A: 在以下场景使用递归互斥量:
-
递归函数需要保护
-
函数调用链中多次需要同一个互斥量
注意: - 获取和释放次数必须匹配 - 只在确实需要时使用(有性能开销) - 普通互斥量更高效
Q3: 如何选择临界区保护方法?¶
A: 根据场景选择:
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 快速变量操作(微秒级) | 关闭中断 | 最快,但影响实时性 |
| 复杂资源保护(毫秒级) | 互斥量 | 不影响中断,支持优先级继承 |
| 防止任务切换 | 调度器锁 | 不关闭中断,防止任务抢占 |
| 中断与任务共享 | 关闭中断 | 互斥量不能在中断中使用 |
决策流程:
Q4: 如何避免死锁?¶
A: 遵循以下原则:
1. 固定获取顺序
// 所有任务按相同顺序获取
void AllTasks(void) {
xSemaphoreTake(mutex1, portMAX_DELAY); // 总是先获取mutex1
xSemaphoreTake(mutex2, portMAX_DELAY); // 再获取mutex2
// 使用资源
xSemaphoreGive(mutex2);
xSemaphoreGive(mutex1);
}
2. 使用超时机制
if(xSemaphoreTake(mutex1, pdMS_TO_TICKS(1000)) == pdTRUE) {
if(xSemaphoreTake(mutex2, pdMS_TO_TICKS(1000)) == pdTRUE) {
// 成功
xSemaphoreGive(mutex2);
} else {
// 超时,释放已获取的资源
xSemaphoreGive(mutex1);
}
}
3. 一次性获取所有资源
// 使用一个互斥量保护资源组
xSemaphoreTake(resource_group_mutex, portMAX_DELAY);
UseAllResources();
xSemaphoreGive(resource_group_mutex);
4. 避免嵌套获取
Q5: 互斥量的性能开销有多大?¶
A: 性能开销分析:
时间开销: - 获取/释放互斥量:约10-50个CPU周期 - 任务切换(如果阻塞):约100-500个CPU周期 - 优先级继承处理:额外10-30个CPU周期
内存开销: - 每个互斥量:约80-120字节(取决于平台) - 包括:控制块、等待列表、优先级信息
对比:
// 关闭中断(最快)
taskENTER_CRITICAL(); // ~5个周期
shared_data++;
taskEXIT_CRITICAL(); // ~5个周期
// 互斥量(较慢,但更安全)
xSemaphoreTake(mutex, portMAX_DELAY); // ~20个周期
shared_data++;
xSemaphoreGive(mutex); // ~20个周期
建议: - 对于极短操作(<10μs),使用关闭中断 - 对于一般操作,互斥量的开销可以接受 - 优先考虑正确性,再考虑性能
Q6: 如何调试互斥量相关问题?¶
A: 调试技巧:
1. 启用跟踪功能
// FreeRTOSConfig.h
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
// 查看任务状态
void DebugTask(void *param) {
char buffer[512];
while(1) {
vTaskList(buffer);
printf("Task Status:\n%s\n", buffer);
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
2. 添加调试输出
printf("[Task %s] Trying to acquire mutex\n", pcTaskGetName(NULL));
xSemaphoreTake(mutex, portMAX_DELAY);
printf("[Task %s] Mutex acquired\n", pcTaskGetName(NULL));
// 使用资源
xSemaphoreGive(mutex);
printf("[Task %s] Mutex released\n", pcTaskGetName(NULL));
3. 使用断言
// 确保互斥量有效
configASSERT(mutex != NULL);
// 确保成功获取
BaseType_t result = xSemaphoreTake(mutex, pdMS_TO_TICKS(5000));
configASSERT(result == pdTRUE);
4. 检查任务优先级
总结¶
核心要点¶
- 互斥量概念
- 用于保护共享资源的同步机制
- 有所有权:谁获取谁释放
-
支持优先级继承,防止优先级反转
-
基本操作
xSemaphoreCreateMutex():创建互斥量xSemaphoreTake():获取互斥量-
xSemaphoreGive():释放互斥量 -
关键特性
- 优先级继承:自动提升持有互斥量的低优先级任务
- 递归互斥量:支持同一任务多次获取
-
所有权检查:只有持有者才能释放
-
最佳实践
- 使用互斥量保护共享资源
- 使用超时避免永久阻塞
- 固定获取顺序避免死锁
- 临界区尽量短小
- 不要在中断中使用互斥量
学习检查¶
完成本教程后,你应该能够:
- 理解互斥量的工作原理和所有权概念
- 创建和使用互斥量保护共享资源
- 理解优先级继承机制
- 识别和避免死锁问题
- 选择合适的临界区保护方法
- 使用递归互斥量处理嵌套调用
- 调试互斥量相关问题
实践建议¶
- 动手实践
- 在开发板上运行本教程的所有示例
- 修改参数,观察不同的行为
-
尝试制造死锁并解决
-
深入学习
- 学习消息队列和事件标志组
- 了解任务间通信的最佳实践
-
研究优先级反转的详细分析
-
项目应用
- 在实际项目中使用互斥量
- 设计合理的资源保护方案
- 优化系统性能和响应时间
下一步¶
推荐学习路径¶
- 消息队列通信机制
- 学习队列的创建和使用
- 掌握任务间数据传递
- 理解队列的阻塞机制
-
参考:消息队列通信机制
-
事件标志组应用
- 学习事件标志组的概念
- 掌握多事件同步方法
- 理解位操作和等待机制
-
参考:事件标志组应用
-
优先级反转问题与解决
- 深入理解优先级反转
- 学习优先级天花板协议
- 掌握实时性分析方法
-
参考:优先级反转问题与解决
-
任务间通信方式对比
- 对比不同通信机制的特点
- 学习如何选择合适的方式
- 掌握性能优化技巧
- 参考:任务间通信方式对比
进阶主题¶
- RTOS中断管理与延迟处理:学习中断与RTOS的配合使用
- 实时性分析与调度可行性:掌握实时系统的分析方法
- RTOS调试技巧与工具:学习高级调试方法
实践项目¶
尝试以下项目来巩固所学知识:
- 多任务数据采集系统
- 使用互斥量保护共享数据结构
- 实现多传感器并发采集
-
确保数据一致性
-
资源管理系统
- 使用互斥量管理有限资源
- 实现资源分配和回收
-
避免死锁和优先级反转
-
线程安全的日志系统
- 使用互斥量保护日志缓冲区
- 支持多任务并发写入
- 实现高效的日志输出
参考资料¶
官方文档¶
推荐阅读¶
- 《FreeRTOS实时内核实用指南》
- 《嵌入式实时操作系统》
- 《并发编程的艺术》
- 《操作系统概念》(第9版)
在线资源¶
相关内容¶
版权声明: 本文档采用 CC BY-SA 4.0 许可协议。
反馈与建议: 如有问题或建议,请通过平台反馈系统联系我们。
最后更新: 2024-01-15