10.FreeRTOS_互斥量

互斥量概述

在博文“ FreeRTOS_信号量 ”中,使用了二进制信号量实现了互斥,保护了串口资源。博文链接如下:

FreeRTOS_信号量-CSDN博客

但还是要引入互斥量的概念。互斥量与二进制信号量相比,能够多实现如下两个功能:

  • 防止优先级反转问题
  • 解决递归上锁/解锁问题

互斥量有两种:

  • 普通互斥量:具有优先级继承功能,解决优先级反转问题
  • 递归锁:具有优先级继承功能、能够解决递归上锁/解锁问题、解决谁上锁谁才能解锁的问题

优先级反转

优先级反转问题

假设任务A、B、C的优先级为1、2、3。在程序开始时,A进行上锁,之后B运行抢断的A,之后C运行抢断了B。在C中,想获得锁,但A已经上锁,所以C进入了阻塞,释放了CPU。这时B继续运行,但不解锁,从而导致C被B抢占,即高优先级的任务是否能继续执行由低优先级的任务决定。

这种现象称为优先级反转。

优先级继承

使用优先级继承的方法解决优先级反转的问题。

在C获得锁之后进入阻塞状态,同时执行上锁的任务A会继承C的优先级3,从而进行执行。当A执行完解锁后,A优先级的优先级变回原来的优先级1。之后C获得锁,不再阻塞,C继续执行,执行完成之后,B运行。

在低优先级任务上锁,高优先级任务获得锁阻塞之后,低优先级任务继承高优先级任务的优先级的操作叫做优先级继承。

递归上锁/解锁

递归上锁原因

递归上锁的示例代码如下:

void fun(){
	xSemaphoreTake(SemaphoreHandleTest,portMAX_DELAY);/* 上锁 */
	/* 一些功能 */
	xSemaphoreGive(SemaphoreHandleTest);/* 解锁 */
}
void Task1(void *param){
	while(1){
		xSemaphoreTake(SemaphoreHandleTest,portMAX_DELAY);/* 上锁 */
		/* 一些功能 */
		fun();
		xSemaphoreGive(SemaphoreHandleTest);/* 解锁 */
	}
}

在上述代码中,任务1首先进行了上锁,之后调用了fun函数。在fun函数中又进行了上锁,但这时已经上锁,所以就阻塞在了fun函数中。最终导致Task1无法执行完成fun函数,也就无法执行解锁函数,形成死锁。

递归锁

使用递归锁可以解决上述问题。递归锁的作用是,如果开始时为A来上锁,那么在上锁之后,A依旧可以进行调用上锁函数进行上锁,而不会进入阻塞状态。但不管怎样,上锁之后都要解锁,即:上锁多少次,就要解锁多少次,上锁与解锁一 一配对。

除此之外,递归锁还可以满足谁上锁,谁才有权力解锁的问题。而信号量和普通互斥锁并不能实现这个功能。

相关配置

在使用互斥量之前,需要打开宏开关,具体的步骤如下:

在使用递归锁之前,需要打开宏开关,具体的步骤如下:

互斥量相关函数

创建普通互斥量

函数声明如下:

/* 这是一个宏,创建普通互斥量 */
xSemaphoreCreateMutex()
 
/* 宏定义 */
#define xSemaphoreCreateMutex()    xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
 
/* 实际调用函数 */
QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )

返回值:互斥量的句柄

互斥量在创建后与二进制信号量不同,互斥量初始值为1,二进制信号量初始值为0

获取/释放互斥量

获取和释放互斥量所用的函数与二值信号量的函数一致。

函数声明如下:

/* 这是一个宏,存入(释放)信号量/互斥量 */
xSemaphoreGive( xSemaphore )

/* 这是一个宏,获取信号量/互斥量 */
xSemaphoreTake( xSemaphore, xBlockTime )

存入函数返回值:成功返回pdPASS,在当前计数值=最大计数值时,会存入失败

xSemaphore :信号量句柄

xBlockTime :阻塞等待时间,portMAX_DELAY为死等

创建递归锁

函数声明如下:

/* 这是一个宏,创建递归锁 */
xSemaphoreCreateRecursiveMutex()
 
/* 宏定义 */
#define xSemaphoreCreateRecursiveMutex()  \
        xQueueCreateMutex( queueQUEUE_TYPE_RECURSIVE_MUTEX )
 
/* 实际调用函数 */
QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )

返回值:互斥量的句柄

创建后初始值为1

获取/释放递归锁

获取和释放递归锁的函数用法与互斥量一致,只是名字有所差别

函数声明如下:

/* 这是一个宏,存入(释放)递归锁 */
xSemaphoreGiveRecursive( xMutex )

/* 这是一个宏,获取递归锁 */
xSemaphoreTakeRecursive( xMutex, xBlockTime )

存入函数返回值:成功返回pdPASS,在当前计数值=最大计数值时,会存入失败

xSemaphore :信号量句柄

xBlockTime :阻塞等待时间,portMAX_DELAY为死等

验证实验

1、基本互斥实验

使用互斥量来确保串口的输出不被打断。

