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 优先级翻转问题
下面我们通过如下的框图来说明一下优先级翻转的问题,让大家有一个形象的认识。
运行条件:
创建 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 互斥信号量的实现,让大家有一个形象的认识。
运行条件:
创建 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 在线版手册:
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 一定要成对的调用
经过测试,互斥信号量是可以被其他任务释放的,但是我们最好不要这么做,因为官方推荐的就是在同一个任务中接收和释放。如果在其他任务释放,不仅仅会让代码整体逻辑变得复杂,还会给使用和维护这套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判断传递共享量:
也可以用我们递归互斥信号量里面的portMAX_DELAY指定永久等待,即xSemaphoreTakeRecursive返回的不是pdTRUE时,会一直等待信号量,直到有信号量来才执行后面的语句。