FreeRTOS — 互斥信号量**

时间:2021-07-18 15:11:46

1 、互 斥 信 号 量

1.1 互斥信号量的概念及其作用

互斥信号量的主要作用是对资源实现互斥访问,使用二值信号量也可以实现互斥访问的功能,不过互斥信号量与二值信号量有区别。下面我们先举一个通过二值信号量实现资源独享,即互斥访问的例子,让大家有一个形象的认识,进而引出要讲解的互斥信号量。

运行条件:  

 让两个任务 Task1 和 Task2 都运行串口打印函数 printf,这里我们就通过二值信号量实现对函数printf 的互斥访问。如果不对函数 printf 进行互斥访问,串口打印容易出现乱码。
 用计数信号量实现二值信号量只需将计数信号量的初始值设置为 1 即可。

代码实现:

   创建二值信号量

static SemaphoreHandle_t xSemaphore = NULL;
/*
*********************************************************************************************************
* 函 数 名: AppObjCreate
* 功能说明: 创建任务通信机制
* 形 参: 无
* 返 回 值: 无
*********************************************************************************************************
*/
static void AppObjCreate (void)
{
/* 创建二值信号量,首次创建信号量计数值是 0 */
xSemaphore
= xSemaphoreCreateBinary();
if(xSemaphore == NULL)
{
/* 没有创建成功,用户可以在这里加入创建失败的处理机制 */
}
/* 先释放一次,将初始值改为 1,利用二值信号量实现互斥功能 */
xSemaphoreGive(xSemaphore);
}

 通过二值信号量实现对 printf 函数互斥访问的两个任务

/*
*********************************************************************************************************
* 函 数 名: vTaskLED
* 功能说明: 实现对串口的互斥访问
* 形 参: pvParameters 是在创建该任务时传递的形参
* 返 回 值: 无
* 优 先 级: 2
*********************************************************************************************************
*/
static void vTaskLED(void *pvParameters)
{
TickType_t xLastWakeTime;
const TickType_t xFrequency = 300;
/* 获取当前的系统时间 */
xLastWakeTime
= xTaskGetTickCount();
while(1)
{
/* 通过二值信号量实现资源互斥访问,永久等待直到资源可用 */
xSemaphoreTake(xSemaphore, portMAX_DELAY);
printf(
"任务 vTaskLED 在运行\r\n");
bsp_LedToggle(
1);
bsp_LedToggle(
4);
xSemaphoreGive(xSemaphore);
/* vTaskDelayUntil 是绝对延迟,vTaskDelay 是相对延迟。*/
vTaskDelayUntil(
&xLastWakeTime, xFrequency);
}
}
/*
*********************************************************************************************************
* 函 数 名: vTaskMsgPro
* 功能说明: 实现对串口的互斥访问
* 形 参: pvParameters 是在创建该任务时传递的形参
* 返 回 值: 无
* 优 先 级: 3
*********************************************************************************************************
*/
static void vTaskMsgPro(void *pvParameters)
{
  TickType_t xLastWakeTime;
  
const TickType_t xFrequency = 300;
  
/* 获取当前的系统时间 */
  xLastWakeTime
= xTaskGetTickCount();
  
while(1)
  {
  
/* 通过二值信号量实现资源互斥访问,永久等待直到资源可用 */
  xSemaphoreTake(xSemaphore, portMAX_DELAY);
  printf(
"任务 vTaskMsgPro 在运行\r\n");
  bsp_LedToggle(
2);
  bsp_LedToggle(
3);
  xSemaphoreGive(xSemaphore);
  
/* vTaskDelayUntil 是绝对延迟,vTaskDelay 是相对延迟。*/
  vTaskDelayUntil(
&xLastWakeTime, xFrequency);
  }
}

   有了上面二值信号量的认识之后,互斥信号量与二值信号量又有什么区别呢?互斥信号量可以防止优先级翻转,而二值信号量不支持,下面我们就讲解一下优先级翻转问题。