与二进制信号量实现该功能相比,代码方面的差异只在初始化不同,其他代码都没有进行修改

二进制信号量实现该功能的初始化代码如下:

/* 创建二进制信号量,初始值自动设置为 0 */
SemaphoreHandleTest = xSemaphoreCreateBinary();
/* 让信号量为1,代表串口资源可用 */
xSemaphoreGive(SemaphoreHandleTest);

互斥量实现该功能的初始化代码如下:

/* 创建互斥量,初始值自动设置为 1 */
SemaphoreHandleTest = xSemaphoreCreateMutex();

实验现象和其他代码实现与博文“ FreeRTOS_信号量 ”中 “ 验证实验 ” 均一致。博文链接如下:

FreeRTOS_信号量-CSDN博客

2、优先级继承实验

根据本文 “互斥量概述” 中 “ 优先级反转 ”内容的描述,来进行编写代码。这里使用二进制信号量与互斥量进行运行结果的对比。

具体的代码实现如下:

QueueHandle_t SemaphoreHandleTest;
char taskA_flag = 0;
char taskB_flag = 0;
char taskC_flag = 0;
void TaskAFunction(void *param){
	
	int i=0;
	
	while(1){
		
		/* 上锁 */
		xSemaphoreTake(SemaphoreHandleTest,portMAX_DELAY);
		/* 执行一个很长的时间的程序 */
		taskA_flag = 1;
		taskB_flag = 0;
		taskC_flag = 0;
		for(i=0;i<20;i++){
			printf("%d,%d,%d",taskA_flag,taskB_flag,taskC_flag);
			taskA_flag = 1;
			taskB_flag = 0;
			taskC_flag = 0;
		}
		/* 解锁 */
		xSemaphoreGive(SemaphoreHandleTest);
		
	}
}
void TaskBFunction(void *param){
	vTaskDelay(5);/* B先休眠,让A任务执行 */
	while(1){
		
		/* B一直在执行,这会导致如果A优先级低于B,则A一直不执行 */
		taskA_flag = 0;
		taskB_flag = 1;
		taskC_flag = 0;
		
	}
}
void TaskCFunction(void *param){
	
	int i=0;
	
	vTaskDelay(10);/* C先休眠,让AB任务执行 */
	while(1){
		
		taskA_flag = 0;
		taskB_flag = 0;
		taskC_flag = 1;
		
		/* 上锁 */
		xSemaphoreTake(SemaphoreHandleTest,portMAX_DELAY);
		/* C任务执行一段时间 */
		for(i=0;i<10;i++){
			printf("this is taskC\r\n");
			taskA_flag = 0;
			taskB_flag = 0;
			taskC_flag = 1;
		}
		/* 解锁 */
		xSemaphoreGive(SemaphoreHandleTest);
	}
}
int main( void )
{
	
	TaskHandle_t xHandleTask1;
	TaskHandle_t xHandleTask2;
	TaskHandle_t xHandleTask3;
	
	prvSetupHardware();
	SerialPortInit();
	printf("UART TEST\r\n");
	
	/* 创建互斥量,初始值自动设置为 1 */
	//SemaphoreHandleTest = xSemaphoreCreateMutex();
	
	/* 创建二进制信号量,初始值自动设置为 0 */
	SemaphoreHandleTest = xSemaphoreCreateBinary();
	/* 让信号量为1,代表资源可用 */
	xSemaphoreGive(SemaphoreHandleTest);
	
	xTaskCreate(TaskAFunction,"TaskA",100,(void*)NULL,1,&xHandleTask1);
	xTaskCreate(TaskBFunction,"TaskB",100,(void*)NULL,2,&xHandleTask2);
	xTaskCreate(TaskCFunction,"TaskC",100,(void*)NULL,3,&xHandleTask3);
	
	vTaskStartScheduler();
	
	return 0;
}

该代码的主要功能就是利用taskA_flag 、taskB_flag、taskC_flag 这三个标志位来判断当前任务是谁在运行。通过逻辑分析仪显示出这三个变量的电平来分析抢占关系。

二值信号量的运行结果如下:

可以看到,在1 -> 2阶段中,BC调用延时处于阻塞态,因此A运行。在2->3阶段中,A运行了一段时间后,B的延时结束,B开始抢占A进行运行。在2->3阶段中,B运行了一段时间后,C的延时结束,C开始抢占B进行运行。以上是正常的抢占流程。

在C抢占B之后,尝试获取信号量,但信号量已经被A所获取,还未进行释放,因此C进入了阻塞状态,之后B进行抢占到CPU继续执行。但B的优先级高于A,A不能够释放信号量,这样就导致了C永远处于阻塞态,只有当B释放CPU,A才能释放信号量,从而C才可以运行,这样就产生了优先级反转的问题。

互斥量的运行结果如下:

可以看到,在1 -> 2阶段中,BC调用延时处于阻塞态,因此A运行。在2->3阶段中,A运行了一段时间后,B的延时结束,B开始抢占A进行运行。在2->3阶段中,B运行了一段时间后,C的延时结束,C开始抢占B进行运行。以上是正常的抢占流程。

