Q: 什么是互斥量?
A: 在多数情况下,互斥型信号量和二值型信号量非常相似,但是从功能上二值型信号量用于同步, 而互斥型信号量用于资源保护。 互斥型信号量和二值型信号量还有一个最大的区别,互斥型信号量可以有效解决优先级反转现象。
优先级反转现象
以上图为例,系统中有3个不同优先级的任务 H/M/L,最高优先级任务H和最低优先级任务L通过普通二值信号量机制,共享资源。目前任务L占有资源,锁定了信号量,Task H运行后将被阻塞,直到Task L释放信号量后,Task H才能够退出阻塞状态继续运行。但是Task H在等待Task L释放信号量的过 程中,中等优先级任务M抢占了任务L(能够抢占的原因是Task H 和 L 通过信号量来共享资源,但是Task M没有参考信号量,所以可以直接打断优先级低的任务),从而延迟了信号量的释放时间,导致Task H阻塞了更长时间,Task M优先级明明比Task H低,但是却可以阻塞它,这种现象称为优先级倒置或反转。
优先级继承
优先级继承是一种使用互斥信号量解决“优先级反转”问题的方法,但是不能完全解决,只能尽可能降低“优先级反转”带来的影响。
当一个互斥信号量正在被一个低优先级的任务持有时, 如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级。
如果上图中的例子中,Tas H 和 Task L 共享的不是普通二值信号量而是互斥信号量,那么当Task L占用信号量的时候,高优先级的Task H依然会被阻塞,但是同时也会将低优先级的Task L的优先级提升至和自己一样的最高级,所以中等优先级的Task M就无法再抢占Task L了,Task H也就不会阻塞很长时间了
互斥量相关 API 函数
注意!互斥信号量不能用于中断服务函数中!
- 输入参数:无
- 返回值: 成功,返回对应互斥量的句柄; 失败,返回 NULL
注意,和之前普通二值信号量和计数信号量不同,之前的二值和计数信号量的原厂API函数在创建完信号量之后就结束了,是Cube再次对其封装,赋予了“创建完信号量就全部释放”的功能,但是互斥量的原厂API函数在创建后,就会自动释放一个互斥量,这个功能不需要Cube封装,是自带的。
实操演示
需求: 1. 演示优先级反转 2. 使用互斥量优化优先级翻转问题
在 C:\mjm_CubeMX_proj 路径下,复制一份Cube的母版并重命名为 :mjm_freeRTOS_Mute:
优先级反转演示:
1. 打开相应的Cube文件,找到左侧的Middleware --> FREERTOS
1.1 然后在下方找到"Task and Queues",创建三个不同优先级的任务:
1.2 在下方找到"Timers and Semaphores",创建普通二值信号量:
2. 生成代码并打开Keil, 编写三个任务的内容:
注意!在写释放信号量的代码时,不能写成下面这种形式,因为在这个例程中,TASK_L和TASK_H都在不停的获取信号量,一旦将printf写在释放信号量函数之后,就会导致printf显示的位置错误,因为信号量一旦被释放,就会被另一个任务所获取了。
if (xSemaphoreGive(myBinarySemHandle) == pdTRUE){ //释放信号量并判断返回值 printf("Task_L has stopped\r\n"); }
#include "stdio.h"
void StartTask_H(void const * argument)
{
for(;;)
{
if (xSemaphoreTake(myBinarySemHandle, portMAX_DELAY ) == pdTRUE){ //获取信号量并判断返回值,注意阻塞时间要设为最大,否则当TASK_L占用时,其他任务会直接获取失败并返回,不会等了
printf("Task_H is running......\r\n");
HAL_Delay(3000);
printf("Task_H has stopped\r\n");
xSemaphoreGive(myBinarySemHandle); //释放信号量
}
osDelay(1000); //注意,此处的Delay必不可少,因为如果没有Delay,信号量将不断被TASK_H 和 Task_L 所获取和释放,轮不到Task_M执行,无法复现优先级反转的效果
}
}
void StartTask_M(void const * argument)
{
for(;;)
{
printf("Task_M:HELLO MJM\r\n"); //Task_M不占用信号量,只是单纯占用CPU打印一句话
osDelay(1000);
}
}
void StartTask_L(void const * argument)
{
for(;;)
{
if (xSemaphoreTake(myBinarySemHandle, portMAX_DELAY ) == pdTRUE){ //获取信号量并判断返回值,注意阻塞时间要设为最大,否则当TASK_L占用时,其他任务会直接获取失败并返回,不会等了
printf("Task_L is running......\r\n");
HAL_Delay(3000);
printf("Task_L has stopped\r\n");
xSemaphoreGive(myBinarySemHandle); //释放信号量
}
osDelay(1000); //注意,此处的Delay必不可少,因为如果没有Delay,信号量将不断被TASK_H 和 Task_L 所获取和释放,轮不到Task_M执行,无法复现优先级反转的效果
}
}
实现效果1
打开串口助手:
回顾刚刚提到的优先级反转的例子:
可见,被鼠标蓝色选中的区域就发生了优先级反转的现象。
使用互斥量优化的演示:
1. 在上个演示的Cube文件中,找到左侧的Middleware --> FREERTOS
1.1 在下方找到"Timers and Semaphores",删除刚刚创建的普通二值信号量:
1.2 在下方找到"Mutexes",创建互斥量:
2. 生成代码并打开Keil, 重写三个任务的内容:
2.1 在freertos.c 中可以看到创建互斥量的代码,和二值信号量的创建非常类似
2.2 重写代码,其实就是将二值信号量的句柄换成互斥量:
使用二值信号量的获取和释放函数可以直接适用于互斥量,从侧面印证了互斥量就是一种特殊的二值信号量。
#include "stdio.h"
void StartTask_H(void const * argument)
{
for(;;)
{
if (xSemaphoreTake(myMutexHandle, portMAX_DELAY ) == pdTRUE){ //获取信号量并判断返回值,注意阻塞时间要设为最大
printf("Task_H is running......\r\n");
HAL_Delay(3000);
printf("Task_H has stopped\r\n");
xSemaphoreGive(myMutexHandle); //释放信号量
}
osDelay(1000); //注意,此处的Delay必不可少,因为如果没有Delay,信号量将不断被TASK_H 和 Task_L 所获取和释放,轮不到Task_M执行,无法复现优先级反转的效果
}
}
void StartTask_M(void const * argument)
{
for(;;)
{
printf("Task_M:HELLO MJM\r\n"); //Task_M不占用信号量,只是单纯占用CPU打印一句话
osDelay(1000);
}
}
void StartTask_L(void const * argument)
{
for(;;)
{
if (xSemaphoreTake(myMutexHandle, portMAX_DELAY ) == pdTRUE){ //获取信号量并判断返回值,注意阻塞时间要设为最大,否则当TASK_L占用时,其他任务会直接获取失败并返回,不会等了
printf("Task_L is running......\r\n");
HAL_Delay(3000);
printf("Task_L has stopped\r\n");
xSemaphoreGive(myMutexHandle); //释放信号量
}
osDelay(1000); //注意,此处的Delay必不可少,因为如果没有Delay,信号量将不断被TASK_H 和 Task_L 所获取和释放,轮不到Task_M执行,无法复现优先级反转的效果
}
}
实现效果2
再次打开串口助手:
可见,在使用互斥量了之后,Task_M不再具备打断Task_L的能力了。