跳转至

互斥量(Mutex)与临界区保护:构建线程安全的RTOS系统

学习目标

完成本教程后,你将能够:

  • 理解互斥量的概念和工作原理
  • 掌握互斥量与信号量的区别
  • 学会使用互斥量保护共享资源
  • 理解优先级继承机制
  • 掌握死锁的识别和避免方法
  • 能够实现线程安全的临界区保护
  • 解决常见的资源竞争问题

前置要求

知识要求

  • 理解RTOS的基本概念
  • 掌握任务创建和管理
  • 了解任务调度和优先级
  • 理解信号量的基本使用

技能要求

  • 能够创建和管理RTOS任务
  • 了解任务状态和状态转换
  • 理解任务优先级的作用
  • 掌握信号量的基本操作

环境要求

  • STM32开发板(或其他支持FreeRTOS的开发板)
  • STM32CubeIDE或Keil MDK开发环境
  • FreeRTOS源码或HAL库
  • 串口调试工具

准备工作

硬件准备

硬件 数量 说明
STM32开发板 1 如STM32F407、STM32F103等
USB数据线 1 用于下载和供电
LED灯 2-3个 用于状态指示(可选)

软件准备

  1. 安装开发环境
  2. STM32CubeIDE v1.10或更高版本
  3. 或Keil MDK v5.30或更高版本

  4. 配置FreeRTOS

  5. 在STM32CubeMX中启用FreeRTOS
  6. 或手动添加FreeRTOS源码到项目

  7. 配置串口

  8. 配置UART用于调试输出
  9. 波特率:115200
  10. 数据位: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:调度器锁

// 暂停任务调度
vTaskSuspendAll();

// 临界区代码(不会被其他任务抢占)
shared_variable++;

// 恢复任务调度
xTaskResumeAll();

特点: - ✅ 不关闭中断 - ✅ 防止任务切换 - ❌ 影响系统响应性 - ❌ 不保护中断访问

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)**是指两个或多个任务互相等待对方释放资源,导致所有任务永久阻塞。

经典死锁场景

任务A:
1. 获取互斥量1
2. 等待互斥量2  ← 阻塞

任务B:
1. 获取互斥量2
2. 等待互斥量1  ← 阻塞

结果:两个任务互相等待,永久死锁!

5.2 死锁的四个必要条件

  1. 互斥条件:资源不能被共享
  2. 持有并等待:持有资源的同时等待其他资源
  3. 不可抢占:资源不能被强制释放
  4. 循环等待:存在资源等待的循环链

破坏任意一个条件即可避免死锁。

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

验证

验证方法

  1. 编译项目

    # 在STM32CubeIDE中
    Project  Build Project
    
    # 或使用命令行
    make
    

  2. 下载到开发板

  3. 连接开发板
  4. 点击"Debug"或"Run"按钮
  5. 程序自动下载并运行

  6. 查看串口输出

  7. 打开串口调试工具
  8. 配置:115200, 8N1
  9. 观察任务执行和互斥量操作的输出

预期结果

互斥量保护示例: - 多个任务正确访问共享变量 - 计数器值正确递增 - 无数据竞争现象

优先级继承示例: - 低优先级任务持有互斥量时优先级提升 - 中优先级任务无法抢占 - 高优先级任务快速获得资源

死锁避免示例: - 任务按固定顺序获取互斥量 - 使用超时机制避免永久等待 - 系统正常运行,无死锁

测试要点

  1. 功能测试
  2. 互斥量创建成功
  3. 任务能够获取和释放互斥量
  4. 共享资源访问正确
  5. 优先级继承工作正常

  6. 边界测试

  7. 多个任务竞争同一互斥量
  8. 超时机制工作正常
  9. 递归互斥量正确处理嵌套

  10. 安全测试

  11. 无数据竞争
  12. 无死锁现象
  13. 无优先级反转问题

故障排除

问题1:互斥量创建失败

现象

mutex = xSemaphoreCreateMutex();
if(mutex == NULL) {
    printf("Failed to create mutex\n");
}

可能原因: 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:任务永久阻塞

现象

// 任务一直等待,永不返回
xSemaphoreTake(mutex, portMAX_DELAY);
printf("This never prints\n");

可能原因: 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:数据仍然不一致

现象

// 使用了互斥量,但数据仍然错误
xSemaphoreTake(mutex, portMAX_DELAY);
shared_data++;
xSemaphoreGive(mutex);

可能原因: 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: 在以下场景使用递归互斥量:

  1. 递归函数需要保护

    void RecursiveFunction(int level) {
        xSemaphoreTakeRecursive(mutex, portMAX_DELAY);
    
        if(level > 0) {
            RecursiveFunction(level - 1);  // 递归调用
        }
    
        xSemaphoreGiveRecursive(mutex);
    }
    

  2. 函数调用链中多次需要同一个互斥量

    void FunctionA(void) {
        xSemaphoreTakeRecursive(mutex, portMAX_DELAY);
        // 操作
        FunctionB();  // 调用FunctionB
        xSemaphoreGiveRecursive(mutex);
    }
    
    void FunctionB(void) {
        xSemaphoreTakeRecursive(mutex, portMAX_DELAY);  // 再次获取
        // 操作
        xSemaphoreGiveRecursive(mutex);
    }
    