在C抢占B之后,尝试获取互斥量,但互斥量已经被A所获取,还未进行释放,因此A进行优先级的继承,此时A的优先级变成了3,即:4 -> 5阶段。在5->6阶段中,A的优先级为3,执行完成打印工作后,对互斥量进行了解锁,这时任务A的优先级变回原来的优先级1,C任务抢占A开始执行。

3、谁上锁,谁才能解锁实验

普通互斥量和二进制信号量并未实现谁上锁,谁才能解锁,这里使用普通互斥量与递归锁来进行对比,来验证递归锁能够实现谁上锁,谁才能解锁的功能。

具体代码实现如下:

QueueHandle_t SemaphoreHandleTest;

void PrintFunction(void *param){
	
	while(1){
		
//		/* 以下为互斥量测试 */
//		xSemaphoreTake(SemaphoreHandleTest,portMAX_DELAY);
//		printf("%s",(char*)param);
//		xSemaphoreGive(SemaphoreHandleTest);
		
		/* 以下为递归锁测试 */
		xSemaphoreTakeRecursive(SemaphoreHandleTest,portMAX_DELAY);
		printf("%s",(char*)param);
		xSemaphoreGiveRecursive(SemaphoreHandleTest);

		vTaskDelay(1);
	}
}
void BreakSemaphoreTask(void *param){
	
	vTaskDelay(1);
	while(1){
		
		
//		/* 以下为互斥量测试 */
//		while(1){
//			
//			/* 如果获得不到锁,就直接往里面放一把锁 */
//			if(xSemaphoreTake(SemaphoreHandleTest,0) == pdFALSE){
//				xSemaphoreGive(SemaphoreHandleTest);
//			}else{
//				break;
//			}
//		}
//		printf("this is break\r\n");
//		xSemaphoreGive(SemaphoreHandleTest);
		
		/* 以下为递归锁测试 */
		while(1){
			
			/* 如果获得不到锁,就直接往里面放一把锁 */
			if(xSemaphoreTakeRecursive(SemaphoreHandleTest,0) == pdFALSE){
				xSemaphoreGiveRecursive(SemaphoreHandleTest);
			}else{
				break;
			}
		}
		printf("this is break\r\n");
		xSemaphoreGiveRecursive(SemaphoreHandleTest);
		
		vTaskDelay(1);
	}
}



int main( void )
{
	
	TaskHandle_t xHandleTask1;
	TaskHandle_t xHandleTask2;
	TaskHandle_t xHandleTask3;
	
	prvSetupHardware();
	SerialPortInit();
	printf("UART TEST\r\n");
	
//	/* 创建互斥量,初始值自动设置为 1 */
//	SemaphoreHandleTest = xSemaphoreCreateMutex();
	
	/* 创建递归锁,初始值自动设置为 1 */
	SemaphoreHandleTest = xSemaphoreCreateRecursiveMutex();
	
	xTaskCreate(PrintFunction,"TaskA",100,(void*)"this is TaskA\r\n",1,&xHandleTask1);
	xTaskCreate(PrintFunction,"TaskB",100,(void*)"this is TaskB\r\n",1,&xHandleTask2);
	xTaskCreate(BreakSemaphoreTask,"TaskC",100,(void*)NULL,1,&xHandleTask3);
	
	vTaskStartScheduler();
	
	return 0;
}

互斥量的运行结果如下:

可以看到,打印结果乱套了。这是因为在A上锁之后,C任务去放进去了一把锁,导致B任务原来在阻塞,突然获得了锁,执行了B任务中的printf。因此互斥量并没有实现谁上锁,谁才能够解锁的功能。

递归锁的运行结果如下:

可以看到,打印变得非常正常。这是因为递归锁可以实现谁上锁,谁才能够解锁,因此不能够再C中解开A的锁,从而执行结果变得正常。

4、递归上锁实验

在“ 谁上锁,谁才能解锁实验 ”的代码基础上进行修改,将打印函数修改为如下情况:

void PrintFunction(void *param){
	
	int i=0;
	
	while(1){
		
//		/* 以下为互斥量测试 */
//		xSemaphoreTake(SemaphoreHandleTest,portMAX_DELAY);
//		printf("%s",(char*)param);
//		xSemaphoreGive(SemaphoreHandleTest);
		
		/* 以下为递归锁测试 */
		xSemaphoreTakeRecursive(SemaphoreHandleTest,portMAX_DELAY);
		for(i=0;i<5;i++){
            /* 递归上锁,上锁之后再上一次锁 */
			xSemaphoreTakeRecursive(SemaphoreHandleTest,portMAX_DELAY);
			printf("lock:%i\r\n",i);
			xSemaphoreGiveRecursive(SemaphoreHandleTest);
		}
		printf("%s",(char*)param);
		xSemaphoreGiveRecursive(SemaphoreHandleTest);

		vTaskDelay(1);
	}
}

运行结果如下:

可以看到lock0~4正常打印,说明并未产生阻塞的情况,递归锁可以递归上锁。

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值