1.互斥量
互斥量 Mutex 主要用于保护共享资源,实现对临界资源的独占访问。
当一个任务成功获取互斥量后,该任务获得互斥量的所有权,此时互斥量处于被占用状态,其他任务不能再获取该互斥量,只能阻塞等待或超时返回。
互斥量和二值信号量区别之一就是:
互斥量有所有权。 二值信号量没有所有权概念。谁 Take 到 Mutex,谁就是 owner。
只有 owner 应该 Give 这个 Mutex。
当持有互斥量的任务访问完共享资源后,应释放互斥量,使互斥量重新变为可用状态。互斥量应该由持有它的任务释放,不能由其他任务随意释放。
普通互斥量不支持同一任务重复获取。如果需要同一个任务多次获取同一个锁,应使用递归互斥量,并且 Take 多少次就必须 Give 多少次。
1)互斥信号量
普通 Mutex 创建:
SemaphoreHandle_t printMutex; printMutex = xSemaphoreCreateMutex();使用:
xSemaphoreTake(printMutex, portMAX_DELAY); /* 访问共享资源 */ xSemaphoreGive(printMutex);如果同一个任务已经拿到了这个普通 Mutex,又再次执行:
xSemaphoreTake(printMutex, portMAX_DELAY);就可能把自己阻塞住,造成死锁。
比如:
void TaskA(void *argument) { xSemaphoreTake(printMutex, portMAX_DELAY); // 已经拿到 mutex 了 xSemaphoreTake(printMutex, portMAX_DELAY); // 这里可能死锁 xSemaphoreGive(printMutex); xSemaphoreGive(printMutex); }普通互斥量不适合这种重复加锁。
2)递归互斥量
递归互斥量创建:
SemaphoreHandle_t recursiveMutex; recursiveMutex = xSemaphoreCreateRecursiveMutex();递归获取:
xSemaphoreTakeRecursive(recursiveMutex, portMAX_DELAY); xSemaphoreTakeRecursive(recursiveMutex, portMAX_DELAY);释放时也要释放相同次数:
xSemaphoreGiveRecursive(recursiveMutex); xSemaphoreGiveRecursive(recursiveMutex);也就是说:
普通 Mutex:
同一个任务不应该重复 Take。
递归 Mutex:
同一个任务可以重复 Take,但必须 Give 相同次数。
互斥量通过优先级继承机制,缓解优先级反转问题。
因为 Mutex 的优先级继承主要解决一种典型情况:
低优先级任务拿着 Mutex
高优先级任务等待这个 Mutex
中优先级任务一直抢占低优先级任务
导致高优先级任务迟迟拿不到资源
这就是优先级反转。
用了 Mutex 后:
高优先级任务等待 Mutex ↓ 低优先级任务临时继承高优先级 ↓ 低优先级任务尽快运行并释放 Mutex ↓ 高优先级任务拿到 Mutex互斥量不能在中断中使用
互斥量是给任务用的,不给 ISR 用。
不要在中断里写:
xSemaphoreTake(uartMutex, 0); xSemaphoreGive(uartMutex);原因:1.Mutex 有所有权和优先级继承机制;2.中断不是任务;3.中断没有“任务所有权”概念;
中断通知任务,用:任务通知、二值信号量 FromISR 或队列 FromISR。
保护共享资源,用:Mutex
2.互斥量优先级继承
Mutex 的优先级继承机制用于缓解优先级反转问题。那么什么是优先级反转呢
优先级反转就是:当一个低优先级任务持有某个 Mutex,而高优先级任务也想获取这个 Mutex 时,高优先级任务会因为资源被占用而进入阻塞状态。此时如果存在中优先级任务不断抢占低优先级任务,就会导致低优先级任务迟迟无法运行并释放 Mutex,高优先级任务也一直无法继续运行。
为了解决这个问题,FreeRTOS 的 Mutex 支持优先级继承。当高优先级任务阻塞等待某个 Mutex 时,当前持有该 Mutex 的低优先级任务会临时继承等待任务中的最高优先级,使它能够尽快运行并释放 Mutex。当它释放 Mutex 后,它的优先级会恢复到原来的基础优先级。
优先级继承是 Mutex 的特性,二值信号量不具备这个机制。因此保护共享资源时应该优先使用 Mutex,而不是二值信号量。
一句话形容优先级继承就是:谁挡住了高优先级任务,谁就临时升到高优先级,赶紧把锁释放。
3.互斥量为什么不能当信号量使用
原因 1:Mutex 有所有权
Mutex 是“谁 Take,谁 Give”。
例如:
xSemaphoreTake(uartMutex, portMAX_DELAY); /* 使用 UART */ xSemaphoreGive(uartMutex);这个 Mutex 是当前任务拿到的,就应该由当前任务释放。
但是二值信号量常见用法是:中断Give,任务Take
这个场景中,Give 的不是同一个任务,甚至是 ISR。
这对二值信号量是正常用法,但对 Mutex 不合适。
原因 2:Mutex 不能在中断中使用
你不能在 ISR 里这样用 Mutex:
xSemaphoreGive(uartMutex); // 不推荐/不应在ISR中使用 xSemaphoreTake(uartMutex, 0); // 不应在ISR中使用因为中断不是任务,没有 Mutex 所有权概念。
如果是中断通知任务,应该用:
xSemaphoreGiveFromISR(binarySem, &xHigherPriorityTaskWoken);或者更推荐:
vTaskNotifyGiveFromISR(taskHandle, &xHigherPriorityTaskWoken);Mutex 是保护资源的,不是通知事件的
Mutex 解决的问题是:这个资源一次只能一个任务用。
例如:
多个任务同时 printf
多个任务同时访问 I2C
多个任务同时访问 SPI
多个任务同时访问 Flash
这时候用 Mutex。
二值信号量解决的问题是:某个事件发生了,通知任务去处理。
4多任务printf
1. 多个任务同时 printf 会怎样?
比如有两个任务:
TaskA: printf("TaskA: motor start\r\n"); TaskB: printf("TaskB: adc value = 123\r\n");如果没有 Mutex 保护,可能输出成这样:
TaskA: moTaskB: adc value = tor start 123或者:
TaskA: motor TaskB: adc value = 123 start因为printf()不是一个瞬间完成的动作。
它内部可能经历:
格式化字符串 ↓ 写入缓冲区 ↓ 调用串口发送 ↓ 一个字节一个字节发出去在这个过程中,FreeRTOS 可能发生任务切换。
所以 TaskA 还没打印完,TaskB 就插进来了,最终串口输出就会乱。
2. 正确做法:用普通 Mutex 保护 printf
应该设计一个统一的打印接口:
SemaphoreHandle_t printfMutex; void Debug_Init(void) { printfMutex = xSemaphoreCreateMutex(); } void Debug_Printf(const char *str) { if(xSemaphoreTake(printfMutex, portMAX_DELAY) == pdPASS) { printf("%s", str); xSemaphoreGive(printfMutex); } }如果Debug_Printf拿到 printfMutex 后不释放
其他任务再想 printf
都会阻塞在 xSemaphoreTake()
整个打印功能相当于被 Debug_Printf锁死
普通 Mutex 要求 Take 和 Give 成对出现,不建议同一任务重复获取同一个 Mutex。
3.如果同一个任务因为函数嵌套需要多次获取同一个锁,应使用递归互斥量,并且 TakeRecursive 几次就必须 GiveRecursive 几次。
比如:
void Debug_Printf(const char *str) { xSemaphoreTakeRecursive(debugMutex, portMAX_DELAY); printf("%s", str); xSemaphoreGiveRecursive(debugMutex); } void Motor_PrintStatus(void) { xSemaphoreTakeRecursive(debugMutex, portMAX_DELAY); Debug_Printf("motor status\r\n"); xSemaphoreGiveRecursive(debugMutex); }5.串口调试打印示例
#include "FreeRTOS.h" #include "semphr.h" #include "usart.h" #include <stdio.h> #include <stdarg.h> #include <string.h> #define DEBUG_PRINTF_BUF_SIZE 256 static SemaphoreHandle_t g_debug_printf_mutex = NULL; static char g_debug_printf_buf[DEBUG_PRINTF_BUF_SIZE]; /* Debug_Printf 初始化 */ void Debug_Printf_Init(void) { g_debug_printf_mutex = xSemaphoreCreateMutex(); if(g_debug_printf_mutex == NULL) { /* Mutex 创建失败,通常是 FreeRTOS heap 不够 */ Error_Handler(); } } /* 统一打印接口 */ void Debug_Printf(const char *fmt, ...) { va_list args; int len; if(g_debug_printf_mutex == NULL) { return; } /* 获取 Mutex。 如果其他任务正在 printf,本任务会阻塞等待。 */ if(xSemaphoreTake(g_debug_printf_mutex, portMAX_DELAY) == pdPASS) { va_start(args, fmt); len = vsnprintf(g_debug_printf_buf, DEBUG_PRINTF_BUF_SIZE, fmt, args); va_end(args); if(len > 0) { if(len > DEBUG_PRINTF_BUF_SIZE) { len = DEBUG_PRINTF_BUF_SIZE; } /* 串口发送。 这里用阻塞发送做演示。 实际工程可以改成 DMA 发送。 */ HAL_UART_Transmit(&huart1, (uint8_t *)g_debug_printf_buf, len, 100); } /* 打印完成后必须释放 Mutex。 否则其他任务会一直无法 printf。 */ xSemaphoreGive(g_debug_printf_mutex); } }使用示例:
void TaskA(void *argument) { while(1) { Debug_Printf("TaskA running\r\n"); vTaskDelay(pdMS_TO_TICKS(1000)); } } void TaskB(void *argument) { while(1) { Debug_Printf("TaskB value = %d\r\n", 123); vTaskDelay(pdMS_TO_TICKS(500)); } }初始化位置:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); Debug_Printf_Init(); MX_FREERTOS_Init(); osKernelStart(); while(1) { } }