一、信号量的本质

信号量是一种特殊的队列—— 复用队列的 “互斥保护 + 休眠唤醒” 机制,但放弃了 “数据传输” 功能,仅保留 “计数值管理” 能力。

特性维度 普通队列 (Queue) 信号量 (Semaphore)
(本质是特殊的队列)
核心用途 传递数据
用于任务间、中断与任务间传输具体的数据内容(如传感器数值、指令包)。
管理资源计数 / 同步
用于统计可用资源数量(如停车位、DMA通道)或事件同步(如 ISR 通知任务),不传递具体数据。
数据存储区
(Buffer)

分配一块连续的环形缓冲区。内存布局:[队列头] + [数据区]

不分配数据缓冲区。内存布局:仅 [队列头]
(通过设置 uxItemSize = 0 实现,节省内存)
关键创建参数 uxItemSize > 0
必须指定每个元素的大小(字节数),以便 malloc 数据区。
uxItemSize = 0
元素大小设为 0,告诉内核不需要为数据分配空间。
核心字段复用
uxMessagesWaiting
表示当前队列中有效的消息条数。 表示当前可用的资源计数值 (Count)。
发送/释放操作
(Give/Send)
1. 关中断
2. 拷贝数据到缓冲区 (memcpy)
3. 计数值 +1
4. 唤醒等待接收的任务
1. 关中断
2. 跳过数据拷贝 (因大小为 0)
3. 计数值 +1
4. 唤醒等待获取的任务
接收/获取操作
(Take/Receive)
1. 关中断
2. 检查计数值 > 0 ?
3. 是:拷贝数据出缓冲区
4. 计数值 -1
5. 否:阻塞等待
1. 关中断
2. 检查计数值 > 0 ?
3. 是:跳过数据拷贝,直接计数值 -1
4. 否:阻塞等待
等待链表 两个链表:
1. xTasksWaitingToSend (写满时等待)
2. xTasksWaitingToReceive (读空时等待)
复用相同的链表结构,但通常只关注一个方向:
- 二值/计数信号量:主要用 xTasksWaitingToReceive (等资源时等待)。
- 互斥量:额外记录持有者信息。
典型应用场景 - 串口数据接收缓冲
- 传感器数据共享
- 命令下发
- 二值信号量:中断通知任务、任务间简单握手。
- 计数信号量:限制同时访问资源的任务数 (如只有 3 个打印机)。
性能开销 较高
涉及内存分配、数据拷贝 (memcpy)。
极低
无内存拷贝,仅修改整数和链表指针,速度极快。

二、信号量分类

类型 创建 API 计数范围 典型用途
二进制信号量 xSemaphoreCreateBinary() 0 或 1 同步:ISR 通知任务、任务间简单握手。类似“标志位”,但带阻塞功能。
计数型信号量 xSemaphoreCreateCounting(max, start) 0 ~ Max 资源管理:如停车场模型、限制同时访问某资源的任务数量 (如只有 3 个 DMA 通道)。

两种信号量都是直接调用xQueueGenericCreate,进行的特殊队列创建

二进制信号量创建原理

#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
    // 【核心】直接是一个宏,直接调用 xQueueGenericCreate
    // 参数解读:
    // (UBaseType_t) 1: 队列长度为 1 (只有 0 或 1 两种状态)
    // semSEMAPHORE_QUEUE_ITEM_LENGTH: 元素大小 (宏定义通常为 0) -> 【关键点:不分配数据区】
    // queueQUEUE_TYPE_BINARY_SEMAPHORE: 队列类型标记
    #define xSemaphoreCreateBinary()    xQueueGenericCreate( ( UBaseType_t ) 1, 
                                                             semSEMAPHORE_QUEUE_ITEM_LENGTH, 
                                                             queueQUEUE_TYPE_BINARY_SEMAPHORE )
#endif

计数信号量创建原理

#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
    #define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount )    xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
#endif


QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount,
                                             const UBaseType_t uxInitialCount )
{
    QueueHandle_t xHandle;

    // 1. 断言检查:最大计数值不能为 0,初始值不能大于最大值
    configASSERT( uxMaxCount != 0 );
    configASSERT( uxInitialCount <= uxMaxCount );

    // 2. 【核心】调用通用队列创建函数
    // 参数解读:
    // uxMaxCount: 队列长度 (即信号量的最大资源数,如停车场总车位)
    // queueSEMAPHORE_QUEUE_ITEM_LENGTH: 元素大小 (宏定义通常为 0) -> 【关键点:不分配数据区】
    // queueQUEUE_TYPE_COUNTING_SEMAPHORE: 队列类型标记 (用于内部逻辑判断)
    xHandle = xQueueGenericCreate( uxMaxCount, 
                                   queueSEMAPHORE_QUEUE_ITEM_LENGTH, 
                                   queueQUEUE_TYPE_COUNTING_SEMAPHORE );

    if( xHandle != NULL )
    {
        // 3. 【特殊操作】手动初始化计数值
        // 普通队列创建时,消息数默认为 0。
        // 但信号量允许设置初始资源数 (如系统启动时就有 5 个可用资源)。
        // 这里直接修改队列头中的 uxMessagesWaiting 字段。
        ( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;

        traceCREATE_COUNTING_SEMAPHORE();
    }
    else
    {
        traceCREATE_COUNTING_SEMAPHORE_FAILED();
    }

    return xHandle;
}

三、信号量Take/Give

1.Take(count--)

2.Give(count++)

操作 普通队列 (Queue) 信号量 (Semaphore)
Give (发送/释放) 1. 关中断
2. 拷贝数据到 Buffer
3. uxMessagesWaiting++
4. 唤醒等待接收的任务
1. 关中断
2. 不拷贝数据 (Size=0)
3. uxMessagesWaiting++ (Count++)
4. 唤醒等待获取的任务
Take (接收/获取) 1. 关中断
2. 检查 uxMessagesWaiting > 0?
3. 是:拷贝数据出 Buffer,Count--
4. 否:阻塞 (加入等待链表)
1. 关中断
2. 检查 uxMessagesWaiting > 0?
3. 是:不拷贝,直接 Count--
4. 否:阻塞 (加入等待链表)

四、互斥量的引入:优先级反转

先把互斥量当做二维信号量,了解优先级翻转问题

任务 L (低优先级) 运行,获取了互斥量(占用资源)。

任务 H (高优先级) 就绪,抢占 L 运行。

任务 H 尝试获取同一个互斥量 -> 失败(被 L 占有)。

        H 进入阻塞状态,等待 L 释放。

任务 M (中优先级) 就绪。

        因为 L 的优先级 < M,M 抢占了 L

        L 无法运行 -> 无法释放互斥量。

        H 继续等待...

结果高优先级任务 H 被中优先级任务 M 卡死了,尽管 H 和 M 没有任何资源冲突。这就是“优先级反转”。

解决方法:优先级继承(互斥量特性)

当 高优先级任务 H 因获取互斥量失败而阻塞时:

检测:H 发现互斥量被低优先级任务 L 持有。

继承:内核临时提升 L 的优先级,使其等于 H 的优先级。

执行

  • L 现在变成了“高优先级”,可以立即抢占 M 运行。
  • L 快速执行完临界区代码,释放互斥量。
  • 恢复原始低优先级。
  • H 获得互斥量,立即运行。

结果:M 虽然插队了,但无法长时间阻塞 H。H 尽快拿到了资源。

特性维度 二进制信号量 (Binary Semaphore) 互斥量 (Mutex)
计数值范围 0 或 1
(0=无资源/未触发,1=有资源/已触发)
0 或 1
(0=已被持有,1=空闲可用)
(数值表现相同,但语义不同)
核心机制 关中断 + 链表阻塞/唤醒
(纯计数逻辑)
关中断 + 链表阻塞/唤醒 + 优先级继承
(含所有权管理逻辑)
主要用途 同步 (Synchronization)
1. 中断通知任务 (ISR -> Task)
2. 任务间简单握手 (Task A -> Task B)
(关注“事件是否发生”)
互斥 (Mutual Exclusion)
保护共享资源 (如串口、I2C、全局变量),防止多任务同时访问。
(关注“谁在使用资源”)
优先级反转 ❌ 无法解决 ✅ 完美解决
通过优先级继承机制:当高优先级任务等待时,临时提升持有锁的低优先级任务的优先级,使其尽快释放锁。
所有权概念 ❌ 无所有权
任何任务都可以 Give (释放),任何任务都可以 Take (获取)。
✅ 严格所有权
必须由获取锁的任务来释放锁。
(例如:任务 A 获取,若任务 B 尝试释放,操作会失败或报错)
底层本质 特殊队列
长度=1,元素大小=0,类型=QUEUE_TYPE_BINARY
特殊队列
长度=1,元素大小=0,类型=QUEUE_TYPE_MUTEX
(额外维护 pxMutexHolder 指针和原始优先级字段)
典型应用场景 - 按键中断唤醒处理任务
- DMA 传输完成通知
- 两个任务间的简单流程同步
- 访问共享硬件外设 (UART, SPI)
- 修改全局临界区数据
- 文件系统操作保护
创建后初始状态 通常为 0 (不可用)
(需等待一次 Give 才能 Take)
通常为 1 (可用)
(创建后即可被第一个任务获取)

五、互斥量核心

1.互斥量创建

底层依旧使用xQueueGenericCreate,但是被指定为queueQUEUE_TYPE_MUTEX类型

#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
    #define xSemaphoreCreateMutex()    xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
#endif


QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )
{
    QueueHandle_t xNewQueue;
    
    // 1. 定义常量:互斥量的“铁律”
    // 长度固定为 1:互斥量只有两种状态——“有人持有”(0) 或 “无人持有”(1)。
    // 就像停车场的只有一个专属车位,要么被占了,要么空着。
    const UBaseType_t uxMutexLength = ( UBaseType_t ) 1; 
    
    // 大小固定为 0:互斥量不传递数据,只记录“谁持有它”。
    // 所以不需要数据缓冲区 (Buffer),省内存!
    const UBaseType_t uxMutexSize = ( UBaseType_t ) 0;

    // 2. 【核心】复用队列创建函数
    // 传入参数:长度 1, 大小 0, 类型 (由外部传入,区分是普通互斥量还是递归互斥量)
    xNewQueue = xQueueGenericCreate( uxMutexLength, uxMutexSize, ucQueueType );

    // 3. 【关键区别】额外初始化
    // 普通队列和信号量创建完就直接返回了。
    // 但互斥量需要记录“是谁持有我?”以及“持有者的原始优先级是多少?”。
    // 这些额外信息普通队列不需要,所以必须单独调用这个函数来初始化。
    prvInitialiseMutex( ( Queue_t * ) xNewQueue );

    return xNewQueue;
}

