互斥信号量

1. 优先级翻转

优先级翻转是使用二值信号量时常遇见的问题,在可剥夺内核中非常常见,但是在实时系统中不允许出现这种现象,因为会破坏任务的预期顺序,可能会导致严重后果。

如下图所示的优先级翻转的例子:

  • 低优先级任务L要访问共享资源,在获取到信号量使用CPU的过程中,如果此时高优先级任务H到达,会剥夺L的CPU使用权从而运行任务H
  • 当H想要访问共享资源时,由于此时该资源的信号量仍被L占用着,H只能挂起等待L释放该信号量
  • L继续运行,此时中优先级任务M到达,再次剥夺L的CPU使用权从而运行任务M
  • M执行完后,将CPU使用权归还给任务L,L继续运行
  • L运行完毕并释放出了信号量,至此高优先级的任务H才能获取到该信号量访问共享资源并运行

FreeRTOS系列|互斥信号量_信号量
由上图可见,任务H的优先级实际上降到了任务L的优先级水平,因为要一直等待L释放其占用的共享资源。过程中不需要使用共享资源的中优先级任务M抢占CPU使用权后顺利运行完成,这样就相当于M的优先级高于J,导致优先级翻转

2. 互斥信号量

互斥信号量其实就是一个拥有优先级继承的二值信号量。二值信号适合于同步应用中,互斥信号适用于需要互斥访问的应用中。互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,使用完资源后必须归还这个钥匙

当一个互斥信号量正被一个低优先级任务使用时,若有高优先级的任务也尝试获取该互斥信号量的话就会被阻塞。不过此时高优先级任务会将低优先级任务的优先级提升到与与自已相同的等级(即优先级继承)。优先级继承只是尽可能降低优先级翻转带来的影响,并不能完全消除优先级翻转

FreeRTOS系列|互斥信号量_freertos_02

3. 互斥信号量的API函数

3.1 创建互斥信号量
/********************动态创建互斥信号量**********************************************/
SemaphoreHandle_t xSemaphoreCreateMutex(void)
/********************静态创建互斥信号量**********************************************/
SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t * pxSemaphoreBuffer)
//参数:pxSemaphoreBuffer 指向一个StaticSemaphore_t类型的变量,用来保存信号量结构体
/***********************************************************************************/
返回值:创建成功返回互斥信号量句柄;失败返回NULL

动态互斥信号量创建函数是一个宏,最终是通过xQueueCreateMutex()函数来完成,其源码如下:

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateMutex()  xQueueCreateMutex(queueQUEUE_TYPE_MUTEX)		
#endif
/**************xQueueCreateMutex源码分析*********************/
QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType ){
  Queue_t *pxNewQueue;
  const UBaseType_t uxMutexLength = (UBaseType_t) 1,uxMutexSize = (UBaseType_t) 0;
  /* 创建一个队列长度为1,队列项长度为0,队列类型为queueQUEUE_TYPE_MUTEX的队列 */
  pxNewQueue = (Queue_t *)xQueueGenericCreate(uxMutexLength,uxMutexSize,ucQueueType);
  /* 初始化互斥信号量,其实就是初始化消息队列的控制块 */
  prvInitialiseMutex( pxNewQueue );
  return pxNewQueue;
}
/**************prvInitialiseMutex源码分析*********************/
static void prvInitialiseMutex( Queue_t *pxNewQueue ){
  if( pxNewQueue != NULL ){
	/* 对创建好的队列结构体的某些互斥信号量特有的成员变量重新赋值 */
	pxNewQueue->pxMutexHolder = NULL;//互斥信号量专有的宏
	pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;//互斥信号量专有的宏
	/* 如果是递归互斥信号量,将以下成员变量赋值为0 */
	pxNewQueue->u.uxRecursiveCallCount = 0;
	traceCREATE_MUTEX( pxNewQueue );
	/* 释放互斥信号量 */
	(void) xQueueGenericSend(pxNewQueue,NULL,(TickType_t) 0U,queueSEND_TO_BACK);
  }
  else{
	traceCREATE_MUTEX_FAILED();
  }
}
3.2 释放互斥信号量

释放互斥信号量函数与二值信号量、计数信号量释放函数是一样的。但是由于互斥信号涉及到优先级继承的问题,所以处理过程会有一些区别。

信号量释放函数xSemaphoreGive()实际上调用xQueueGenericSend()函数,分析该函数源码(消息队列中有介绍)可见函数会调用prvCopyDataToQueue()函数。互斥信号量的优先级继承就是在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 ){
		/* 调用以下函数处理优先级继承问题 */
		xReturn = xTaskPriorityDisinherit((void *) pxQueue->pxMutexHolder);
		pxQueue->pxMutexHolder = NULL;//互斥信号量释放后,就不属于任何任务了
	  }
	  else{
		mtCOVERAGE_TEST_MARKER();
	  } 
	}
	#endif /* configUSE_MUTEXES */
  }
  /*********************************************************/
  /********************省略其他处理代码*********************/
  /*********************************************************/
  pxQueue->uxMessagesWaiting = uxMessagesWaiting + ( UBaseType_t ) 1;
  return xReturn
}

优先级处理函数xTaskPriorityDisinherit()的源码分析如下:

BaseType_t xTaskPriorityDisinherit(TaskHandle_t const pxMutexHolder){
  TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;
  BaseType_t xReturn = pdFALSE;
  if( pxMutexHolder != NULL ){
	/* 任务获取到互斥信号量后就会涉及到优先级继承 */
	configASSERT( pxTCB == pxCurrentTCB );
	configASSERT( pxTCB->uxMutexesHeld );
	( pxTCB->uxMutexesHeld )--;//用于标记任务当前获取到的互斥信号量个数
	/* 如果任务当前优先级和任务基优先级不同,则存在优先级继承 */
	if(pxTCB->uxPriority != pxTCB->uxBasePriority){
	  /* 当前任务只获取到一个互斥信号量 */
	  if( pxTCB->uxMutexesHeld == ( UBaseType_t ) 0 ){
		/* 把任务的当前优先级降低到基优先级 */
		if(uxListRemove(&(pxTCB->xStateListItem)) == (UBaseType_t ) 0){
		  taskRESET_READY_PRIORITY( pxTCB->uxPriority );
		}
		else{
		  mtCOVERAGE_TEST_MARKER();
		}
		/* 使用新的优先级将任务添加到就绪列表中 */
		traceTASK_PRIORITY_DISINHERIT( pxTCB, pxTCB->uxBasePriority );
		pxTCB->uxPriority = pxTCB->uxBasePriority;
		/* 复位任务的事件列表项 */
		listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), \
		(TickType_t)configMAX_PRIORITIES - (TickType_t)pxTCB->uxPriority); 
		prvAddTaskToReadyList( pxTCB );//将恢复优先级后的任务重新添加到就绪表
		xReturn = pdTRUE;//返回pdTRUE,表示需要进行任务调度
	  }
	  else{
		mtCOVERAGE_TEST_MARKER();
	  }
	}
	else{
	  mtCOVERAGE_TEST_MARKER();
	}
  }
  else{
	mtCOVERAGE_TEST_MARKER();
  }
  return xReturn;
}
3.3 获取互斥信号量

互斥信号量获取函数与二值信号量、计数信号量获取函数是一样的,都是xSemaphoreTake()函数,最终调用xQueueGenericReceive();获取互斥信号量的过程也需要处理优先级继承的问题,源码分析如下示

