08-信号量与互斥信号量.md 8.6 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之FreeRTOS实时操作系统 V1.0.3 — 第11~12章 + 配套代码10/11/12/13" author: "尚硅谷研究院" date: 2026-07-12 url: ""

created: 2026-07-12

信号量与互斥信号量

用生活理解:信号量就像动车上的卫生间——

  • 二值信号量 = 卫生间指示灯:有人(0)/没人(1)。你想用卫生间,先看灯(获取信号量),绿灯就进去,红灯就等着
  • 计数信号量 = 停车场余位显示屏:显示剩余车位数。每进一辆车减1,每出一辆车加1
  • 互斥信号量 = 带"优先级继承"的卫生间(优化版):如果你(高优先级)想上厕所但被同事(低优先级)占着,系统会让那个同事赶紧上完让给你

信号量分类

类型 创建函数 计数值 典型用途 可递归
二值信号量 xSemaphoreCreateBinary() 0 或 1 同步(任务间/中断与任务间)
计数信号量 xSemaphoreCreateCounting(max,init) 0~max 资源管理、事件计数
互斥信号量 xSemaphoreCreateMutex() 0 或 1 互斥访问(带优先级继承)

通用操作xSemaphoreGive() = 释放(给),xSemaphoreTake() = 获取(拿)。是所有信号量都通用的!


示例 1:二值信号量(项目10)

情景

  • task1:按 KEY1 → 释放信号量(Give)
  • task2:获取信号量(Take),成功就打印

就像:task1 负责按铃,task2 听到铃响就开始干活。

核心代码

#include "semphr.h"  // 信号量头文件

SemaphoreHandle_t sem_handle;  // 信号量句柄

void freertos_start(void)
{
    // 参数1: 队列长度(二值信号量=1)
    // 参数2: 每个消息的大小(二值信号量=0)
    sem_handle = xSemaphoreCreateBinary();

    xTaskCreate(/* task1 */);
    xTaskCreate(/* task2 */);
    vTaskStartScheduler();
}

void task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        key = Key_Detect();
        if (key == KEY1_PRESS)
        {
            // 释放信号量(把卫生间灯从"有人"变"没人")
            xSemaphoreGive(sem_handle);
            printf("信号量已释放\r\n");
        }
        vTaskDelay(500);
    }
}

void task2(void *pvParameters)
{
    while (1)
    {
        // 获取信号量(等卫生间变空)
        // portMAX_DELAY = 死等,直到有信号量可用
        xSemaphoreTake(sem_handle, portMAX_DELAY);
        printf("获取信号量成功,开始干活\r\n");
    }
}

二值信号量的本质

二值信号量本质上就是一个长度=1、项目大小=0 的特殊队列。你不关心"消息内容"是什么,只关心"有没有消息"。

源码实现:信号量底层就是用队列实现的——xSemaphoreCreateBinary() 内部调用了 xQueueCreate(1, 0)。就像火车卫生间的指示灯——红灯=有人(已获取),绿灯=空闲(已释放)。队列什么都能干,信号量能干的队列都能干,但队列能干的信号量不一定能干。


示例 2:计数信号量(项目11)

情景

  • task1:按 KEY1 → 释放计数信号量(计数值+1,相当于进一辆车)
  • task2:每秒获取一次(计数值-1,相当于出一辆车)

核心代码

SemaphoreHandle_t count_sem;

void freertos_start(void)
{
    // 创建一个计数信号量:最大100,初始0
    count_sem = xSemaphoreCreateCounting(100, 0);
    // 最大计数=100:停车场最多停100辆车
    // 初始计数=0:停车场一开始是空的

    xTaskCreate(/* task1 */);
    xTaskCreate(/* task2 */);
    vTaskStartScheduler();
}

void task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        key = Key_Detect();
        if (key == KEY1_PRESS)
        {
            xSemaphoreGive(count_sem);  // 计数值+1
        }
        vTaskDelay(500);
    }
}

void task2(void *pvParameters)
{
    while (1)
    {
        xSemaphoreTake(count_sem, portMAX_DELAY);  // 计数值-1
        // 获取后查看当前计数值
        UBaseType_t count = uxSemaphoreGetCount(count_sem);
        printf("当前计数值: %d\r\n", count);
        vTaskDelay(1000);
    }
}

