信号量与互斥锁
掌握二值/计数信号量与互斥量的区别,理解优先级翻转这一经典陷阱以及 FreeRTOS 的优先级继承对策,学会递归互斥量与 ISR 版本。
掌握二值/计数信号量与互斥量的区别,理解优先级翻转这一经典陷阱以及 FreeRTOS 的优先级继承对策,学会递归互斥量与 ISR 版本。
Take 使计数减一(不足则阻塞),Give 使计数加一(唤醒等待者)。用于任务/中断间的事件同步和资源计数,不像互斥量那样强调"谁拿谁还"。GiveFromISR 通知任务"事件来了",任务 Take 后处理。它是"中断延迟处理"的经典工具,创建后初始为空(需先 Give 才能 Take)。V8.2 后此场景更推荐用第 3 章的任务通知,更快更省。configUSE_RECURSIVE_MUTEXES=1,用 xSemaphoreTakeRecursive/GiveRecursive。二值信号量看起来也能当锁用,但它没有优先级继承,会引入不可控的优先级翻转延迟;而且它允许跨任务 Give/Take,容易写出逻辑错误。凡是"保护临界区"就用 Mutex;凡是"通知事件/计数资源"才用信号量。这是 FreeRTOS 的黄金准则。
#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); }
互斥量的优先级继承依赖"持有者任务"的概念,而中断没有任务上下文,因此没有 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)。