优先级翻转以及互斥信号量

本文探讨了优先级翻转现象在抢占式内核中的表现,尤其是在实时操作系统中,解释了任务调度如何在中断和互斥信号量的作用下影响高优先级任务的执行。重点介绍了互斥信号量的优先级继承机制如何降低优先级翻转的影响。
摘要由CSDN通过智能技术生成

优先级翻转实验以及分析现象

优先级翻转指的是高优先级的任务反而慢执行,低优先级的任务反而优先执行。优先级翻转在抢占式内核中是非常常见的,但是在实时操作系统中是不允许出现优先级翻转的,因为优先级翻转会破坏任务的预期顺序,可能会导致未知的严重后果。
在这里插入图片描述
上图中三个任务的优先级的顺序为H>M>L,L 获取信号量还未来得及释放,由于H就绪抢占L并开始运行,信号量被L占有从而导致H进入阻塞,此时L应该继续运行,但M的优先级高于L,所以M就绪抢占L,任务M开始执行完毕。任务L运行并释放信号量,此时H获取信号量,H运行。
高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。但其他中等优先级的任务却能抢到CPU资源。从现象上看,就像是中优先级的任务比高优先级任务具有更高的优先权(即优先级翻转)
在这里插入图片描述

接下来我们编写程序来模拟优先级翻转这个实验。
以下是这三个任务执行的具体内容。

void task1( void * pvParameters )
{	
    while(1)
    {
	  printf("低优先级任务获取信号量成功\r\n");
	  SemaphoreTake(flag,portMAX_DELAY);
	  printf("低优先级任务正在运行\r\n");
	  delay_ms(3000);
	  printf("低优先级任务释放信号量\r\n");
      xSemaphoreGive(flag); 
      vTaskDelay(1000);
    }
}
void task2( void * pvParameters )
{
    while(1)
    {
			printf("中优先级任务正在运行\r\n");
			vTaskDelay(1000);
			
    }
}
void task3( void * pvParameters )
{
    while(1)
    {
	  printf("高优先级任务正在运行\r\n");
      xSemaphoreTake(flag,portMAX_DELAY);
      printf("高优先级任务正在运行\r\n");
      delay_ms(1000);
      printf("高优先级任务释放信号量\r\n");
      xSemaphoreGive(flag); 
      vTaskDelay(1000);	
    }
}

在这里插入图片描述

上图为程序运行的结果,刚开始高优先级任务最先开始运行,先获取信号量,接着释放信号量,接下来vTaskDelay(1000);阻塞1000ms轮到中优先级任务开始执行,中优先级打印中优先级正在运行,接下来vTaskDelay(1000)进行任务调度,轮到低优先级任务进行。低优先级获取信号量并开始运行,但是这里占有信号量的时间为delay_ms(3000);当阻塞时间1000ms过去后,高优先级抢占,开始获取信号量,但是低优先任务获取信号量后进入delay_ms,占有该信号量,所以此时高优先级任务进入阻塞,此时中优先级任务开始运行,接下来阻塞1000ms,发现1000ms后低优先级还在延时当中,所以中优先级一直运行,直到低优先级释放信号量,此时高优先级任务解除阻塞,继续正常运行。
至于为啥delay_ms期间不是占用CPU为啥还能继续进行任务调度。原因如下:
在RTOS(实时操作系统)环境下,尽管忙等待确实占用CPU资源并且在逻辑上不释放控制权给其他任务,但通过中断机制,系统仍然可以执行其他任务。这里的关键是理解RTOS的中断和任务调度机制如何相互作用,即使在一个任务执行忙等待时。

中断和任务调度

  1. 中断机制:在现代微控制器和RTOS中,中断是一种允许外部事件(如定时器溢出、外设事件等)打断当前执行流程的机制。当中断发生时,CPU会立即暂停当前任务的执行,跳转到对应的中断服务例程(ISR)执行。

  2. SysTick中断:在FreeRTOS中,SysTick中断是一个特别重要的中断,它定期发生并由系统用来跟踪时间的流逝,如任务延时、时间片调度等。每次SysTick中断发生,RTOS的调度器都会评估是否需要根据任务的优先级或者等待状态来切换任务。

  3. 从ISR返回时的任务切换:如果在中断服务例程中,RTOS调度器决定另一个任务比当前运行的任务有更高的运行优先级,那么当中断服务完成,从ISR返回到用户任务执行时,系统会切换到那个更高优先级的任务执行。这就是即使在忙等待期间,系统仍能执行其他任务的原因。