计数信号量的两种用法

用途 创建参数 行为
事件计数 CreateCounting(max, 0) Give=事件发生(+1),Take=处理事件(-1)
资源管理 CreateCounting(max, max) Take=占用资源(-1),Give=释放资源(+1)

示例 3:优先级翻转(项目12)

什么是优先级翻转?

用生活理解:你在机场排队安检(你就是高优先级任务A)。你前面有个老人(低优先级任务B)正在安检,你只能等着。这时来了个 VIP(中优先级任务C),工作人员让 VIP 插队到前面先安检。结果 VIP 安检完了,老人才能继续。老人完了才轮到你。问题:一个"插队的"反而让你(最高优先级)等了最久——这就是优先级翻转。

代码演示

// 三个任务优先级:Task1(2) < Task2(3) < Task3(4)
// 共享一个二值信号量

void task1(void *pvParameters)  // 低优先级 Task1(2)
{
    while (1)
    {
        xSemaphoreTake(sem_handle, portMAX_DELAY);  // 拿到信号量
        printf("低优先级Task1正在执行...\r\n");
        HAL_Delay(3000);  // 占用信号量3秒!
        xSemaphoreGive(sem_handle);
        vTaskDelay(1000);
    }
}

void task2(void *pvParameters)  // 中优先级 Task2(3)
{
    while (1)
    {
        printf("中优先级Task2正在执行...\r\n");
        HAL_Delay(1500);            // 纯粹的计算任务
        vTaskDelay(1000);
    }
}

void task3(void *pvParameters)  // 高优先级 Task3(4)
{
    while (1)
    {
        xSemaphoreTake(sem_handle, portMAX_DELAY);  // 等信号量
        printf("高优先级Task3正在执行...\r\n");
        HAL_Delay(1000);
        xSemaphoreGive(sem_handle);
        vTaskDelay(1000);
    }
}

优先级翻转的本质:"倒反天罡"——低优先级任务反而阻塞了高优先级任务。中优先级任务"山中无老虎,猴子称霸王"——不受限制地运行,持续抢占持有锁的低优先级任务,导致高优先级等待时间无限延长。这是信号量的典型问题,不是所有同步机制都有。

执行流程(问题所在):

时间 →
Task1(低) 拿信号量 ──── 干了3秒 ──── 释放
Task3(高)              等着...  等着...  拿到!
Task2(中)          打断了Task1  执行完
                     ^ 问题就在这里!
                     中优先级插队了,高优先级反而等最久

示例 4:互斥信号量解决优先级翻转(项目13)

解决方法

xSemaphoreCreateBinary() 换成 xSemaphoreCreateMutex(),其他代码完全一样!

// freertos_start 中只需改这一行
// sem_handle = xSemaphoreCreateBinary();   // 二值信号量:会有优先级翻转
sem_handle = xSemaphoreCreateMutex();        // 互斥信号量:自动处理优先级翻转

互斥信号量做了什么?

系统检测到"高优先级任务在等低优先级任务释放信号量"时,临时把低优先级任务的优先级提升到和高优先级一样高。这样中优先级任务就不能插队了,低优先级任务赶紧执行完释放信号量,高优先级任务就能拿到。用完就丢——资源释放后优先级再降回去。

创建时自动释放:创建互斥信号量时会自动执行一次 Give 操作,确保初始状态为"可用"。

互斥信号量下:
Task1(低) 拿信号量 ──── 干了3秒 ──── 释放
              ↑优先级被临时提升到4   ↑恢复原来优先级
Task3(高)   等着...                   拿到!
Task2(中)           想插队但发现Task1优先级比自己高了,插不了

互斥信号量的限制

限制 原因
❌ 不能在中断中使用 优先级继承需要在任务上下文中运行
✅ 可以递归获取 同一个任务可以多次 Take,但要Give同样次数

配套代码

  • code/10_二值信号量/
  • code/11_计数型信号量/
  • code/12_优先级翻转/
  • code/13_互斥信号量/

后续章节:[[09-事件标志组与任务通知]]