注意: - 获取和释放次数必须匹配 - 只在确实需要时使用(有性能开销) - 普通互斥量更高效

Q3: 如何选择临界区保护方法?

A: 根据场景选择:

场景 推荐方法 原因
快速变量操作(微秒级) 关闭中断 最快,但影响实时性
复杂资源保护(毫秒级) 互斥量 不影响中断,支持优先级继承
防止任务切换 调度器锁 不关闭中断,防止任务抢占
中断与任务共享 关闭中断 互斥量不能在中断中使用

决策流程

是否在中断中访问?
├─ 是 → 使用关闭中断
└─ 否 → 操作时间?
    ├─ 极短(<10μs) → 使用关闭中断
    └─ 较长(>10μs) → 使用互斥量

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. 检查任务优先级

UBaseType_t priority = uxTaskPriorityGet(NULL);
printf("Current priority: %d\n", priority);

总结

核心要点

  1. 互斥量概念
  2. 用于保护共享资源的同步机制
  3. 有所有权:谁获取谁释放
  4. 支持优先级继承,防止优先级反转

  5. 基本操作

  6. xSemaphoreCreateMutex():创建互斥量
  7. xSemaphoreTake():获取互斥量
  8. xSemaphoreGive():释放互斥量

  9. 关键特性

  10. 优先级继承:自动提升持有互斥量的低优先级任务
  11. 递归互斥量:支持同一任务多次获取
  12. 所有权检查:只有持有者才能释放

  13. 最佳实践

  14. 使用互斥量保护共享资源
  15. 使用超时避免永久阻塞
  16. 固定获取顺序避免死锁
  17. 临界区尽量短小
  18. 不要在中断中使用互斥量

学习检查

完成本教程后,你应该能够:

  • 理解互斥量的工作原理和所有权概念
  • 创建和使用互斥量保护共享资源
  • 理解优先级继承机制
  • 识别和避免死锁问题
  • 选择合适的临界区保护方法
  • 使用递归互斥量处理嵌套调用
  • 调试互斥量相关问题

实践建议

  1. 动手实践
  2. 在开发板上运行本教程的所有示例
  3. 修改参数,观察不同的行为
  4. 尝试制造死锁并解决

  5. 深入学习

  6. 学习消息队列和事件标志组
  7. 了解任务间通信的最佳实践
  8. 研究优先级反转的详细分析

  9. 项目应用

  10. 在实际项目中使用互斥量
  11. 设计合理的资源保护方案
  12. 优化系统性能和响应时间

下一步

推荐学习路径

  1. 消息队列通信机制
  2. 学习队列的创建和使用
  3. 掌握任务间数据传递
  4. 理解队列的阻塞机制
  5. 参考:消息队列通信机制

  6. 事件标志组应用

  7. 学习事件标志组的概念
  8. 掌握多事件同步方法
  9. 理解位操作和等待机制
  10. 参考:事件标志组应用

  11. 优先级反转问题与解决

  12. 深入理解优先级反转
  13. 学习优先级天花板协议
  14. 掌握实时性分析方法
  15. 参考:优先级反转问题与解决

  16. 任务间通信方式对比

  17. 对比不同通信机制的特点
  18. 学习如何选择合适的方式
  19. 掌握性能优化技巧
  20. 参考:任务间通信方式对比

进阶主题

  • RTOS中断管理与延迟处理:学习中断与RTOS的配合使用
  • 实时性分析与调度可行性:掌握实时系统的分析方法
  • RTOS调试技巧与工具:学习高级调试方法

实践项目

尝试以下项目来巩固所学知识:

  1. 多任务数据采集系统
  2. 使用互斥量保护共享数据结构
  3. 实现多传感器并发采集
  4. 确保数据一致性

  5. 资源管理系统

  6. 使用互斥量管理有限资源
  7. 实现资源分配和回收
  8. 避免死锁和优先级反转

  9. 线程安全的日志系统

  10. 使用互斥量保护日志缓冲区
  11. 支持多任务并发写入
  12. 实现高效的日志输出

参考资料

官方文档

推荐阅读

  • 《FreeRTOS实时内核实用指南》
  • 《嵌入式实时操作系统》
  • 《并发编程的艺术》
  • 《操作系统概念》(第9版)

在线资源

相关内容


版权声明: 本文档采用 CC BY-SA 4.0 许可协议。

反馈与建议: 如有问题或建议,请通过平台反馈系统联系我们。

最后更新: 2024-01-15