FreeRTOS内核(四)信号量与互斥量的核心机制
·
一、信号量的本质
信号量是一种特殊的队列—— 复用队列的 “互斥保护 + 休眠唤醒” 机制,但放弃了 “数据传输” 功能,仅保留 “计数值管理” 能力。
| 特性维度 | 普通队列 (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 快速执行完临界区代码,释放互斥量。
- 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 个关键设计:
- 计数值:复用队列的
uxMessagesWaiting字段,代表「资源数 / 可用状态」; - 无数据区:创建时指定
uxItemSize=0,无需拷贝数据,仅管理计数值和等待状态; - 等待链表:复用
xTasksWaitingToReceive(等待获取)、xTasksWaitingToSend(等待释放),管理阻塞的任务; - 原子操作:所有计数值修改(++/--)均在关中断后执行,保证多任务 / 中断下的原子性。
1. 信号量使用原则
- 需任务与中断同步 → 必用二进制信号量(中断版释放 API);
- 需多资源计数 → 必用计数型信号量;
- 无高优先级任务的简单互斥 → 可使用二进制信号量(轻量化);
- 避免在高优先级任务参与的互斥场景中使用二进制信号量(会引发优先级反转)。
2. 互斥量使用原则
- 有高优先级任务的共享资源互斥 → 必用互斥量(优先级继承解决反转);
- 仅能在任务间使用,中断中绝对不能调用互斥量 API;
- 互斥量释放必须与获取同任务(非持有者释放会失败);
- 嵌套获取时,释放次数必须等于获取次数(否则不会真正释放)。
3. 通用原则
- 所有获取操作需合理设置等待时间,避免无限等待(
portMAX_DELAY)导致任务死锁; - 中断中仅能使用带 FromISR 后缀的 API(仅信号量支持),且需配合中断安全的队列 / 信号量;
- 计数值的修改均由内核保证原子性,用户无需额外加锁;
- 等待的任务会按优先级排序,唤醒时优先唤醒最高优先级任务。
更多推荐



所有评论(0)