1.2 优先级翻转问题

  下面我们通过如下的框图来说明一下优先级翻转的问题,让大家有一个形象的认识。

FreeRTOS — 互斥信号量**

运行条件: 

 创建 3 个任务 Task1,Task2 和 Task3,优先级分别为 3,2,1。也就是 Task1 的优先级最高。
 任务 Task1 和 Task3 互斥访问串口打印 printf,采用二值信号实现互斥访问。
 起初 Task3 通过二值信号量正在调用 printf,被任务 Task1 抢占,开始执行任务 Task1,也就是上图的起始位置。

运行过程描述如下:
 任务 Task1 运行的过程需要调用函数 printf,发现任务 Task3 正在调用,任务 Task1 会被挂起,等待 Task3 释放函数 printf。
 在调度器的作用下,任务 Task3 得到运行,Task3 运行的过程中,由于任务 Task2 就绪,抢占了 Task3的运行。优先级翻转问题就出在这里了,从任务执行的现象上看,任务 Task1 需要等待 Task2 执行完毕才有机会得到执行,这个与抢占式调度正好反了,正常情况下应该是高优先级任务抢占低优先级任务的执行,这里成了高优先级任务 Task1 等待低优先级任务 Task2 完成。所以这种情况被称之为优先级翻转问题
 任务 Task2 执行完毕后,任务 Task3 恢复执行,Task3 释放互斥资源后,任务 Task1 得到互斥资源,从而可以继续执行。

上面就是一个产生优先级翻转问题的现象。

1.3 FreeRTOS 互斥信号量的实现

  FreeRTOS 互斥信号量是怎么实现的呢?其实相对于二值信号量,互斥信号量就是解决了一下优先级翻转的问题。下面我们通过如下的框图来说明一下 FreeRTOS 互斥信号量的实现,让大家有一个形象的认识。

FreeRTOS — 互斥信号量**

运行条件:

 创建 2 个任务 Task1 和 Task2,优先级分别为 1 和 3,也就是任务 Task2 的优先级最高。
 任务 Task1 和 Task2 互斥访问串口打印 printf。
 使用 FreeRTOS 的互斥信号量实现串口打印 printf 的互斥访问。

运行过程描述如下:

 低优先级任务 Task1 执行过程中先获得互斥资源 printf 的执行。此时任务 Task2 抢占了任务 Task1的执行,任务 Task1 被挂起。任务 Task2 得到执行。
 任务 Task2 执行过程中也需要调用互斥资源,但是发现任务 Task1 正在访问,此时任务 Task1 的优先级会被提升到与 Task2 同一个优先级,也就是优先级 3,这个就是所谓的优先级继承(Priority inheritance),这样就有效地防止了优先级翻转问题。任务 Task2 被挂起,任务 Task1 有新的优先级继续执行。
 任务 Task1 执行完毕并释放互斥资源后,优先级恢复到原来的水平。由于互斥资源可以使用,任务Task2 获得互斥资源后开始执行。


上面就是一个简单的 FreeRTOS 互斥信号量的实现过程。

1.4 FreeRTOS 中断方式互斥信号量的实现

 互斥信号量仅支持用在 FreeRTOS 的任务中,中断函数中不可使用。

 2 互 斥 信 号 量 API 函 数

使用如下 18 个函数可以实现 FreeRTOS 的信号量(含计数信号量,二值信号量和互斥信号):
 xSemaphoreCreateBinary()
 xSemaphoreCreateBinaryStatic()
 vSemaphoreCreateBinary()
 xSemaphoreCreateCounting()
 xSemaphoreCreateCountingStatic()
 xSemaphoreCreateMutex()
 xSemaphoreCreateMutexStatic()
 xSem'CreateRecursiveMutex()
 xSem'CreateRecursiveMutexStatic()
 vSemaphoreDelete()
 xSemaphoreGetMutexHolder()
 uxSemaphoreGetCount()
 xSemaphoreTake()
 xSemaphoreTakeFromISR()
 xSemaphoreTakeRecursive()
 xSemaphoreGive()
 xSemaphoreGiveRecursive()

  xSemaphoreGiveFromISR()