忙等待与中断

  • 在执行忙等待的代码期间,如果没有禁用中断,那么系统中断仍然可以发生。这意味着,虽然忙等待代码逻辑上没有释放CPU给其他任务,但中断(特别是SysTick中断)可以打断这个等待,允许RTOS调度器运行其他任务。

  • 忙等待的实现通常是通过循环检查某个条件,而这个循环本身并不阻止中断的发生。因此,尽管当前任务看起来一直在占用CPU,中断机制使得系统有机会在忙等待期间响应外部事件或定时事件,并根据需要切换任务。

互斥信号量

互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。在互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙, 当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。
互斥信号量使用和二值信号量相同的 API 操作函数,所以互斥信号量也可以设置阻塞时间,不同于二值信号量的是互斥信号量具有优先级继承的特性。当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。
优先级继承并不能完全的消除优先级翻转,它只是尽可能的降低优先级翻转带来的影响。
硬实时应用应该在设计之初就要避免优先级翻转的发生。互斥信号量不能用于中断服务函数中,
原因如下:
● 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
● 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
为什么互斥信号量只适用于任务
优先级继承逻辑:优先级继扅的逻辑是基于任务优先级的动态调整。中断服务函数(ISR)并不具有“优先级”概念,至少不是以同样的方式作为任务优先级来处理。ISR的“优先级”是指中断的优先级,这与任务调度的优先级是两个不同的概念。因此,优先级继承机制在ISR中没有意义。

中断上下文与任务上下文:中断服务函数执行于中断上下文,这意味着它们运行在特权级别,通常需要快速完成并且不能被阻塞。任务运行在任务上下文,可以根据调度策略进行挂起、阻塞和恢复。互斥信号量的阻塞特性与中断的快速响应要求相冲突。

为什么ISR中不能等待互斥信号量
阻塞操作:在RTOS中,等待互斥信号量通常意味着如果信号量不可用,任务将进入阻塞状态,直到信号量被释放。然而,在ISR中进行阻塞操作是不合适的,因为这会导致中断处理延迟,影响系统对实时事件的响应能力。

RTOS调度器的影响:在中断上下文中进行阻塞操作可能会干扰RTOS的调度器,因为调度器不期望在处理中断时发生任务切换。此外,中断服务函数中的阻塞操作可能导致不可预测的行为,比如死锁或者优先级反转,尤其是当中断优先级配置不当时。
FreeRTOS 提供了两个互斥信号量创建函数,如下图所示:
在这里插入图片描述

互斥信号量创建过程分析

#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
ueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )
{
		Queue_t *pxNewQueue;
		const UBaseType_t uxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0;
		pxNewQueue = ( Queue_t * ) xQueueGenericCreate( uxMutexLength, uxMutexSize,\ (1)
		 ucQueueType );
		prvInitialiseMutex( pxNewQueue ); (2)
		return pxNewQueue;
}

(1)、调用函数 xQueueGenericCreate()创建一个队列,队列长度为 1,队列项长度为 0,队列类型为参数 ucQueueType。由于本函数是创建互斥信号量的,所以参数 ucQueueType 为queueQUEUE_TYPE_MUTEX。
(2)、调用函数 prvInitialiseMutex()初始化互斥信号量。
接下来来看初始化互斥量这个函数。

static void prvInitialiseMutex( Queue_t *pxNewQueue )
{
if( pxNewQueue != NULL )
{
		//虽然创建队列的时候会初始化队列结构体的成员变量,但是此时创建的是互斥
		//信号量,因此有些成员变量需要重新赋值,尤其是那些用于优先级继承的。
		pxNewQueue->pxMutexHolder = NULL; (1)
		//这行代码将互斥量的持有者设置为NULL,表示当前没有任务持有这个互斥量。
		pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX; (2)
		//这行代码设置互斥量的类型为queueQUEUE_IS_MUTEX,这是一个枚举值,用于区分这个队列实际上是用作互斥量。
		//如果是递归互斥信号量的话。
		pxNewQueue->u.uxRecursiveCallCount = 0; (3)
		//这行代码初始化递归调用计数为0。
		//递归互斥量允许同一个任务多次获取互斥量而不会导致死锁,只要释放次数与获取次数相匹配。
		traceCREATE_MUTEX( pxNewQueue );
		//释放互斥信号量
		( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U,\
		 queueSEND_TO_BACK );
		//这行代码使用xQueueGenericSend函数释放互斥量。
		//尽管这里的目的是初始化互斥量,但通过发送操作来实现。
		//NULL表示没有实际的数据被发送,( TickType_t ) 0U表示操作不会等待,
		//queueSEND_TO_BACK表示数据(如果有的话)会被添加到队列的末尾
		}
		else
		{
			traceCREATE_MUTEX_FAILED();
		}
}

