FreeRTOS源码分析-9 互斥信号量

这篇具有很好参考价值的文章主要介绍了FreeRTOS源码分析-9 互斥信号量。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

1 优先级翻转问题

2 互斥信号量概念及其应用

2.2FreeRTOS互斥信号量介绍

2.3FreeRTOS互斥信号量工作原理

3 互斥信号量函数应用

3.1功能分析

3.2API详解

3.3功能实现

4 递归互斥信号量函数应用

4.1死锁现象

​编辑

4.2API详解

4.3解决死锁

5 互斥信号量实现原理

5.1互斥信号量创建

5.2互斥信号量获取&释放

5.3优先级继承原理

5.4递归互斥信号量获取&释放


1 优先级翻转问题

二值信号量中一个bug

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

低优先级任务可以被高优先级任务抢占,但是如果这个时候低优先级任务占用信号量,那么高优先级任务会阻塞。这时候中优先级抢占了低优先级的任务,再等低优先级任务运行完释放信号量,这时候高优先级任务才运行。

上述情况中优先级任务抢占了高优先级任务,系统会出现问题,保证不了了任务的实时性。

功能需求

  • 新建三个任务,优先级分别为中高低
  • 新建二值信号量,用于模拟优先级翻转
  • 低优先级任务获取信号量后,被中优先级打断,中优先级任务执行时间较长,因为低优先级任务还未释放信号量,高优先级任务就无法获取信号量继续运行

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

使用接口在低优先级任务中模拟调度 ,内部其实就是PendSV

//创建二值信号量
osSemaphoreDef(PrBinarySem);
PrBinarySemHandle = osSemaphoreCreate(osSemaphore(PrBinarySem), 1);

//创建low、nomal、high任务

  osThreadDef(DelayTask, Delay_Task, osPriorityLow, 0, 128);
  DelayTaskHandle = osThreadCreate(osThread(DelayTask), NULL);

  osThreadDef(LedTask, Led_Task, osPriorityNormal, 0, 128);
  LedTaskHandle = osThreadCreate(osThread(LedTask), NULL);

  osThreadDef(HighTask, High_Task, osPriorityHigh, 0, 128);
  HighTaskHandle = osThreadCreate(osThread(HighTask), NULL);

uint32_t

void Delay_Task(void const * argument)
{
  for(;;)
  {
	printf("Low Task Take Sem\r\n");  
	if(xSemaphoreTake(PrBinarySemHandle,portMAX_DELAY) == pdPASS)
    {
	    printf("Low Task is Runing\r\n");
	}
    //为了保证中优先级任务有足够时间抢占抢占
	for(i=0;i<2000000;i++){
		taskYIELD();
	}
	printf("Low Task Give Sem\r\n");  
	xSemaphoreGive(PrBinarySemHandle);
    osDelay(500);
  }
}

void Led_Task(void const * argument)
{

  for(;;)
  {
	printf("Normal Task is Runing\r\n");
    osDelay(500);
  }
}

void High_Task(void const * argument)
{
  for(;;)
  {
	printf("High Task Take Sem\r\n");  
	if(xSemaphoreTake(PrBinarySemHandle,portMAX_DELAY) == pdPASS)
    {
	    printf("High Task is Runing\r\n");
	}
	printf("High Task Give Sem\r\n");  
	xSemaphoreGive(PrBinarySemHandle);
    osDelay(500);
  }
}

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

2 互斥信号量概念及其应用

2.1互斥信号量概念

用于解决优先级反转问题

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

当高优先级任务运行时,如果信号量被低优先级任务获取,那么临时提高低优先级任务。

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

 但是二值信号量是共有的,所有任务都可以获取。 

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

互斥信号量特性:

  • 优先级继承
  • 任务独享公共资源

2.2FreeRTOS互斥信号量介绍

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

Mutex 互斥信号量

RecursiveMuxtex 互斥信号量(解决普通信号量的死锁问题)

2.3FreeRTOS互斥信号量工作原理

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机 互斥信号量工作原理即特性

  • 优先级继承
  • 任务独享公共资源

 FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

上述伪代码,再调用bar申请了互斥信号,当再去调用foo的时候又去申请了互斥信号量,但是bar的互斥信号量还未解锁,造成了死锁。即如果调用2次即死锁。

递归信号量即每次lock一次+1,每次解锁-1,解决死锁问题。当lock次数为0,即恢复原有状态。

3 互斥信号量函数应用

3.1功能分析

  • 1、修改优先级翻转实验
  • 2、使用互斥信号量,解决优先级翻转问题

3.2API详解

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

