FreeRTOS教程6 互斥量
1、准备材料
STM32CubeMX软件(Version 6.10.0)
Keil µVision5 IDE(MDK-Arm)
2、学习目标
本文主要学习 FreeRTOS 互斥量的相关知识,包括优先级翻转问题、优先级继承、死锁现象、创建/删除互斥量 和 获取/释放互斥量等知识
3、前提知识
3.1、优先级翻转问题
使用二值信号量用于进程间同步时可能会出现优先级翻转的问题,什么是“优先级翻转”问题呢?考虑如下所述的任务运行过程
- 在 t1 时刻,低优先级的任务 TaskLP 切入运行状态,并且获取到了一个二值信号量 Binary Semaphores
- 在 t2 时刻,高优先级的任务 TaskHP 请求获取二值信号量 Binary Semaphores ,但是由于 TaskLP 还未释放该二值信号量,所以在 t3 时刻,任务 TaskHP 进入阻塞状态等待二值信号量被释放
- 在 t4 时刻,中等优先级的任务 TaskMP 进入就绪状态,由于不需要获取二值信号量,因此抢占低优先级任务任务 TaskLP 切入运行状态
- 在 t5 时刻,任务 TaskMP 运行结束,任务 TaskLP 再次切入运行状态
- 在 t6 时刻,任务 TaskLP 运行结束,释放二值信号量 Binary Semaphores,此时任务 TaskHP 从等待二值信号量的阻塞状态切入运行状态
- 在t7时刻,任务 TaskHP 运行结束
根据上述流程读者可以发现一个问题,即在 t4 时刻中等优先级的任务 TaskMP 先于高优先级的任务 TaskHP 抢占了处理器,这破坏了 FreeRTOS 基于优先级抢占式执行的原则,我们将这种情况称为优先级翻转问题,上述描述的任务运行过程具体时刻流程图如下图所示
优先级翻转可能是一个严重的问题,但在小型嵌入式系统中,通常可以在系统设计时通过考虑如何访问资源来避免该问题
3.2、优先级继承
为了解决使用二值信号量可能会出现的优先级翻转问题,对二值信号量做了改进,增加了一种名为 “优先级继承” 的机制,改进后的实例称为了互斥量,注意虽然互斥量可以减缓优先级翻转问题的出现,但是并不能完全杜绝
接下来我们来通过例子介绍什么是优先级继承?
仍然考虑由 “3.1、优先级翻转问题” 小节中提出的任务运行过程的例子,具体流程如下所述,读者可以细心理解其中的不同之处
- 在 t1 时刻,低优先级的任务 TaskLP 切入运行状态,并且获取到了一个互斥量 Mutexes
- 在 t2 时刻,高优先级的任务 TaskHP 请求获取互斥量 Mutexes ,但是由于 TaskLP 还未释放该互斥量,所以在 t3 时刻,任务 TaskHP 进入阻塞状态等待互斥量被释放,但是与二值信号量不同的是,此时 FreeRTOS 将任务 TaskLP 的优先级临时提高到与任务 TaskHP 一致的优先级,也即高优先级
- 在 t4 时刻,中等优先级的任务 TaskMP 进入就绪状态发生任务调度,但是由于任务 TaskLP 此时优先级被提高到了高优先级,因此任务 TaskMP 仍然保持就绪状态等待优先级较高的任务执行完毕
- 在 t5 时刻,任务 TaskLP 执行完毕释放互斥量 Mutexes,此时任务 TaskHP 抢占处理器切入运行状态,并恢复任务 TaskLP 原来的优先级
- 在 t6 时刻,任务 TaskHP 执行完毕,此时轮到任务 TaskMP 执行
- 在 t7 时刻,任务 TaskMP 运行结束
根据互斥量的上述任务流程读者可以发现与二值信号量的不同之处,上述描述的任务运行过程具体时刻流程图如下图所示
3.3、什么是互斥量?
互斥量/互斥锁是一种特殊类型的二进制信号量,用于控制对在两个或多个任务之间共享资源的访问
互斥锁可以被视为一个与正在共享的资源相关联的令牌,对于合法访问资源的任务,它必须首先成功 “获取” 令牌,成为资源的持有者,当持有者完成对资源的访问之后,其需要 ”归还” 令牌,只有 “归还” 令牌之后,该令牌才可以再次被其他任务所 “获取” ,这样保证了互斥的对共享资源的访问,上述机制如下图所示 (注释1)
3.4、死锁现象
“死锁” 是使用互斥锁进行互斥的另一个潜在陷阱,当两个任务因为都在等待对方占用的资源而无法继续进行时,就会发生死锁,考虑如下所述的情况
- 任务 A 执行并成功获取互斥量 X
- 任务 A 被任务 B 抢占
- 任务 B 在尝试获取互斥量 X 之前成功获取互斥量 Y,但互斥量 X 由任务 A 持有,因此对任务 B 不可用,任务 B 选择进入阻塞状态等待互斥量 X 被释放
- 任务 A 继续执行,它尝试获取互斥量 Y,但互斥量 Y 由任务 B 持有,所以对于任务 A 来说是不可用的,任务 A 选择进入阻塞状态等待待释放的互斥量 Y
经过上述的这样一个过程,读者可以发现任务 A 在等待任务 B 释放互斥量 Y ,而任务 B 在等待任务 A 释放互斥量 X ,两个任务都在阻塞状态无法执行,从而导致 ”死锁“ 现象的发生,与优先级翻转一样,避免 “死锁” 的最佳方法是在设计时考虑其潜在影响,并设计系统以确保不会发生死锁
3.5、什么是递归互斥量?
任务也有可能与自身发生死锁,如果任务尝试多次获取相同的互斥体而不首先返回互斥体,就会发生这种情况,考虑以下设想:
- 任务成功获取互斥锁
- 在持有互斥体的同时,任务调用库函数
- 库函数的实现尝试获取相同的互斥锁,并进入阻塞状态等待互斥锁变得可用
在此场景结束时,任务处于阻塞状态以等待互斥体返回,但任务已经是互斥体持有者。 由于任务处于阻塞状态等待自身,因此发生了死锁
通过使用递归互斥体代替标准互斥体可以避免这种类型的死锁,同一任务可以多次 “获取” 递归互斥锁,并且只有在每次 “获取” 递归互斥锁之后都调用一次 “释放” 递归互斥锁,才会返回该互斥锁
因此递归互斥量可以视为特殊的互斥量,一个互斥量被一个任务获取之后就不能再次获取,其他任务想要获取该互斥量必须等待这个任务释放该互斥连,但是递归互斥量可以被一个任务重复获取多次,当然每次获取必须与一次释放配对使用
注意不管是互斥量,还是递归互斥量均存在优先级继承机制,但是由于 ISR 并不是任务,因此互斥量和递归互斥量不能在中断中使用
3.5、创建互斥量
互斥量在使用之前必须先创建,因为互斥量分为互斥量和递归互斥量两种,所以 FreeRTOS 也提供了不同的 API 函数,具体如下所述
/**
* @brief 动态分配内存创建互斥信号量函数
* @retval 创建互斥信号量的句柄
*/
SemaphoreHandle_t xSemaphoreCreateMutex(void);
/**
* @brief 静态分配内存创建互斥信号量函数
* @param pxMutexBuffer:指向StaticSemaphore_t类型的变量,该变量将用于保存互斥锁型信号量的状态
* @retval 返回成功创建后的互斥锁的句柄,如果返回NULL则表示内存不足创建失败
*/
SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t *pxMutexBuffer);
/**
* @brief 动态分配内存创建递归互斥信号量函数
* @retval 创建递归互斥信号量的句柄,如果返回NULL则表示内存不足创建失败
*/
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);
/**
* @brief 动态分配内存创建二值信号量函数
* @param pxMutexBuffer:指向StaticSemaphore_t类型的变量,该变量将用于保存互斥锁型信号量的状态
*/
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(
StaticSemaphore_t pxMutexBuffer);
3.6、获取互斥量
获取互斥量直接使用获取信号量的函数即可,但对于递归互斥量需要专门的获取函数,具体如下所述
/**
* @brief 获取信号量函数
* @param xSemaphore:正在获取的信号量的句柄
* @param xTicksToWait:等待信号量变为可用的时间
* @retval 成功获取信号量则返回pdTRUE, xTicksToWait过期,信号量不可用,则返回pdFALSE
*/
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
/**
* @brief 获取递归互斥量
* @param xMutex:正在获得的互斥锁的句柄
* @param xTicksToWait:等待信号量变为可用的时间
* @retval 成功获取信号量则返回pdTRUE, xTicksToWait过期,信号量不可用,则返回pdFALSE
*/
BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex,
TickType_t xTicksToWait);
3.7、释放互斥量
释放互斥量直接使用释放信号量的函数即可,但对于递归互斥量需要专门的释放函数,具体如下所述
/**
* @brief 释放信号量函数
* @param xSemaphore:要释放的信号量的句柄
* @retval 成功释放信号量则返回pdTRUE, 若发生错误,则返回pdFALSE
*/
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
/**
* @brief 释放递归互斥量
* @param xMutex:正在释放或“给出”的互斥锁的句柄
* @retval 成功释放递归互斥量后返回pdTRUE
*/
BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t xMutex);
3.8、删除互斥量
直接使用信号量的删除函数即可,具体如下所述
/**
* @brief 获取信号量函数
* @param xSemaphore:要删除的信号量的句柄
* @retval None
*/
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);
4、实验一:优先级翻转问题
4.1、实验目标
既然实验是讨论优先级翻转问题,那么我们来复现 “3.1、优先级翻转问题” 小节中所描述到的任务运行过程,具体如下所述
- 创建一个二值信号量 BinarySem_PI,用于演示优先级翻转问题
- 创建一个低优先级任务 Task_Low ,在该任务中获取二值信号量 BinarySem_PI ,并通过延时模拟长时间连续运行,运行结束后释放该二值信号量,整个过程会通过串口输出提示信息
- 创建一个中等优先级任务 Task_Middle,该任务负责在 Task_Low 模拟长时间连续运行期间抢占其处理器控制权限
- 创建一个高优先级任务 Task_High,该任务总是尝试获取二值信号量 BinarySem_PI
4.2、CubeMX相关配置
首先读者应按照"FreeRTOS教程1 基础知识"章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求
本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信”,如下图所示
单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Tasks and Queues 选项卡双击默认任务修改其参数,然后增加另外两个不同优先级的任务,具体如下图所示
然后在 Configuration 中单击 Timers and Semaphores ,在 Binary Semaphores 中单击 Add 按钮新增加一个名为 BinarySem_PI 的二值信号量,具体如下图所示
配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可
4.3、添加其他必要代码
按照 “STM32CubeMX教程9 USART/UART 异步通信” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述
首先应该在 freertos.c 中添加信号量相关 API 和 printf() 函数的头文件,如下所述
/*freertos.c中添加头文件*/
#include "semphr.h"
#include "stdio.h"
然后在该文件中实现三个不同优先级的任务,主要是一些串口输出给用户的提示信息,方便演示实验目的,具体如下所述
/*低优先级任务*/
void AppTask_Low(void *argument)
{
/* USER CODE BEGIN AppTask_Low */
/* Infinite loop */
uint8_t str1[]="Task_Low take it\r\n";
uint8_t str2[]="Task_Low give it\r\n";
uint8_t str3[]="return Task_Low\r\n";
for(;;)
{
//获取信号量
if(xSemaphoreTake(BinarySem_PIHandle, pdMS_TO_TICKS(200))==pdTRUE)
{
printf("%s",str1);
//模拟任务连续运行
HAL_Delay(500);
printf("%s",str3);
HAL_Delay(500);
printf("%s",str2);
//释放信号量
xSemaphoreGive(BinarySem_PIHandle);
}
}
/* USER CODE END AppTask_Low */
}
/*中等优先级任务*/
void AppTask_Middle(void *argument)
{
/* USER CODE BEGIN AppTask_Middle */
/* Infinite loop */
uint8_t strMid[]="Task_Middle is running\r\n";
for(;;)
{
printf("%s", strMid);
vTaskDelay(500);
}
/* USER CODE END AppTask_Middle */
}
/*高优先级任务*/
void AppTask_High(void *argument)
{
/* USER CODE BEGIN AppTask_High */
/* Infinite loop */
uint8_t strHigh1[]="Into Task_High\r\n";
uint8_t strHigh2[]="Task_High get token\r\n";
uint8_t strHigh3[]="Task_High give token\r\n";
for(;;)
{
printf("%s",strHigh1);
//获取信号量
if(xSemaphoreTake(BinarySem_PIHandle, portMAX_DELAY)==pdTRUE)
{
printf("%s",strHigh2);
printf("%s",strHigh3);
//释放信号量
xSemaphoreGive(BinarySem_PIHandle);
}
vTaskDelay(500);
}
/* USER CODE END AppTask_High */
}
在 "FreeRTOS教程5 信号量" 文章 ”3.2、创建信号量“ 小节中曾提到,信号量被创建完之后是无效的,但是这里我们需要让刚创建的二值信号量有效,否则 Task_High 和 Task_Low 都将无法获取二值信号量,因此最后修改二值信号量的初始值为 1 即可,具体如下所示
/*将初始值0修改为1*/
BinarySem_PIHandle = osSemaphoreNew(1, 1, &BinarySem_PI_attributes);
4.4、烧录验证
烧录程序,打开串口助手,按住开发板复位按键,目的是为了让串口助手接收程序从最开始输出的信息,这里我们只分析第一轮,因为延时、语句执行等微小的时间差异会导致第二轮任务进入阻塞和退出阻塞的时间与第一轮有差异,如下所述为第一轮详细的任务执行流程
- 当创建完三个不同优先级的任务后不会立即得到执行,而是进入就绪状态等待调度器的启动
- 当调度器启动之后会按照优先级从最高优先级开始执行,因此串口输出 “Into Task_High” 表示进入高优先级任务,然后在高优先级任务 Task_High 中获得二值信号量,然后立马释放二值信号量,最后进入 500ms 的阻塞状态
- 当高优先级任务进入阻塞状态后,接下来会执行就绪状态的中等优先级任务 Task_Middle ,该任务无具体功能,仅仅通过串口输出 “Task_Middle is running”,然后同样进入 500ms 的阻塞状态
- 由于高优先级和中等优先级任务都进入阻塞状态,这时才轮到低优先级任务 Task_Low 执行,低优先级任务 Task_Low 成功获取到二值信号量并通过串口输出 “Task_Low take it” ,然后利用 500ms 的 HAL 库延时函数模拟连续运行
- 在 Task_Low 连续运行期间,在其即将执行完第一个 HAL_Delay(500); 时,高优先级任务 Task_High 从 500ms 的阻塞状态恢复,然后尝试获取已经被 Task_Low 获取的二值信号量,结果就是进入阻塞状态等待 Task_Low 释放二值信号量
- 紧接着 Task_Middle 从 500ms 的阻塞状态恢复,通过串口输出 “Task_Middle is running”,接着再次进入 500ms 阻塞状态
- 由于高优先级和中等优先级任务再次进入阻塞状态,因此调度器返回 Task_Low 被抢占时的程序处继续执行,因此 Task_Low 通过串口输出 “return Task_Low” ,然后利用第二个 HAL_Delay(500); 继续模拟长时间运行
- 在 Task_Low 第二个 HAL_Delay(500); 即将执行完毕时,Task_Middle 再次从 500ms 的阻塞状态恢复,通过串口输出 “Task_Middle is running” ,然后再次进入 500ms 阻塞状态(这里 Task_High 由于不是因为延时进入的阻塞状态所以未恢复运行状态)
- 最后返回 Task_Low 任务,释放二值信号量,一旦 Task_Low 任务释放二值信号量,等待二值信号量的高优先级任务 Task_High 会立马退出阻塞状态成功获取到二值信号量,并会通过串口输出 “Task_High get token“
从上述过程可知,从 Task_Low 获取二值信号量之后到第一轮结束,Task_High 等待 Task_Low 释放二值信号量,等待期间中等优先级的任务 Task_Middle 却先于高优先级任务 Task_High 得到了执行,这就是所谓的优先级翻转问题,上述过程所述的实际串口输出如下图所示
4.5、互斥量的应用
首先在 STM32CubeMX 中单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Mutexes 选项卡,单击 Add 按钮增加互斥量 Mutex_PI ,具体如下图所示
然后将上述实验使用的所有二值信号量句柄 BinarySem_PIHandle 修改为互斥量 Mutex_PIHandle,不需要做其他任何操作,烧录程序即可
打开串口助手,观察串口助手的输出,如下所述为第一轮详细的任务执行流程
- 前4个步骤与 ”4.4、烧录验证“ 小节一致,只不过从二值信号量修改为互斥量
- 在第5步时,高优先级任务 Task_High 从 500ms 的阻塞状态恢复,输出 ”Into Task_High“ ,然后尝试获取已经被 Task_Low 获取的互斥量,结果就是进入阻塞状态等待 Task_Low 释放互斥量,同时将 Task_Low 的优先级临时提高到和高优先级任务 Task_High 一样的优先级
- 紧接着 Task_Middle 从 500ms 的阻塞状态恢复,但是由于现在 Task_Low 任务的优先级要高于中等优先级任务 Task_Middle ,因此不能抢占 Task_Low 任务,故无法执行任务体输出 ”Task_Middle is running“ ,所以其状态变为就绪状态,它将等待所有高优先级的任务执行完后才会执行
- 于是优先级被临时提高到高优先级的任务 Task_Low 继续执行其函数体内容,输出 ”return Task_Low“ ,然后执行第二个 HAL_Delay(500); ,最后释放互斥量,通过串口输出 ”Task_Low give it“
- 一旦互斥量被 Task_Low 释放,处于阻塞状态的 Task_High 就会立马恢复运行状态获取到互斥量,所以会通过串口输出 ”Task_High get token“ 和 ”Task_High give token“ ,同时当互斥量被 Task_High 任务成功获取之后,会将任务 Task_Low 临时提高的优先级恢复到其原来的低优先级,最后 Task_High 调用延时函数进入 500ms 的阻塞状态
- 当高优先级任务 Task_High 进入阻塞状态后,系统内现在剩余就绪状态的中等优先级任务 Task_Middle 和 低优先级任务 Task_Low ,所以轮到 Task_Middle 任务执行,其将通过串口输出 ”Task_Middle is runing“ ,至此一轮结束
读者可以自行对比将二值信号量更换为互斥量之后的串口输出结果,可以发现在步骤4中,中等优先级的任务 Task_Middle 不再先于高优先级的任务 Task_High 得到执行,上述整个过程串口数据的完整输出如下图所示
5、注释详解
注释1:图片来源于 Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf
参考资料
Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf