目录
3.1 互斥量创建函数xSemaphoreCreateMutex()
一. 前言
大家好,我是旭辉君,一个智能硬件领域深度探索的技术博主。
在上篇文章中,我们探索了二值信号量与计数信号量的原理与使用,链接如下:
FreeRTOS系列教程(四):如何使用信号量-CSDN博客文章浏览阅读523次,点赞11次,收藏19次。本文主要探索了二值信号量与计数信号量的原理及其使用方法,包括信号量的原理,信号量的运行机制,信号量与消息队列的比较,信号量的相关API函数,以及信号量的使用实验等。通过本文,我们将会知道:如何创建和使用二值信号量?如何创建和使用计数信号量?信号量与消息队列有什么异同?为什么要使用信号量?信号量的运行机制是什么?https://blog.csdn.net/weixin_42434952/article/details/138291510?spm=1001.2014.3001.5502接下来,我们将继续探索互斥量。所谓互斥量,其本质是一种特殊的,具有优先级继承机制的二值信号量。那什么又是优先级继承机制呢,优先级继承机制是指:当一个互斥信号量正在被一个低优先级的任务持有时,如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞,然后高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级。
大家可能会有疑惑:
- 为什么要使用互斥信号量?
- 什么情况下使用互斥信号量?
- 为什么要有优先级继承机制?
- 互斥信号量怎么使用?
带着这些问题,我们进入互斥信号量之旅!
二. 互斥信号量运行机制
2.1 互斥量的应用场景
在某些场景下,我们想要让一些限定的资源在某一时刻只能被一个任务访问,那如何保护资源不被多个任务同时操作呢?
比如有两个任务需要对串口进行发送数据,其硬件资源只有一个串口,那么这两个任务肯定不能同时发送,不然会导致数据错误。这时候,我们就可以用互斥量对串口资源进行保护,当一个任务正在使用串口的时候,另一个任务则无法使用串口,等到该任务使用串口完毕之后,释放互斥量,另外一个任务才能获得串口的使用权。
其实,使用上文我们介绍的二值信号量,也可以用来保护临界资源,保证其不会被多任务同时访问,但二值信号量在访问临界资源时存在其固有缺陷,会引起优先级翻转现象,这个我们接下来会详细介绍。
2.2 互斥信号量与二值信号量的区别
互斥信号量:主要用于管理对共享资源的访问,确保在任何给定时间内只有一个任务可以访问该资源。特别适用于实现任务间的互斥,即防止多个任务同时访问同一资源。
其特点为:
- 所有权:互斥信号量有所有权的概念,即只有获取互斥信号量的任务才能释放它。这有助于避免其他任务意外释放不属于自己的锁。互斥量用于保护资源时必须要被返还。
- 优先级继承:为了解决优先级翻转问题,互斥信号量通常实现了优先级继承或优先级天花板协议。这意味着如果低优先级任务持有互斥信号量而高优先级任务正在等待该信号量,低优先级任务的优先级会暂时提升到高优先级任务的优先级,直到互斥信号量被释放。
- 使用场景:当需要保护共享资源,确保在任何时候只有一个任务可以访问资源,并且关心优先级反转问题时,应选择互斥信号量。
二值信号量:它的值只能是0或1。主要用于任务间的同步。
其特点为:
- 没有所有权:二值信号量没有所有权的概念,任何任务都可以释放二值信号量,无论是哪个任务获取了它。信号量用于数据同步时不需要返还。
- 没有优先级继承:二值信号量通常不提供优先级继承的功能,因此可能会遇到优先级反转的问题,除非操作系统特别提供了解决方案。
- 使用场景:当需要简单的任务间或中断到任务的同步,或通知任务某事件已发生,且同步时间很短时,应选择二值信号量。
举个例子,假设有一个共享的硬件资源,如串口,需要多个任务按顺序访问,可以使用互斥信号量来确保在修改发送内容时不会产生冲突。而对于任务间需要同步完成信号的情况,二值信号量是一个更合适的选择。
2.3 优先级继承机制
首先给大家讲述一下,什么是优先级翻转现象。
我们知道任务的优先级在创建的时候就已经是设置好的,高优先级的任务可以打断低优先级的任务,抢占 CPU 的使用权。但是在很多场合中,某些资源只有一个,当低优先级任务正在占用该资源的时候,即便高优先级任务,由于获取不到信号量,也只能乖乖的等待低优先级任务使用完该资源后释放资源。这里高优先级任务无法运行而低优先级任务可以运行的现象称为“优先级翻转”。
举个例子,如下图所示,现在有 3 个任务分别为 H 任务(High)、 M 任务(Middle)、 L 任务(Low), 3 个任务的优先级顺序为 H 任务>M 任务>L 任务。正常运行的时候 H 任务可以打断 M 任务与 L 任务, M 任务可以打断 L 任务,假设系统中有一个资源被保护了,此时该资源被 L 任务正在使用中,某一刻, H 任务需要使用该资源,但是 L 任务还没使用完, H任务则因为申请不到资源而进入阻塞态, L 任务继续使用该资源,此时已经出现了“优先级翻转”现象,高优先级任务在等着低优先级的任务执行,如果在 L 任务执行的时候刚好M 任务被唤醒了,由于 M 任务优先级比 L 任务优先级高,那么会打断 L 任务,抢占了CPU 的使用权,直到 M 任务执行完,再把 CUP 使用权归还给 L 任务, L 任务继续执行,等到执行完毕之后释放该资源, H 任务此时才从阻塞态解除,使用该资源。这个过程,本来是最高优先级的 H 任务,在等待了更低优先级的 L 任务与 M 任务,其阻塞的时间是 M任务运行时间+L 任务运行时间,这只是只有 3 个任务的系统,假如很多个这样子的任务打断最低优先级的任务,那这个系统最高优先级任务岂不是崩溃了,对系统运行会产生严重的影响!
那怎么解决这种不良的影响呢?这儿就体现出了优先级继承机制的厉害之处。
同样的情况,如下图所示,在优先级继承机制的影响下,当 H 任务申请该资源的时候,由于申请不到资源会进入阻塞态,那么系统就会把当前正在使用资源的 L 任务的优先级临时提高到与 H 任务优先级相同,此时 M 任务被唤醒了,因为它的优先级比 H 任务低,所以无法打断 L 任务,因为此时 L 任务的优先级被临时提升到 H,所以当 L 任务使用完该资源了,进行释放,那么此时 H 任务优先级最高,将接着抢占 CPU 的使用权, H 任务的阻塞时间仅仅是 L 任务的执行时间,这样就把优先级翻转的危害降到了最低。
需要注意的是,优先级继承并不能完全消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响。同时,互斥信号量不能用于中断服务函数中,只能用于任务中,因为其特有的优先级继承机制只在任务中起作用。
三. 互斥信号量相关API函数
3.1 互斥量创建函数xSemaphoreCreateMutex()
SemaphoreHandle_t xSemaphoreCreateMutex( void )
其中,
返回值:如果已成功创建,则返回创建的互斥量的句柄。 如果创建失败, 则返回 NULL。
同创建二值信号量一样,其实互斥量的创建也是调用 xQueueGenericCreate()函数,只是传递的参数不同。创建成功后,互斥量结构体成员在内存中的排布如下:
其他互斥量的使用函数,我们在上节信号量里已经有讲解,在互斥量的使用上同样适用:
互斥量删除函数 vSemaphoreDelete():删除互斥量。
互斥量获取函数 xSemaphoreTake():任务对互斥量的所有权是独占的,任意时刻互斥量只能被一个任务持有,如果互斥量处于开锁状态,那么获取该互斥量的任务将成功获得该互斥量,并拥有互斥量的使用权;如果互斥量处于闭锁状态,获取该互斥量的任务将无法获得互斥量, 任务将被挂起,与二值信号量不同的是,在任务被挂起之前,会进行优先级继承,如果当前任务优先级比持有互斥量的任务优先级高,那么将会临时提升持有互斥量任务的优先级。
互斥量释放函数 xSemaphoreGive():只有已持有互斥量所有权的任务才能释放它, 当任务调用xSemaphoreGive()函数时会将互斥量变为开锁状态,等待获取该互斥量的任务将从阻塞态转为就绪。如果任务的优先级被互斥量的优先级翻转机制临时提升,那么当互斥量被释放后, 任务的优先级将恢复为原本设定的优先级。
需要注意的是,任务获取互斥量成功后,在使用完毕需要立即释放,否则很容易造成其他任务无法获取互斥量。
四. 互斥信号量相关实验
4.1 模拟优先级翻转实验
创建了三个任务与一个二值信号量, 任务分别是高优先级任务,中优先级任务,低优先级任务, 用于模拟产生优先级翻转。其中task_example_3为低优先级,task_example_2为中优先级,task_example_1为最高优先级。
二值信号量创建后,需要先释放一个二值信号量,然后再高优先级任务和低优先级任务中都不断获取与释放信号量,不同的是,低优先级任务再获取到信号量以后,还要进行漫长的其他任务处理才会释放信号量。主体代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "freertos/semphr.h"
TaskHandle_t task_example_1_handle = NULL;
TaskHandle_t task_example_2_handle = NULL;
TaskHandle_t task_example_3_handle = NULL;
QueueHandle_t Test_Queue =NULL;
SemaphoreHandle_t BinarySem_Handle = NULL;
TickType_t xWakeTime_task1;
TickType_t xWakeTime_task2;
TickType_t xWakeTime_task3;
static void task_example_1(void* arg)
{
BaseType_t xReturn = pdTRUE;
while (1)
{
xReturn = xSemaphoreTake(BinarySem_Handle,portMAX_DELAY);
xWakeTime_task1 = xTaskGetTickCount();
if(pdTRUE == xReturn) printf("high_task计数信号量获取成功at tick: %d\r\n",xWakeTime_task1);
else printf("high_task计数信号量获取失败!\r\n");
vTaskDelay(10 / portTICK_RATE_MS);
xReturn = xSemaphoreGive(BinarySem_Handle);
xWakeTime_task1 = xTaskGetTickCount();
if( xReturn == pdTRUE ) printf("high_task计数信号量释放成功at tick: %d\r\n",xWakeTime_task1);
else printf("high_task计数信号量释放失败!\r\n");
vTaskDelay(1000 / portTICK_RATE_MS);
}
}
static void task_example_2(void* arg)
{
// BaseType_t xReturn = pdPASS; /* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
xWakeTime_task2 = xTaskGetTickCount();
printf("middle_task is Running at tick:%d\r\n",xWakeTime_task2);
vTaskDelay(300 / portTICK_RATE_MS);
}
}
static void task_example_3(void* arg)
{
BaseType_t xReturn = pdPASS;
static uint32_t i;
while (1)
{
xReturn = xSemaphoreTake(BinarySem_Handle,portMAX_DELAY);
xWakeTime_task3 = xTaskGetTickCount();
if(pdTRUE == xReturn) printf("low_task计数信号量获取成功 at tick:%d\r\n",xWakeTime_task3);
else printf("low_task计数信号量获取失败!\r\n");
//模拟低优先级任务占用互斥量
for (i = 0; i < 50000000; i++) {
}
xReturn = xSemaphoreGive(BinarySem_Handle);
xWakeTime_task3 = xTaskGetTickCount();
if( xReturn == pdTRUE ) printf("low_task计数信号量释放成功at tick:%d\r\n",xWakeTime_task3);
else printf("low_task计数信号量释放失败!\r\n");
vTaskDelay(1000 / portTICK_RATE_MS);
}
}
void app_main(void)
{
BaseType_t xReturn;
/* 创建 BinarySem */
BinarySem_Handle = xSemaphoreCreateBinary();
if(NULL != BinarySem_Handle)
printf("BinarySem_Handle二值信号量创建成功!\r\n");
xSemaphoreGive( BinarySem_Handle );//给出二值信号量
//start task
xReturn = xTaskCreate(task_example_1, "task_example_1", 2048, NULL, 3, &task_example_1_handle);
if(xReturn == pdPASS) printf("创建 task_example_1 任务成功!\r\n");
else printf("创建 task_example_1 任务失败!\r\n");
xReturn = xTaskCreate(task_example_2, "task_example_2", 2048, NULL, 2, &task_example_2_handle);
if(xReturn == pdPASS) printf("创建 task_example_2 任务成功!\r\n");
else printf("创建 task_example_2 任务失败!\r\n");
xReturn = xTaskCreate(task_example_3, "task_example_3", 2048, NULL, 1, &task_example_3_handle);
if(xReturn == pdPASS) printf("创建 task_example_3 任务成功!\r\n");
else printf("创建 task_example_3 任务失败!\r\n");
printf("Minimum free heapconfigMINIMAL_STACK_SIZE size: %d bytes\n", esp_get_minimum_free_heap_size());
}
从下图的串口运行输出,我们可以看到:在低优先级任务获取到信号量以后,中优先级任务持续运行,因为这时候因为低优先级还未释放信号量,那么高优先级任务就无法取得信号量继续运行,此时就发生了优先级翻转,直到低优先级任务释放信号量,高优先级任务才得以运行。
4.2 互斥信号量实验
创建互斥量,把xSemaphoreCreateBinary()改为xSemaphoreCreateMutex(),其他不变,观察实验现象:在中等任务进行到tick91后,由于把优先级翻,把低优先级任务设置到了和高优先级一样的优先级,所以在低优先级中不断执行完for循环后,执行完成后到了tick316。
五. 小结
本文主要探索了互斥信号量的原理及其使用方法,包括互斥信号量运行机制,互斥信号量的应用场景,互斥信号量的优先级继承机制,互斥信号量的函数使用,以及信号量的使用实验等。通过本文,不知道大家对第一节的几个问题,有没有自己的答案。如果自己的答案不太清晰,或者对文中某些地方不太明白的同学,欢迎留言交流。原创不易,大家的点赞和关注是对我持续更新最大的鼓励,谢谢!也为坚持看到系列文章此处的同学点赞!
想要完整源码工程的同学,可以扫码,或者微信搜索:硬件电子与嵌入式小栈 ,关注微信公众号,留言即可获取,公众号里面也会有丰富的干货文章哦。