GetmutexHolder是独有的api,查看当前是谁独占了资源。如果没有被占用,我们即可获取信号量,使用take(take和give都是标准的api)

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

3.3功能实现

使能互斥锁

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

 FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

生产工程代码

void MX_FREERTOS_Init(void) {

  osMutexDef(PrMutex);
  PrMutexHandle = osMutexCreate(osMutex(PrMutex));
    
  //略 ...

}

void Led_Task(void const * argument)
{
  for(;;)
  {
	printf("Normal Task is Runing\r\n");
    osDelay(500);
  }
}


void Delay_Task(void const * argument)
{
  for(;;)
  {
	printf("Low Task Take Mutex\r\n");  
	 if(xSemaphoreTake(PrMutexHandle,portMAX_DELAY) == pdPASS){
		printf("Low Task is Runing\r\n");
	 }
	 for(i=0;i<2000000;i++){
	 	 taskYIELD();
	 }
	 printf("Low Task Give Mutex\r\n");  
	 xSemaphoreGive(PrMutexHandle);
    osDelay(500);
  }
}


void High_Task(void const * argument)
{
  for(;;)
  {
	printf("High Task Take Mutex\r\n");  
    //高优先级任务获取互斥锁
	if(xSemaphoreTake(PrMutexHandle,portMAX_DELAY) == pdPASS)
    {
	    printf("High Task is Runing\r\n");
	}
	printf("High Task Give Mutex\r\n");  
	xSemaphoreGive(PrMutexHandle);
    osDelay(500);
  }
}

效果

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

4 递归互斥信号量函数应用

4.1死锁现象

普通信号量只能获取一次,修改一下高优先级任务。

void High_Task(void const * argument)
{
  for(;;)
  {
        printf("High Task Take RecursiveMutex 1\r\n");  
        if(xSemaphoreTake(PrMutexHandle,portMAX_DELAY) == pdPASS)
		{
			printf("High Task is Runing\r\n");
        }
        //再次获取信号量
        if(xSemaphoreTake(PrMutexHandle,portMAX_DELAY) == pdPASS)
		{
			printf("High Task is Runing\r\n");
        }
		printf("High Task Give RecursiveMutex 2\r\n");  
		xSemaphoreGiveRecursive(myRecursiveMutexHandle);
        osDelay(500);
  }
}

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

4.2API详解

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

获取接口(不能再临界区内调用,即模拟的中断中)

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

释放接口(FAIL,代表持有者不是任务本身)

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

4.3解决死锁

  • 1、模拟死锁现象
  • 2、使用递归互斥信号量解决死锁问题

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

 FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

osMutexId myRecursiveMutexHandle;

void MX_FREERTOS_Init(void) 
{

  osMutexDef(myRecursiveMutex);
  myRecursiveMutexHandle = osRecursiveMutexCreate(osMutex(myRecursiveMutex));
  //略 ...
}

void Led_Task(void const * argument)
{
    for(;;)
    {
        printf("Normal Task is Runing\r\n");
        osDelay(500);//1msʱ»ù
    }
}

void High_Task(void const * argument)
{

    for(;;)
    {
        printf("High Task Take RecursiveMutex 1\r\n");  
        if(xSemaphoreTakeRecursive(myRecursiveMutexHandle,portMAX_DELAY) == pdPASS)
		{
			printf("High Task is Runing\r\n");
        }
		printf("High Task Take RecursiveMutex 2\r\n");  
		if(xSemaphoreTakeRecursive(myRecursiveMutexHandle,portMAX_DELAY) == pdPASS)
        {
			printf("High Task is Runing\r\n");
		}
		printf("High Task Give RecursiveMutex 1\r\n");  
		xSemaphoreGiveRecursive(myRecursiveMutexHandle);
		printf("High Task Give RecursiveMutex 2\r\n");  
		xSemaphoreGiveRecursive(myRecursiveMutexHandle);
        osDelay(500);
  }
}

void Delay_Task(void const * argument)
{
    for(;;)
    {
        printf("Low Task Take RecursiveMutex\r\n");  
        if(xSemaphoreTakeRecursive(myRecursiveMutexHandle,portMAX_DELAY) == pdPASS){
            printf("Low Task is Runing\r\n");
        }
        for(i=0;i<2000000;i++)
        {
            taskYIELD();
	     }
	     printf("Low Task Give RecursiveMutex\r\n");  
	     xSemaphoreGiveRecursive(myRecursiveMutexHandle);
         osDelay(500);
    }
}

 效果

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

5 互斥信号量实现原理

