Chapter 05 · FreeRTOS 实时操作系统

信号量与互斥锁

掌握二值/计数信号量与互斥量的区别,理解优先级翻转这一经典陷阱以及 FreeRTOS 的优先级继承对策,学会递归互斥量与 ISR 版本。

课程进度42%

关键概念

信号量 (Semaphore)
一个带计数的同步原语,内核里由不存数据的队列实现。Take 使计数减一(不足则阻塞),Give 使计数加一(唤醒等待者)。用于任务/中断间的事件同步资源计数,不像互斥量那样强调"谁拿谁还"。
二值信号量 (Binary Semaphore)
计数只有 0/1 的信号量,像一面旗子。典型场景:中断里 GiveFromISR 通知任务"事件来了",任务 Take 后处理。它是"中断延迟处理"的经典工具,创建后初始为空(需先 Give 才能 Take)。V8.2 后此场景更推荐用第 3 章的任务通知,更快更省。
计数信号量 (Counting Semaphore)
计数可 0 到 maxCount 的信号量,用于管理"有多少个可用资源"。例如 3 个 DMA 通道,初值设 3,每占用一个 Take,用完 Give。也可当事件计数器:中断多次 Give 累积,任务逐个 Take 处理,不会丢事件(只要不溢出)。
互斥量 (Mutex)
Mutual Exclusion,专门保护临界资源(共享变量、外设、SD 卡等)。它有"持有者"概念:谁 Take 谁必须自己 Give,不能跨任务。最关键的是它内建优先级继承机制来缓解优先级翻转。互斥量创建后初始为"可用"(可直接 Take)。
优先级翻转 (Priority Inversion)
经典实时系统陷阱:低优先级任务 L 持有互斥量,高优先级任务 H 想要该锁被阻塞;此时一个中等优先级任务 M 就绪,抢占了 L。结果 L 迟迟无法释放锁,H 被 M 间接卡住——高优先级任务实际被中优先级任务拖延。火星探路者号曾因此重启。
优先级继承 (Priority Inheritance)
FreeRTOS 互斥量的对策:当高优先级任务 H 因等锁而阻塞时,临时把锁的持有者 L 的优先级提升到 H 的水平,使 L 不被中优先级任务抢占,尽快跑完临界区释放锁;L 释放后优先级恢复原值。它不能根治翻转,但把最坏延迟限制在临界区长度内。信号量没有此机制。
递归互斥量 (Recursive Mutex)
允许同一个任务重复 Take 多次而不死锁的互斥量,内部维护一个持有计数,须 Take 几次就 Give 几次才真正释放。用于一个已持锁的函数又调用另一个也要取同一把锁的函数的场景。需 configUSE_RECURSIVE_MUTEXES=1,用 xSemaphoreTakeRecursive/GiveRecursive
死锁 (Deadlock)
两个任务各持一把锁又都在等对方的锁,互不相让,永久阻塞。避免手段:规定统一的加锁顺序、尽量只持一把锁、给 Take 设合理超时而非永久等待、缩短临界区。互斥量不能自动解死锁,只能靠设计规避。

信号量 vs 互斥量:核心差异

信号量(同步/计数)

  • 用于事件通知、资源计数
  • 可 A 任务 Give、B 任务 Take
  • 无持有者概念
  • 优先级继承
  • 二值信号量创建后初始为空

互斥量(保护资源)

  • 用于保护临界资源
  • 谁 Take 谁必须自己 Give
  • 有持有者概念
  • 内建优先级继承
  • 创建后初始为可用
保护共享数据,一定用互斥量而非二值信号量

二值信号量看起来也能当锁用,但它没有优先级继承,会引入不可控的优先级翻转延迟;而且它允许跨任务 Give/Take,容易写出逻辑错误。凡是"保护临界区"就用 Mutex;凡是"通知事件/计数资源"才用信号量。这是 FreeRTOS 的黄金准则。

优先级翻转全过程图解

优先级翻转 与 优先级继承对比 ═══════════════════════════════════════════════════════════ 无优先级继承(翻转发生) H(高): ....[想拿锁,阻塞]............................[拿到,运行] M(中): .........[就绪,抢占L,长时间运行]███████████ L(低): [拿锁]██[被M抢占,无法释放锁].............[释放锁] ↑ H 被 M 间接拖延,实时性被破坏 有优先级继承(FreeRTOS Mutex) H(高): ....[想拿锁,阻塞]..[拿到,运行] L(低): [拿锁]██[优先级临时升到H]██[释放锁,降回] M(中): .........[就绪,但打不过被提升的L].....[运行] ↑ L 尽快跑完临界区,H 延迟被限制在临界区长度内

二值信号量:中断延迟处理

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

SemaphoreHandle_t g_binSem = NULL;

void EventTask(void *arg) {
  for (;;) {
    /* 等待中断给出的信号量;永久阻塞直到事件到来 */
    if (xSemaphoreTake(g_binSem, portMAX_DELAY) == pdTRUE) {
      handle_event();   /* 在任务里从容处理 */
    }
  }
}

void EXTI0_IRQHandler(void) {
  BaseType_t woken = pdFALSE;
  __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
  /* ISR 版:给出信号量,唤醒 EventTask */
  xSemaphoreGiveFromISR(g_binSem, &woken);
  portYIELD_FROM_ISR(woken);
}

void setup_bin(void) {
  g_binSem = xSemaphoreCreateBinary();   /* 初始为空 */
  xTaskCreate(EventTask, "EVT", 256, NULL, 3, NULL);
}

互斥量:保护共享外设

SemaphoreHandle_t g_i2cMutex = NULL;

/* 多个任务都要用同一条 I2C 总线,须互斥访问 */
float read_sensor_locked(uint8_t addr) {
  float v = 0;
  /* 取锁,最多等 50ms(避免永久死锁) */
  if (xSemaphoreTake(g_i2cMutex,
                     pdMS_TO_TICKS(50)) == pdTRUE) {
    v = i2c_read_float(addr);   /* 临界区:独占 I2C */
    xSemaphoreGive(g_i2cMutex);  /* 谁取谁还 */
  } else {
    printf("I2C busy, read timeout\r\n");
  }
  return v;
}

void setup_mutex(void) {
  /* 互斥量:创建后初始"可用",内建优先级继承 */
  g_i2cMutex = xSemaphoreCreateMutex();
  configASSERT(g_i2cMutex);
}
互斥量绝不能在 ISR 中使用

互斥量的优先级继承依赖"持有者任务"的概念,而中断没有任务上下文,因此没有 xSemaphoreTakeFromISR 这种互斥量 ISR API。中断里需要同步时用二值/计数信号量的 *FromISR 版本,或用任务通知。若在 ISR 里误用互斥量会破坏内核。

计数信号量:管理资源池

/* 3 个可用的 DMA 通道 */
SemaphoreHandle_t g_dmaPool;

void setup_count(void) {
  /* 最大计数 3,初始可用 3 */
  g_dmaPool = xSemaphoreCreateCounting(3, 3);
}

void use_dma(void) {
  if (xSemaphoreTake(g_dmaPool, pdMS_TO_TICKS(100))) {
    int ch = acquire_dma_channel();  /* 占一个通道 */
    do_transfer(ch);
    release_dma_channel(ch);
    xSemaphoreGive(g_dmaPool);        /* 归还 */
  }
}

递归互斥量

/* 需 configUSE_RECURSIVE_MUTEXES = 1 */
SemaphoreHandle_t g_rec;

void outer(void) {
  xSemaphoreTakeRecursive(g_rec, portMAX_DELAY);  /* 计数=1 */
  inner();                                          /* 内部再取同一锁 */
  xSemaphoreGiveRecursive(g_rec);                  /* 计数=1 */
}
void inner(void) {
  xSemaphoreTakeRecursive(g_rec, portMAX_DELAY);  /* 计数=2,不死锁 */
  /* ... 临界操作 ... */
  xSemaphoreGiveRecursive(g_rec);                  /* 计数=1 */
}
/* Take 两次必须 Give 两次,计数归 0 才真正释放 */
本章小结

信号量用于事件同步与资源计数:二值信号量像旗子(中断 GiveFromISR 通知任务 Take),计数信号量管理"多少个可用资源";两者都无持有者概念、可跨任务 Give/Take、无优先级继承互斥量专门保护临界资源,谁 Take 谁 Give,创建后初始可用,且内建优先级继承优先级翻转是指低优先级任务持锁、高优先级任务等锁时被中优先级任务间接拖延;FreeRTOS 互斥量用优先级继承临时提升持锁者优先级来缓解,把延迟限制在临界区长度内。保护共享数据一律用 Mutex 而非二值信号量。递归互斥量(configUSE_RECURSIVE_MUTEXES)允许同任务重复 Take,Take 几次 Give 几次。互斥量不能在 ISR 用(无 FromISR 版本);死锁只能靠统一加锁顺序、设超时、缩短临界区来规避。API:xSemaphoreCreateBinary/Counting/Mutex/RecursiveMutex + xSemaphoreTake/Give(+Recursive/+FromISR)。