关于这 18 个函数的讲解及其使用方法可以看 FreeRTOS 在线版手册:

FreeRTOS — 互斥信号量**

2.1 函数 xSemaphoreCreateMutex

函数原型:
SemaphoreHandle_t xSemaphoreCreateMutex( void )
函数描述:
函数 xSemaphoreCreateMutex 用于创建互斥信号量。
 返回值,如果创建成功会返回互斥信号量的句柄,如果由于 FreeRTOSConfig.h 文件中 heap 大小不足,无法为此互斥信号量提供所需的空间会返回 NULL。 

使用这个函数要注意以下问题:
1. 此函数是基于函数 xQueueCreateMutex 实现的:
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
函数 xQueueCreateMutex 的实现是基于消息队列函数 xQueueGenericCreate 实现的。
2. 使用此函数要在 FreeRTOSConfig.h 文件中使能宏定义:
#define configUSE_MUTEXES 1

使用举例:

static SemaphoreHandle_t xMutex = NULL;
/*
*********************************************************************************************************
* 函 数 名: AppObjCreate
* 功能说明: 创建任务通信机制
* 形 参: 无
* 返 回 值: 无
*********************************************************************************************************
*/
static void AppObjCreate (void)
{
/* 创建互斥信号量 */
xMutex
= xSemaphoreCreateMutex();
if(xSemaphore == NULL)
{
/* 没有创建成功,用户可以在这里加入创建失败的处理机制 */
}
}

2.2 函数 xSemaphoreGive

函数原型:
xSemaphoreGive( SemaphoreHandle_t xSemaphore ); /* 信号量句柄 */
函数描述:
函数 xSemaphoreGive 用于在任务代码中释放信号量。
 第 1 个参数是信号量句柄。
 返回值,如果信号量释放成功返回 pdTRUE,否则返回 pdFALSE,因为信号量的实现是基于消息队列,返回失败的主要原因是消息队列已经满了。
使用这个函数要注意以下问题:
1. 此函数是基于消息队列函数 xQueueGenericSend 实现的:
#define xSemaphoreGive( xSemaphore )  xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
2. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序中使用的是xSemaphoreGiveFromISR。

3. 使用此函数前,一定要保证用函数 xSemaphoreCreateBinary(), xSemaphoreCreateMutex() 或者 xSemaphoreCreateCounting()创建了信号量。
4. 此函数不支持使用 xSemaphoreCreateRecursiveMutex()创建的信号量。

 

2.3 函数 xSemaphoreTake

函数原型:
xSemaphoreTake( SemaphoreHandle_t xSemaphore, /* 信号量句柄 */
TickType_t xTicksToWait ); /* 等待信号量可用的最大等待时间 */
函数描述:
函数 xSemaphoreTake 用于在任务代码中获取信号量。
 第 1 个参数是信号量句柄。
 第 2 个参数是没有信号量可用时,等待信号量可用的最大等待时间,单位系统时钟节拍。

 返回值,如果创建成功会获取信号量返回 pdTRUE,否则返回 pdFALSE。
使用这个函数要注意以下问题:
1. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序使用的是xSemaphoreTakeFromISR。
2. 如果消息队列为空且第 2 个参数为 0,那么此函数会立即返回。
3. 如果用户将 FreeRTOSConfig.h 文件中的宏定义 INCLUDE_vTaskSuspend 配置为 1 且第 2 个参数配置为 portMAX_DELAY,那么此函数会永久等待直到信号量可用。

使用举例:

static SemaphoreHandle_t xMutex = NULL;
/*
*********************************************************************************************************
* 函 数 名: vTaskLED
* 功能说明: 实现串口的互斥访问,防止多个任务同时访问造成串口打印乱码
* 形 参: pvParameters 是在创建该任务时传递的形参
* 返 回 值: 无
* 优 先 级: 2
*********************************************************************************************************
*/
static void vTaskLED(void *pvParameters)
{
TickType_t xLastWakeTime;
const TickType_t xFrequency = 200;
/* 获取当前的系统时间 */
xLastWakeTime
= xTaskGetTickCount();
while(1)
{
/* 互斥信号量,xSemaphoreTake 和 xSemaphoreGive 一定要成对的调用 */
xSemaphoreTake(xMutex, portMAX_DELAY);
printf(
"任务 vTaskLED 在运行\r\n");
bsp_LedToggle(
2);
bsp_LedToggle(
3);
xSemaphoreGive(xMutex);
/* vTaskDelayUntil 是绝对延迟,vTaskDelay 是相对延迟。*/
vTaskDelayUntil(
&xLastWakeTime, xFrequency);
}
}

 互斥信号量,xSemaphoreTake 和 xSemaphoreGive 一定要成对的调用 

FreeRTOS — 互斥信号量**

经过测试,互斥信号量是可以被其他任务释放的,但是我们最好不要这么做,因为官方推荐的就是在同一个任务中接收和释放。如果在其他任务释放,不仅仅会让代码整体逻辑变得复杂,还会给使用和维护这套API的人带来困难。遵守规范,总是好的。

裸机编程的时候,我经常想一个问题,就是怎么做到当一个标志位触发的时候,立即执行某个操作,如同实现标志中断一样,在os编程之后,我们就可以让一个优先级最高任务一直等待某个信号量,如果获得信号量,就执行某个操作,实现类似标志位中断的作用(当然,要想正真做到中断效果,那就需要屏蔽所有可屏蔽中断,而临界区就可以做到)。

再说一下递归互斥信号量:递归互斥信号量,其实就是互斥信号量里面嵌套互斥信号量

使用举例:

static void vTaskMsgPro(void *pvParameters)
{
TickType_t xLastWakeTime;
const TickType_t xFrequency = 1500;

/* 获取当前的系统时间 */
xLastWakeTime
= xTaskGetTickCount();

while(1)
{
/* 递归互斥信号量,其实就是互斥信号量里面嵌套互斥信号量 */
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
{
/* -------------------------------------- */
//假如这里是被保护的资源,第1层被保护的资源,用户可以在这里添加被保护资源
/* ---------------------------------------------------------------------------- */
printf(
"任务vTaskMsgPro在运行,第1层被保护的资源,用户可以在这里添加被保护资源\r\n");

/* 第1层被保护的资源里面嵌套被保护的资源 */
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
{
/* ------------------------------------------------------------------------ */
//假如这里是被保护的资源,第2层被保护的资源,用户可以在这里添加被保护资源
/* ------------------------------------------------------------------------ */
printf(
"任务vTaskMsgPro在运行,第2层被保护的资源,用户可以在这里添加被保护资源\r\n");

/* 第2层被保护的资源里面嵌套被保护的资源 */
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
{
printf(
"任务vTaskMsgPro在运行,第3层被保护的资源,用户可以在这里添加被保护资源\r\n");
bsp_LedToggle(
1);
bsp_LedToggle(
4);
}
xSemaphoreGiveRecursive(xRecursiveMutex);
}
xSemaphoreGiveRecursive(xRecursiveMutex);
}
xSemaphoreGiveRecursive(xRecursiveMutex);

/* vTaskDelayUntil是绝对延迟,vTaskDelay是相对延迟。*/
vTaskDelayUntil(
&xLastWakeTime, xFrequency);
}
}

可以前面的那个官方文档那样用if判断传递共享量:

FreeRTOS — 互斥信号量**

也可以用我们递归互斥信号量里面的portMAX_DELAY指定永久等待,即xSemaphoreTakeRecursive返回的不是pdTRUE时,会一直等待信号量,直到有信号量来才执行后面的语句。