BaseType_t xQueueGenericReceive(QueueHandle_t xQueue, 
								void * const pvBuffer, 
								TickType_t xTicksToWait, 
								const BaseType_t xJustPeeking){
  BaseType_t xEntryTimeSet = pdFALSE;
  TimeOut_t xTimeOut;
  int8_t *pcOriginalReadPosition;
  Queue_t * const pxQueue = ( Queue_t * ) xQueue;

  for( ;; ){
	taskENTER_CRITICAL();
	{
	  const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;
      /* 判断队列是否有消息 */
	  if(uxMessagesWaiting > (UBaseType_t ) 0){
		/* 如有数据,调用以下函数使用数据拷贝的方式从队列中提取数据 */
		pcOriginalReadPosition = pxQueue->u.pcReadFrom;
		prvCopyDataFromQueue( pxQueue, pvBuffer );
		if( xJustPeeking == pdFALSE ){//数据读取后需要将数据删除
		  traceQUEUE_RECEIVE( pxQueue );
          /* 移除消息 */
		  pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;
    	  #if ( configUSE_MUTEXES == 1 )
		  {
			if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){
			/* 若获取信号量成功,则标记互斥信号量的所有者 */
			  pxQueue->pxMutexHolder = (int8_t *) pvTaskIncrementMutexHeldCount(); 
			}
			else{
			  mtCOVERAGE_TEST_MARKER();
			}
		  }
		  #endif /* configUSE_MUTEXES */
		  /* 查看是否有任务因为入队而阻塞,若有就解除阻塞态 */
 		  if(listLIST_IS_EMPTY(&( pxQueue->xTasksWaitingToSend)) == pdFALSE){
			if(xTaskRemoveFromEventList(&(pxQueue->xTasksWaitingToSend)) != pdFALSE){
			  /* 若解除阻塞的任务优先级比当前任务高,就需要进行一次任务切换 */
			  queueYIELD_IF_USING_PREEMPTION();
			}
			else{
			  mtCOVERAGE_TEST_MARKER();
			}
		  }
		  else{
			mtCOVERAGE_TEST_MARKER();
		  }
		}
		else{//数据读取后不需要将数据删除
		  traceQUEUE_PEEK( pxQueue );
		  pxQueue->u.pcReadFrom = pcOriginalReadPosition;
		  /* 查看是否有任务因为出队而阻塞,若有就解除阻塞态 */
		  if(listLIST_IS_EMPTY(&(pxQueue->xTasksWaitingToReceive)) == pdFALSE){
			if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){
			  /* 若解除阻塞的任务优先级比当前任务高,就需要进行一次任务切换 */
			  queueYIELD_IF_USING_PREEMPTION();
			}
			else{
			  mtCOVERAGE_TEST_MARKER();
			}
		  }
		  else{
			mtCOVERAGE_TEST_MARKER();
		  }
		}
		taskEXIT_CRITICAL();
		return pdPASS;
	  }
	  else{	//若队列为空
		if( xTicksToWait == ( TickType_t ) 0 ){
		  /* 如阻塞时间为0,则直接返回errQUEUE_EMPTY */
		  taskEXIT_CRITICAL();
		  traceQUEUE_RECEIVE_FAILED( pxQueue );
		  return errQUEUE_EMPTY;
		}
		else if( xEntryTimeSet == pdFALSE ){
		  /* 若阻塞时间不为0,则初始化时间状态结构体 */
		  vTaskSetTimeOutState( &xTimeOut );
		  xEntryTimeSet = pdTRUE;
		}
		else{
		  /* Entry time was already set. */
		  mtCOVERAGE_TEST_MARKER();
		}
	  }
	}
	taskEXIT_CRITICAL();
	vTaskSuspendAll();
	prvLockQueue( pxQueue );
	/* 更新时间状态结构体,检查是否超时 */
	if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){
	  /* 检查队列是否为空 */
	  if( prvIsQueueEmpty( pxQueue ) != pdFALSE){//若队列为空
		traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );
		#if ( configUSE_MUTEXES == 1 )
		{
		  if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){
			taskENTER_CRITICAL();
			{
			  /* 处理互斥信号量的优先级继承 */
			  vTaskPriorityInherit( ( void * ) pxQueue->pxMutexHolder );
			}
			taskEXIT_CRITICAL();
		  }
		  else{
			mtCOVERAGE_TEST_MARKER();
		  }
		}
		#endif
		/* 由于队列为空,将任务添加到xTasksWaitingToReceive列表中 */
		vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
		prvUnlockQueue( pxQueue );
		if( xTaskResumeAll() == pdFALSE )
		{
		  portYIELD_WITHIN_API();
		}
		else
		{
		  mtCOVERAGE_TEST_MARKER();
		}
	  }
	  else	//若队列不为空,重试一次出队
	  {
		prvUnlockQueue( pxQueue );
		( void ) xTaskResumeAll();
	  }
	}
	else
	{
	  prvUnlockQueue( pxQueue );
	  ( void ) xTaskResumeAll();
      if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
	  {
		traceQUEUE_RECEIVE_FAILED( pxQueue );
		return errQUEUE_EMPTY;
	  }
	  else
	  {
		mtCOVERAGE_TEST_MARKER();
	  }
	}
  }
}