5.1互斥信号量创建

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define xSemaphoreCreateMutex() 
	
	xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
	||||
		
#endif

	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;
	}
	
	static void prvInitialiseMutex( Queue_t *pxNewQueue )
	{
		if( pxNewQueue != NULL )
		{
			/*
				1、信号的持有者为空
				2、消息队列的类型为互斥信号量
				3、递归记录初始为0
				4、往消息队列发送一个消息->其实赋值互斥信号量为1
			*/
			pxNewQueue->pxMutexHolder = NULL;
			pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;

			/* In case this is a recursive mutex. */
			pxNewQueue->u.uxRecursiveCallCount = 0;

			traceCREATE_MUTEX( pxNewQueue );

			/* Start with the semaphore in the expected state. */
			( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK );
		}
		else
		{
			traceCREATE_MUTEX_FAILED();
		}
	}

5.2互斥信号量获取&释放

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

#define xSemaphoreTake( xSemaphore, xBlockTime )		
	xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )
	/*
		队列不为空处理>0
		1、判断是否为互斥信号量
		2、记录当前任务为信号持有者
	*/
	#if ( configUSE_MUTEXES == 1 )
					{
						if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
						{
							/* Record the information required to implement
							priority inheritance should it become necessary. */
							pxQueue->pxMutexHolder = ( int8_t * ) pvTaskIncrementMutexHeldCount(); /*lint !e961 Cast is not redundant as TaskHandle_t is a typedef. */
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
	/*
		队列为空处理==0
		1、判断是否为互斥信号量
		2、进入临界段
		3、优先级继承
	*/				
	#if ( configUSE_MUTEXES == 1 )
				{
					if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
					{
						taskENTER_CRITICAL();
						{
							vTaskPriorityInherit( ( void * ) pxQueue->pxMutexHolder );
						}
						taskEXIT_CRITICAL();
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				#endif

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

#define xSemaphoreGive( xSemaphore )		
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
	//互斥信号量处理在数据拷贝接口中
	xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
	
	/*
		1、判断是否为互斥信号量
		2、恢复任务优先级
		3、信号持有者赋值为空,也就是说其他任务可以获取了
	
	*/
		#if ( configUSE_MUTEXES == 1 )
		{
			if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
			{
				/* The mutex is no longer being held. */
				xReturn = xTaskPriorityDisinherit( ( void * ) pxQueue->pxMutexHolder );
				pxQueue->pxMutexHolder = NULL;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif /* configUSE_MUTEXES */

5.3优先级继承原理

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

//持有者记录增加
#if ( configUSE_MUTEXES == 1 )

	void *pvTaskIncrementMutexHeldCount( void )
	{
		/* If xSemaphoreCreateMutex() is called before any tasks have been created
		then pxCurrentTCB will be NULL. */
		if( pxCurrentTCB != NULL )
		{
			//持有者任务控制块里 持有记录加一
			( pxCurrentTCB->uxMutexesHeld )++;
		}
		//返回当前任务控制块
		return pxCurrentTCB;
	}

#endif /* configUSE_MUTEXES */


//优先级继承
//参数:持有互斥信号量的任务控制块
#if ( configUSE_MUTEXES == 1 )

	void vTaskPriorityInherit( TaskHandle_t const pxMutexHolder )
	{
	
	TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;
		//1、任务控制块不为空
		if( pxMutexHolder != NULL )
		{
			//2、优先级小于当前获取信号量的有限,才会去处理继承
			if( pxTCB->uxPriority < pxCurrentTCB->uxPriority )
			{
				/*  */
				if( ( listGET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ) ) & taskEVENT_LIST_ITEM_VALUE_IN_USE ) == 0UL )
				{
					//3、修改持有者事件列表中,列表项的属性值 为当前任务优先级
					listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxCurrentTCB->uxPriority ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}

				/* 
					4、判断持有者任务是否在就绪列表中 
						4.1、移除
						4.2、修改任务优先级,这个修改是任务控制块里的信息
						4.3、添加到新的就绪列表中
				
				*/
				if( listIS_CONTAINED_WITHIN( &( pxReadyTasksLists[ pxTCB->uxPriority ] ), &( pxTCB->xStateListItem ) ) != pdFALSE )
				{
					if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
					{
						taskRESET_READY_PRIORITY( pxTCB->uxPriority );
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					/* Inherit the priority before being moved into the new list. */
					pxTCB->uxPriority = pxCurrentTCB->uxPriority;
					prvAddTaskToReadyList( pxTCB );
				}
				else
				{
					/* Just inherit the priority. */
					pxTCB->uxPriority = pxCurrentTCB->uxPriority;
				}

				traceTASK_PRIORITY_INHERIT( pxTCB, pxCurrentTCB->uxPriority );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}

#endif /* configUSE_MUTEXES */


//优先级恢复
/*
	1、
*/
BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder )
	{
	TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;
	BaseType_t xReturn = pdFALSE;

		if( pxMutexHolder != NULL )
		{
			//1、持有者任务持有记录减一
			( pxTCB->uxMutexesHeld )--;

			/* Has the holder of the mutex inherited the priority of another
			task? */
			//2、优先级是否修改过
			if( pxTCB->uxPriority != pxTCB->uxBasePriority )
			{
				//3、递归记录为0的时候
				/* Only disinherit if no other mutexes are held. */
				if( pxTCB->uxMutexesHeld == ( UBaseType_t ) 0 )
				{
					/* 4、从当前就绪列表中移除*/
					if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
					{
						taskRESET_READY_PRIORITY( pxTCB->uxPriority );
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					/* 5、恢复任务优先级 */
					traceTASK_PRIORITY_DISINHERIT( pxTCB, pxTCB->uxBasePriority );
					pxTCB->uxPriority = pxTCB->uxBasePriority;

					/*6、已经不是持有者,把任务添加到新的就绪列表中去*/
					listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxTCB->uxPriority ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
					prvAddTaskToReadyList( pxTCB );

					/* 触发上下文切换,释放CPU使用权 */
					xReturn = pdTRUE;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		return xReturn;
	}

5.4递归互斥信号量获取&释放

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机

#define xSemaphoreGiveRecursive( xMutex )	
xQueueGiveMutexRecursive( ( xMutex ) )

/*
	参数:
		信号量句柄
	步骤:
		1、判断当前任务是否为持有者
			1.1、递归记录减一
			1.2、判断记录是否为0
			1.3、发送一个消息
		2、不为持有者返回错误

*/
BaseType_t xQueueGiveMutexRecursive( QueueHandle_t xMutex )
	{
	BaseType_t xReturn;
	Queue_t * const pxMutex = ( Queue_t * ) xMutex;

	
		if( pxMutex->pxMutexHolder == ( void * ) xTaskGetCurrentTaskHandle() ) /*lint !e961 Not a redundant cast as TaskHandle_t is a typedef. */
		{
			( pxMutex->u.uxRecursiveCallCount )--;

			
			if( pxMutex->u.uxRecursiveCallCount == ( UBaseType_t ) 0 )
			{
				( void ) xQueueGenericSend( pxMutex, NULL, queueMUTEX_GIVE_BLOCK_TIME, queueSEND_TO_BACK );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}

			xReturn = pdPASS;
		}
		else
		{
			
			xReturn = pdFAIL;
		}

		return xReturn;
	}

FreeRTOS源码分析-9 互斥信号量,FreeRTOS源码分析,stm32,物联网,单片机文章来源地址https://www.toymoban.com/news/detail-630137.html

#if( configUSE_RECURSIVE_MUTEXES == 1 )
	#define xSemaphoreTakeRecursive( xMutex, xBlockTime )
	xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )
#endif

/*
	参数:互斥信号句柄,超时等待时间
	步骤:
		1、判断是否为持有者
			1.1、递归记录加一
			1.2、返回成功
		2、不为持有者
			2.1、接收消息---获取信号量
			2.2、获取成功----递归记录加一
			2.3、获取失败---- 返回失败

*/
BaseType_t xQueueTakeMutexRecursive( QueueHandle_t xMutex, TickType_t xTicksToWait )
	{
	BaseType_t xReturn;
	Queue_t * const pxMutex = ( Queue_t * ) xMutex;



		if( pxMutex->pxMutexHolder == ( void * ) xTaskGetCurrentTaskHandle() ) /*lint !e961 Cast is not redundant as TaskHandle_t is a typedef. */
		{
			( pxMutex->u.uxRecursiveCallCount )++;
			xReturn = pdPASS;
		}
		else
		{
			xReturn = xQueueGenericReceive( pxMutex, NULL, xTicksToWait, pdFALSE );

			/* pdPASS will only be returned if the mutex was successfully
			obtained.  The calling task may have entered the Blocked state
			before reaching here. */
			if( xReturn != pdFAIL )
			{
				( pxMutex->u.uxRecursiveCallCount )++;
			}
			else
			{
				traceTAKE_MUTEX_RECURSIVE_FAILED( pxMutex );
			}
		}

		return xReturn;
	}

到了这里,关于FreeRTOS源码分析-9 互斥信号量的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包