一、互斥量简介
1、互斥量用于互锁,可以充当资源保护的令牌,当一个任务希望访问某个资源时,它必须先获取令牌,当任务使用完资源后,必须返还令牌,以便其他任务可以访问该资源。
2、互斥量一般用于临界资源保护。
3、用互斥量处理不同任务队临界资源的同步访问时,任务想要获取互斥量才能进行资源访问,如果一旦有任务成功获得了互斥量,则互斥量立即变为闭锁状态,此时其他任务会因为获取不到互斥量而不能访问这个资源,任务会根据用户自定义的等待时间进行等待,直到互斥量被持有的任务释放后,其他任务才能获取互斥量从而得到访问该临界资源,此时互斥量再次上锁,如此一来就可以确保每个时刻只有一个任务正在访问这个临界资源,保证了临界资源操作的安全性。
4、互斥量与递归互斥量
互斥量更适合于可能会引起优先级翻转的情况
递归互斥量更适用于任务可能会多次获取互斥量的情况下,这样可以避免同一任务多次递归持有而造成死锁的问题。
核心在于:谁上锁,就只能由谁开锁。
基本概念
互斥量又称互斥信号量(本质是信号量),是一种特殊的二值信号量,它和信号量不同的是,**它支持互斥量所有权、递归访问以及防止优先级翻转的特性,用于实现对临界资源的独占式处理。**任意时刻互斥量的状态只有两种,开锁或闭锁。当互斥量被任务持有时,该互斥量处于闭锁状态,这个任务获得互斥量的所有权。当该任务释放这个互斥量时,该互斥量处于开锁状态,任务失去该互斥量的所有权。当一个任务持有互斥量时,其他任务将不能再对该互斥量进行开锁或持有。持有该互斥量的任务也能够再次获得这个锁而不被挂起,这就是递归访问,也就是递归互斥量的特性,这个特性与一般的信号量有很大的不同,在信号量中,由于已经不存在可用的信号量,任务递归获取信号量时会发生主动挂起任务最终形成死锁。
如果想要用于实现同步(任务之间或者任务与中断之间),二值信号量或许是更好的选择,虽然互斥量也可以用于任务与任务、任务与中断的同步,但是互斥量更多的是用于保护资源的互锁。
用于互锁的互斥量可以充当保护资源的令牌,当一个任务希望访问某个资源时,它必须先获取令牌。当任务使用完资源后,必须还回令牌,以便其它任务可以访问该资源。是不是很熟悉,在我们的二值信号量里面也是一样的,用于保护临界资源,保证多任务的访问井然有序。当任务获取到信号量的时候才能开始使用被保护的资源,使用完就释放信号量,下一个任务才能获取到信号量从而可用使用被保护的资源。但是**信号量会导致的另一个潜在问题,那就是任务优先级翻转。**而 FreeRTOS 提供的互斥量可以通过优先级继承算法,可以降低优先级翻转问题产生的影响,所以,用于临界资源的保护一般建议使用互斥量。
运作机制
用互斥量处理不同任务对临界资源的同步访问时,任务想要获得互斥量才能进行资源访问,如果一旦有任务成功获得了互斥量,则互斥量立即变为闭锁状态,此时其他任务会因为获取不到互斥量而不能访问这个资源,任务会根据用户自定义的等待时间进行等待,直到互斥量被持有的任务释放后,其他任务才能获取互斥量从而得以访问该临界资源,此时互斥量再次上锁,如此一来就可以确保每个时刻只有一个任务正在访问这个临界资源,保证了临界资源操作的安全性。
二、STM32CubeMX设置
1、配置RCC、USART1、时钟72M
2、配置SYS,将Timebase Source修改为除滴答定时器外的其他定时器。
3、初始化LED的两个引脚
4、开启FreeRTOS,v1与v2版本不同,一般选用v1即可
5、创建两个线程:任务LED1用作发送,LED2用作接收。
以上步骤可参考:STM32CubeMX学习笔记22---FreeRTOS(任务创建和删除)-CSDN博客
6、创建互斥量Mutex
在 Mutexes
进行配置。
- Mutex Name: 互斥量名称
- Allocation: 分配方式:
Dynamic
动态内存创建 - Conrol Block Name: 控制块名称
7、生成代码
三、程序编程
1、创建一个互斥量:osMutexCreate
函数 | osMutexId osMutexCreate (const osMutexDef_t *mutex_def) |
---|---|
参数 | mutex_def: 引用由osMutexDef定义的互斥量 |
返回值 | 成功返回互斥量ID,失败返回0 |
例:
osMutexId myMutex01Handle;
osMutexDef(myMutex01);
myMutex01Handle = osMutexCreate(osMutex(myMutex01));
2、创建一个递归互斥量:osRecursiveMutexCreate
用于创建一个递归互斥量,不是递归的互斥量由函数 osMutexCreate() 创建,且只能被同一个任务获取一次,如果同一个任务想再次获取则会失败。递归信号量则相反,它可以被同一个任务获取很多次,获取多少次就需要释放多少次。递归信号量与互斥量一样,都实现了优先级继承机制,可以减少优先级反转的反生。
函数 | osMutexId osRecursiveMutexCreate (const osMutexDef_t *mutex_def) |
---|---|
参数 | mutex_def: 引用由osMutexDef定义的互斥量 |
返回值 | 成功返回互斥量ID,失败返回0 |
要想使用该函数必须在 Config parameters
中把 USE_RECURSIVE_MUTEXES
选择 Enabled
来使能。
例:
osMutexId myMutex02Handle;
osMutexDef(myMutex02);
myMutex02Handle = osRecursiveMutexCreate(osMutex(myMutex02));
3、删除一个互斥量:osMutexDelete
用于删除一个互斥量。
函数 | osStatus osMutexDelete (osMutexId mutex_id) |
---|---|
参数 | mutex_id: 互斥量ID |
返回值 | 错误码 |
例:
osMutexDelete(myMutex01Handle);
4、获取互斥量:osMutexWait
用于获取互斥量,但是递归互斥量并不能使用这个 API 函数获取。
例:
osMutexWait(myMutex01Handle,osWaitForever);//一直等待获取互斥量
5、获取递归互斥量:osRecursiveMutexWait
用于获取递归互斥量的宏,与互斥量的获取函数一样,osMutexWait()也是一个宏定义,它最终使用现有的队列机制,实际执行的函数是 xQueueTakeMutexRecursive() 。 获取递归互斥量之前必须由 osRecursiveMutexCreate() 这个函数创建。要注意的是该函数不能用于获取由函数 osMutexCreate() 创建的互斥量。
要想使用该函数必须在 Config parameters
中把 USE_RECURSIVE_MUTEXES
选择 Enabled
来使能。
例:
osRecursiveMutexWait(myMutex02Handle,osWaitForever);//一直等待获取互斥量
6、释放互斥量: osMutexRelease
用于释放互斥量,但不能释放由函数 osRecursiveMutexCreate() 创建的递归互斥量。
函数 | osStatus osMutexRelease (osMutexId mutex_id) |
---|---|
参数 | mutex_id: 互斥量ID |
返回值 | 错误码 |
例:
osMutexRelease(myMutex01Handle);
7、释放递归互斥量:osRecursiveMutexRelease
用于释放一个递归互斥量。已经获取递归互斥量的任务可以重复获取该递归互斥量。使用 osRecursiveMutexWait() 函数成功获取几次递归互斥量,就要使用 osRecursiveMutexRelease() 函数返还几次,在此之前递归互斥量都处于无效状态,别的任务就无法获取该递归互斥量。使用该函数接口时,只有已持有互斥量所有权的任务才能释放它,每释放一该递归互斥量,它的计数值就减 1。当该互斥量的计数值为 0 时(即持有任务已经释放所有的持有操作),互斥量则变为开锁状态,等待在该互斥量上的任务将被唤醒。如果任务的优先级被互斥量的优先级翻转机制临时提升,那么当互斥量被释放后,任务的优先级将恢复为原本设定的优先级。
函数 | osStatus osRecursiveMutexRelease (osMutexId mutex_id) |
---|---|
参数 | mutex_id: 互斥量ID |
返回值 | 错误码 |
要想使用该函数必须在 Config parameters
中把 USE_RECURSIVE_MUTEXES
选择 Enabled
来使能。
例:
osRecursiveMutexRelease(myMutex02Handle);
8、互斥量使用
如果两个线程都使用printf进行打印函数,如果同时打印可能导致资源抢夺,此时可以使用互斥量,将两个线程中打印的东西轮流进行打印,此时就不会出现资源抢夺问题。
LED1:
void LED1_Task1(void const * argument)
{
/* USER CODE BEGIN LED1_Task1 */
/* Infinite loop */
osStatus xReturn;
int i=0;
for(;;)
{
HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_5); //LED1状态每500s翻转一次
//获取互斥量 MuxSem,没获取到则一直等待 上锁
xReturn = osMutexWait(myMutex01Handle, /* 互斥量句柄 */
osWaitForever); /* 等待时间 */
if(osOK == xReturn)
{
//获取到互斥量,此时资源上锁,其他任务不得使用该资源(printf)
for(;i<100;)
{
printf("LED1 run %d ",i++);
printf("LED1 run %d ",i++);
printf("LED1 run %d \n",i++);
}
i=0;
}
osMutexRelease(myMutex01Handle);//给出互斥量
osDelay(10);
}
/* USER CODE END LED1_Task1 */
}
LED2:
void LED2_Task03(void const * argument)
{
/* USER CODE BEGIN LED2_Task03 */
/* Infinite loop */
osStatus xReturn = osErrorValue;
int i=0;
for(;;)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_5); //LED1状态每500s翻转一次
printf("LED2 run %d \n",i++);
osDelay(10);
}
/* USER CODE END LED2_Task03 */
}
下载验证:
编译无误后下载到板子上验证,可以看到没有发生资源被占夺的情况
假如将LED的互斥量屏蔽了之后:
void LED1_Task1(void const * argument)
{
/* USER CODE BEGIN LED1_Task1 */
/* Infinite loop */
osStatus xReturn;
int i=0;
for(;;)
{
HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_5); //LED1状态每500s翻转一次
//获取互斥量 MuxSem,没获取到则一直等待 上锁
// xReturn = osMutexWait(myMutex01Handle, /* 互斥量句柄 */
// osWaitForever); /* 等待时间 */
// if(osOK == xReturn)
{
//获取到互斥量,此时资源上锁,其他任务不得使用该资源(printf)
for(;i<100;)
{
printf("LED1 run %d ",i++);
printf("LED1 run %d ",i++);
printf("LED1 run %d \n",i++);
}
i=0;
}
// osMutexRelease(myMutex01Handle);//给出互斥量
osDelay(10);
}
/* USER CODE END LED1_Task1 */
}
可以看到在LED1运行的过程中资源被LED2占夺了,
9、递归互斥量使用
创建递归互斥量:
osMutexId myMutex02Handle;
osMutexDef(myMutex02);
myMutex02Handle = osRecursiveMutexCreate(osMutex(myMutex02));
LED1任务:
void LED1_Task1(void const * argument)
{
/* USER CODE BEGIN LED1_Task1 */
/* Infinite loop */
osStatus xReturn;
int i=0;
for(;;)
{
HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_5); //LED1状态每500s翻转一次
xReturn =osRecursiveMutexWait(myMutex02Handle,osWaitForever);//一直等待获取递归互斥量
printf("LED1 获取递归互斥量 %s \r\n",osOK == xReturn?"成功":"失败");
if(osOK == xReturn)
{
//获取到互斥量,此时资源上锁,其他任务不得使用该资源
for(;i<10;)
{
xReturn =osRecursiveMutexWait(myMutex02Handle,osWaitForever);//一直等待获取递归互斥量
printf("LED1 获取递归互斥量 %s 第%d次\r\n",osOK == xReturn?"成功":"失败",i++);
osMutexRelease(myMutex02Handle);
}
i=0;
}
osMutexRelease(myMutex02Handle);
osDelay(300);
}
/* USER CODE END LED1_Task1 */
}
LED2任务:
void LED2_Task03(void const * argument)
{
/* USER CODE BEGIN LED2_Task03 */
/* Infinite loop */
osStatus xReturn = osErrorValue;
int i=0;
for(;;)
{
xReturn =osRecursiveMutexWait(myMutex02Handle,osWaitForever);//一直等待获取递归互斥量
printf("LED2 获取递归互斥量 %s 第%d次\r\n",osOK == xReturn?"成功":"失败",i++);
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_5); //LED1状态每500s翻转一次
if(osOK == xReturn)
printf("LED2 run %d \n",i++);
osMutexRelease(myMutex02Handle); //释放递归互斥量
osDelay(300);
}
/* USER CODE END LED2_Task03 */
}
下载验证:
可以看到两个任务正常依次运行:
四、常见问题
1、 优先级反转
假设任务A、B都想使用串口,A优先级比较低:
- 任务A获得了串口的互斥量
- 任务B也想使用串口,它将会阻塞、等待A释放互斥量
- 高优先级的任务,被低优先级的任务延迟,这被称为"优先级反转"(priority inversion)
如果涉及3个任务,可以让"优先级反转"的后果更加恶劣。
本节代码为: FreeRTOS_17_mutex_inversion 。
互斥量可以通过"优先级继承",可以很大程度解决"优先级反转"的问题,这也是FreeRTOS中互斥量和二级制信号量的差别。
2、优先级继承
优先级反转的问题在于,LPTask低优先级任务获得了锁,但是它优先级太低而无法运行。
如果能提升LPTask任务的优先级,让它能尽快运行、释放锁,"优先级反转"的问题不就解决了吗?
把LPTask任务的优先级提升到什么水平?
优先级继承:
- 假设持有互斥锁的是任务A,如果更高优先级的任务B也尝试获得这个锁
- 任务B说:你既然持有宝剑,又不给我,那就继承我的愿望吧
- 于是任务A就继承了任务B的优先级
- 这就叫:优先级继承
- 等任务A释放互斥锁时,它就恢复为原来的优先级
互斥锁内部就实现了优先级的提升、恢复。
3、死锁的概念
日常生活的死锁:我们只招有工作经验的人!我没有工作经验怎么办?那你就去找工作啊!
- 假设有2个互斥量M1、M2,2个任务A、B:
- A获得了互斥量M1
- B获得了互斥量M2
- A还要获得互斥量M2才能运行,结果A阻塞
- B还要获得互斥量M1才能运行,结果B阻塞
- A、B都阻塞,再无法释放它们持有的互斥量
- 死锁发生!
3.1、自我死锁
假设这样的场景:
- 任务A获得了互斥锁M
- 它调用一个库函数
- 库函数要去获取同一个互斥锁M,于是它阻塞:任务A休眠,等待任务A来释放互斥锁!
- 死锁发生!
怎么解决这类问题?可以使用递归锁(Recursive Mutexes),它的特性如下:
任务A获得递归锁M后,它还可以多次去获得这个锁
"take"了N次,要"give"N次,这个锁才会被释放
五、参考文献
韦东山freeRTOS系列教程之【第七章】互斥量(mutex)_freertos的互斥量获取返回失败,实际获取成功-CSDN博客
STM32CubeMX学习笔记(31)——FreeRTOS实时操作系统使用(互斥量)_学习cube freertc 互锁-CSDN博客