释 放 互 斥 信 号 量 的 时 候 和 二 值 信 号 量 、 计 数 型 信 号 量 一 样 , 都 是 用 的 函 数xSemaphoreGive()(实际上完成信号量释放的是函数 xQueueGenericSend())。不过由于互斥信号量涉及到优先级继承的问题,所以具体处理过程会有点区别。使用函数 xSemaphoreGive()释放信号 量 最 重 要 的 一 步 就 是 将 uxMessagesWaiting 加 一 , 而 这 一 步 就 是 通 过 函 数prvCopyDataToQueue() 来完成的,释放信号量的函数 xQueueGenericSend() 会调用prvCopyDataToQueue()。互斥信号量的优先级继承也是在函数 prvCopyDataToQueue()中完成的,此函数中有如下一段代码:

static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue, 
const void * pvItemToQueue, 
const BaseType_t xPosition )
{
		BaseType_t xReturn = pdFALSE;
		UBaseType_t uxMessagesWaiting;
		uxMessagesWaiting = pxQueue->uxMessagesWaiting;
		if( pxQueue->uxItemSize == ( UBaseType_t ) 0 )
		{
		#if ( configUSE_MUTEXES == 1 ) //互斥信号量
		{
		if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ) (1)
		{
			xReturn = xTaskPriorityDisinherit( ( void * ) pxQueue->pxMutexHolder );(2)
			pxQueue->pxMutexHolder = NULL; (3)
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
		}
		#endif /* configUSE_MUTEXES */
		}
		/*********************************************************************/
		/*************************省略掉其他处理代码**************************/
		/*********************************************************************/
		pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;
		return xReturn;
}

在这个上下文中,xTaskPriorityDisinherit函数的作用是管理这种优先级继承的逻辑。具体来说,当一个任务释放它持有的互斥量时,这个函数被调用以检查任务是否因为持有互斥量而继承了更高的优先级。如果是这样,函数会将任务的优先级恢复到它原本的优先级,即放弃继承的优先级。
xTaskPriorityDisinherit()代码如下:

BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder )
{
	TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;
	BaseType_t xReturn = pdFALSE;
	if( pxMutexHolder != NULL ) (1)
	{
		//当一个任务获取到互斥信号量以后就会涉及到优先级继承的问题,正在释放互斥
		//信号量的任务肯定是当前正在运行的任务 pxCurrentTCB。
		configASSERT( pxTCB == pxCurrentTCB );
		configASSERT( pxTCB->uxMutexesHeld );
		( pxTCB->uxMutexesHeld )--; (2)
		//是否存在优先级继承?如果存在的话任务当前优先级肯定和任务基优先级不同。
		if( pxTCB->uxPriority != pxTCB->uxBasePriority ) (3)//如果uxMutexesHeld归零,意味着
		//任务已经释放了它持有的所有互斥量,可以将其优先级恢复到基本优先级。
		{
		//当前任务只获取到了一个互斥信号量
		if( pxTCB->uxMutexesHeld == ( UBaseType_t ) 0 ) (4)
		{
			if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) (5)
			{
				taskRESET_READY_PRIORITY( pxTCB->uxPriority ); (6)
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		//使用新的优先级将任务重新添加到就绪列表中
		traceTASK_PRIORITY_DISINHERIT( pxTCB, pxTCB->uxBasePriority );
		pxTCB->uxPriority = pxTCB->uxBasePriority; (7)
		/* Reset the event list item value. It cannot be in use for
		any other purpose if this task is running, and it must be
		running to give back the mutex. */
		listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), \ (8)
		( TickType_t ) configMAX_PRIORITIES - \
		( TickType_t ) pxTCB->uxPriority );
		prvAddTaskToReadyList( pxTCB ); (9)
		xReturn = pdTRUE; (10)
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
		}
		else
		{
		mtCOVERAGE_TEST_MARKER();
		}
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
	return xReturn;
}

获取互斥信号量的函数同获取二值信号量和计数型信号量的函数相同,都是xSemaphoreTake()(实际执行信号量获取的函数是 xQueueGenericReceive()),获取互斥信号量的过程也需要处理优先级继承的问题,函数 xQueueGenericReceive()在文件 queue.c 中有定义,

  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值