2.互斥量获取(xQueueSemaphoreTake)

  • 先尝试 “无锁获取”(计数值 > 0 直接拿),失败则进入 “休眠 + 继承” 逻辑;
  • 优先级继承仅在 “互斥量 + 未超时 + 无资源” 时触发;
  • 超时后需检查是否触发过继承,若有则恢复优先级。

3.优先级继承(xTaskPriorityInherit)

  • 唯一触发条件:持有者当前优先级 < 当前任务优先级;
  • 核心操作:改uxPriority + 移就绪链表(就绪态任务);
  • 非就绪态仅改优先级,唤醒时自动生效。

4、互斥量释放(xQueueGenericSend)

  • 计数值 + 1(标记互斥量可用);
  • 恢复持有者的原始优先级(如果之前触发过优先级继承);
  • 唤醒等待互斥量的高优先级任务;
  • 清空互斥量持有者标记。

六、总结

信号量、互斥量的句柄本质都是队列句柄,底层均通过xQueueGenericCreate创建,核心复用队列的 3 个关键设计:

  1. 计数值:复用队列的uxMessagesWaiting字段,代表「资源数 / 可用状态」;
  2. 无数据区:创建时指定uxItemSize=0,无需拷贝数据,仅管理计数值和等待状态;
  3. 等待链表:复用xTasksWaitingToReceive(等待获取)、xTasksWaitingToSend(等待释放),管理阻塞的任务;
  4. 原子操作:所有计数值修改(++/--)均在关中断后执行,保证多任务 / 中断下的原子性。

1. 信号量使用原则

  1. 任务与中断同步 → 必用二进制信号量(中断版释放 API);
  2. 多资源计数 → 必用计数型信号量;
  3. 无高优先级任务的简单互斥 → 可使用二进制信号量(轻量化);
  4. 避免在高优先级任务参与的互斥场景中使用二进制信号量(会引发优先级反转)。

2. 互斥量使用原则

  1. 有高优先级任务的共享资源互斥 → 必用互斥量(优先级继承解决反转);
  2. 仅能在任务间使用,中断中绝对不能调用互斥量 API;
  3. 互斥量释放必须与获取同任务(非持有者释放会失败);
  4. 嵌套获取时,释放次数必须等于获取次数(否则不会真正释放)。

3. 通用原则

  1. 所有获取操作需合理设置等待时间,避免无限等待(portMAX_DELAY)导致任务死锁;
  2. 中断中仅能使用带 FromISR 后缀的 API(仅信号量支持),且需配合中断安全的队列 / 信号量;
  3. 计数值的修改均由内核保证原子性,用户无需额外加锁;
  4. 等待的任务会按优先级排序,唤醒时优先唤醒最高优先级任务。
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