4. 互斥信号量的应用实例

本实例介绍并模拟如何使用互斥信号量来避免章节1中的优先级翻转现象

使用STM32CubeMX将FreeRTOS移植到工程中,创建优先级为高中低的三个任务、一个互斥信号量

  • High_Task:高优先级任务,会获取互斥信号量,获取成功后进行相应的处理,处理完后释放互斥信号量
  • Middle_Task:中优先级任务,简单的应用任务
  • Low_Task:低优先级任务,会获取互斥信号量,获取成功后进行相应的处理,处理完后释放互斥信号量。但是任务占用互斥信号量的时间比高优先级任务占用的时间要长
4.1 STM32CubeMX设置
  • RCC设置外接HSE,时钟设置为72M
  • USART1选择为异步通讯方式,波特率设置为115200Bits/s,传输数据长度为8Bit,无奇偶校验,1位停止位;
  • 激活FreeRTOS,添加任务,设置任务名称、优先级、堆栈大小、函数名称等参数

FreeRTOS系列|互斥信号量_freertos_03

  • 动态创建互斥信号量

FreeRTOS系列|互斥信号量_互斥_04

  • 使用FreeRTOS操作系统,一定要将HAL库的Timebase Source从SysTick改为其他定时器,选好定时器后,系统会自动配置TIM
  • 输入工程名,选择路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码
4.2 MDK-ARM软件编程
  • 添加High_Task、Middle_Task、Low_Task任务函数代码
/******************HighTask**************************/
void HighTask(void const * argument){
  for(;;){
    vTaskDelay(500);
	printf("High task Pend Semaphore\r\n");
	xSemaphoreTake(MutexSemHandle,portMAX_DELAY);
	printf("High task running!\r\n");
	HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_1);
	xSemaphoreGive(MutexSemHandle);
	vTaskDelay(500);
  }
}
/******************MiddleTask***********************/
void MiddleTask(void const * argument){
  for(;;){
    printf("Middle task running!\r\n");
	HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);
	vTaskDelay(1000);
  }
}
/******************LowTask**************************/
void LowTask(void const * argument){
  for(;;){
    xSemaphoreTake(MutexSemHandle,portMAX_DELAY);
	printf("Low task running!\r\n");
	for(int i=0;i<20000000;i++){
	  taskYIELD();			
	}		
	xSemaphoreGive(MutexSemHandle);
	vTaskDelay(1000);
  }
}
4.3 下载验证

编译无误下载到开发板后,打开串口调试助手,串口输出如下图示的调试信息:

  • 由于High_Task任务延时500ms,因此Middle_Task最先开始运行;
  • 之后Low_Task运行,获取互斥信号;
  • High_Task开始运行,阻塞在请求互斥信号量这里,等待Low_Task释放互斥信号量
  • 此时由于Low_Task正在使用互斥信号量,因此Low_Task的优先级暂时提升到了与High_Task相同的优先级,所以Middle_Task无法打断Low_Task的运行。Low_Task运行完后,释放互斥信号量,High_Task获取互斥信号量并运行

由此可见,互斥信号量有效的解决了优先级翻转的问题

FreeRTOS系列|互斥信号量_mutex_05

关注我的公众号,在公众号里发如下消息,即可获取相应的工程源代码:

FreeRTOS互斥信号量实例

FreeRTOS系列|互斥信号量_mutex_06