freertos源码分析DAY3(二值/计数信号量)

         信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资
源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。( 在多任务系统中,各任务之间需要同步或使用互斥实现来临界资源的保护,信号量可以为用户提供这方面功能的支持
     
          抽象的来说可以将信号量看为一个标志位,它是一个非负数整数,所有获取它的任务都会将该整数-1( 对应消息队列的出队情况 )。当其减到0时,所有获取它的任务都会被阻塞( 对应消息队列中成员出队,但队列中已无队列成员可以出队的情况 )。而当有任务释放它时,就会将该整数+1( 对应消息队列入队情况,但和标准的消息队列入队还是有些区别( 信号量在队列满的其情况下,入队(释放信号量),也不会将当前任务阻塞 )。
       
        这节介绍的信号量的作用主要是用于同步,所以这里将其称为 “同步信号量” ,在freertos中功能偏向于同步的信号量主要有“二值信号量”以及“计数信号量”,这里首先介绍二值信号量;

1. 二值信号量

        二值信号量可以看作是一个二值的标志位,它主要被用于进行任务之间的同步。

1.1. 二值信号量的创建

        二值信号量的创建,具体代码如下所示:(此代码定义在semphc.h文件中

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
#endif

        可以看到它其实就是调用的消息队列创建函数,只不过这里创建了一个特殊的消息队列,它的队长为1,单个空间大小为0(也就是无需队列空间),而最后一个参数并无实质性作用,只是作为标记区分我创建的是上面类型的消息队列;(具体创建的源码过程在上节“消息队列”有详细分析,这里不再做分析

        二值信号量创建出来具体如下所示:(可以看到其并无队列空间

        

信号量被创建后,uxMessagesWaiting变量为默认0(信号量创建后,默认情况下是无可用资源的),若想要其在默认情况下有可用资源则需调用如下函数:(它同样是在头文件semphc中定义

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define vSemaphoreCreateBinary( xSemaphore )																							\
		{																																	\
			( xSemaphore ) = xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE );	\
			if( ( xSemaphore ) != NULL )																									\
			{																																\
				( void ) xSemaphoreGive( ( xSemaphore ) );																					\
			}																														\
		}
#endif

 可以看到,这个API在信号量被创建后,立即释放了一个资源。

1.2 任务中二值信号操作函数

1.2.1 二值信号量的释放

二值信号量释放函数具体如下所示:

#define xSemaphoreGive( xSemaphore )		xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

可以看到它实际调用的是 “消息队列通用发送” 函数xQueueGenericSend这里首先看其参数:

1、其将信号量队列传入其中,但无要发送到队列中的对象(无消息发送到队列);

2、阻塞时间为0(若信号量队列已有资源,此时有任务再次释放信号量则这个任务不会被阻塞);

3、消息插入到队列的方式(这里其实哪种方式都行,因为根本没有消息入队,信号量只是用了队列中的消息个数的统计变量作为其有无资源的指示项);

这里只对信号量的释放过程做一下简单的说明(通用发送函数发送消息的详细过程在 “消息队列” 那里已经分析过了),所以这里只做简单分析:

1、进入通用发送消息函数后,其首先会通过统计消息个数变量判断,消息队列是否满;

   (1)满的话就直接退出函数,不阻塞任务;(因为信号量给的阻塞值是0

   (2)不满的话就继续进行如下操作:

2、若此时信号量队列无资源(队列未满情况),以FIFO的方式将消息插入到队列中,由于信号量的消息队列大小为0,所以其单个空间大小为0,而消息插入到队列后入队指针会向后移动“单个空间大小的位置”(这里的入队指针就没移动位置,因为单个空间大小为0),但记录消息个数的变量uxMessagesWaiting会+1,表示当前信号量有资源了,可以被拿走;

3、信号量资源被释放后,其会查找当前信号量消息等待接收队列中是否有任务,若有任务则将它们恢复就绪(这就实现了任务同步,只有有资源时,才会将相应的任务唤醒并执行);

4、信号量完成资源释放操作后,就会退出当前信号量释放函数;(之后等待任务调度到等待这个信号量的任务,此时就使用信号量完成了一次任务间的同步的操作

        

1.2.2 等待二值信号量资源函数

  等待二值信号量函数具体如下所示:

#define xSemaphoreTake( xSemaphore, xBlockTime )		xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )

不难发现等待二值信号量函数的底层调用的是通用接收消息函数,同样这里首先看其参数:

1、传入前面创建了的信号量队列;

2、无变量接收此消息;

3、阻塞时间可通过宏自行设定(为了实现真正的任务同步,一般设置为最大0xFFFFFFFFUL,表永久阻塞);

4、 false:要做具体的队列操作,而不是只读消息队列中的消息;(队列操作会让uxMessagesWaiting变量--,所以这里必须要进行队列操作,否则不能做将信号量取走的操作

信号量接收等待函数,这里同样只做简略介绍,因为队列通用发送消息函数在前面已经向详细分析过了:

1、首先判断消息队列是否有消息(是否有资源):

        (1)无资源,阻塞当前任务,直到等待的信号量被释放,当前任务才会被就绪;

        (2)有资源则做如下操作:

2、 此时信号量队列是有资源的,进行消息出队操作(出队指针首先会向后移动uxItemSize大小的位置,之后其会从其指向的队列空间中的拷贝消息到相应的消息队列空间,而uxItemSize在信号量中是0,所以这里指针并没有移动,也并没有消息出队);

3、进行出队操作后,会将uxMessagesWaiting-1,表示当前信号量队列中的消息,被任务拿走了一个;

4、之后其会检查消息队列的发送延时列表,看有没有要发送消息的任务(由于信号量释放函数中,设置的延时时间是0,所以信号量的发送延时列表中并不会有任务);

1.3 中断中二值信号量操作函数

1.3.1 中断中释放二值信号量

中断中释放二值信号量的API具体如下所示:

#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )	xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )

可以看到这个API的底层调用的并不是 “消息队列在中断中发送消息函数” ,它用了一个新的底层实现xQueueGiveFromISR,具体如下:(此函数和中断中发送消息函数的区别是,这个函数没有入队操作

BaseType_t xQueueGiveFromISR( QueueHandle_t xQueue, BaseType_t * const pxHigherPriorityTaskWoken )
{
    BaseType_t xReturn;
    UBaseType_t uxSavedInterruptStatus;
    Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
	{
		const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;

		if( uxMessagesWaiting < pxQueue->uxLength )
		{
			const int8_t cTxLock = pxQueue->cTxLock;

			pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;

			if( cTxLock == queueUNLOCKED )
			{
				#if ( configUSE_QUEUE_SETS == 1 )
				{
				  //xxxxxxxxxxxxxxxxx
				}
				#else
				{
					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
					{
						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
						{
							if( pxHigherPriorityTaskWoken != NULL )
							{
								*pxHigherPriorityTaskWoken = pdTRUE;
							}
						}
					}
				}
				#endif 
			}
			else
			{
				pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );
			}
			xReturn = pdPASS;
		}
		else
		{
			xReturn = errQUEUE_FULL;
		}
	}
	portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

	return xReturn;
}

参数:xQueue(需要释放的信号量队列

           pxHigherPriorityTaskWoken (任务切换指示变量


xQueueGiveFromISR函数具体做了如下操作:(以下操作都是在临界段中进行的

1、首先其会判断当前信号量队列是否已满;

        (1)满就直接退出函数,返回队列满错误;

        (2)不满就进行如下操作:(信号量队列还能接收资源

2、将当前队列消息个数+1后,判断当前信号量队列是否被锁;(信号量队列被上锁的情况,就只有任务等待信号量被挂载到接受等待阻塞链表时才会将其上锁,发送情况下是不可能被上锁的,因为信号量的发送任务是不会阻塞任务的

         (1)被上锁:将上锁标志+1,之后中断结束后,会回到通用消息接收函数中,将队列解锁,在解锁过程就会将中断中错过恢复的任务,都恢复就绪;

         (2)没被上锁:检查消息接收阻塞队列中是否有任务,若有则将其恢复,之后再判断当前恢复任务的优先级是否大于当前运行任务,若是则将指导任务切换调度的变量置1;

3、退出临界段并恢复到进入中断前的basepri寄存器配置;(退出此函数后,中断中会通过任务切换指导变量去指导是否需要任务切换

此函数的具体用法如下所示:

SemaphoreHandle_t   xSem_Binary; //定义一个信号量

xSem_Binary = xSemaphoreCreateBinary(); //创建二值信号量

//某某中断中
void xxx_Handler(void)
{
    BaseType_t task_switch = false;
    xSemaphoreGiveFromISR( xSem_Binary, &task_switch)
    if(task_switch == ture)
    {
        portYIELD_FROM_ISR();
    }
}

总结:

        此函数和xQueueGenericSendFromISR唯一的区别就是,xQueueGiveFromISR中没有进行入队操作,它将入队操作直接换为了对uxMessagesWaiting的++;

1.3.2 中断中接收信号量

中断中等待二值信号量的API具体如下所示:(底层实际调用的是xQueueReceiveFromISR中断中接收消息函数

#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken )	xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )

先看其传入的参数:

xSemaphore :传入当前要接收资源的信号量队列;

NULL:没有要接收消息的对象;

pxHigherPriorityTaskWoken :任务切换标志指示;(这里指示的是发送延时列表中恢复的任务,若发送延时列表中恢复的任务优先级>当前运行任务,则这位置1,表现在需要进行一次任务切换,但其实这个参数永远不会变为1,因为信号量发送任务不会阻塞任务

中断中接收信号量这个函数用的很少,看了源码后也感觉这个函数没什么用处,因为信号量的所有发送API压根就不会阻塞信号量发送任务(发送不了就下个周期再发),所以这个函数后面的指示参数压根就不会为1,这个函数有点奇葩。

2.  计数信号量

        计数信号量可以被认为长度大于 1 的队列,信号量使用者依然不必关心存储在队列中的消息,只需关心队列是否有消息即可。
        在实际让使用中,常将计数信号量用于事件计数( 记录事件发生次数 )与资源管理。
计数信号量用于事件计数时:
        当某个事件发生时,任务或者中断将释放一个信号量( 信号量计数值+1 ),当处理被事件时( 一般在任务中处理 ),处理任务会取走该信号量( 信号量计数值- 1 ),信号量的计数值则表示还有多少个事件没被处理。

        

计数信号量用于资源管理时:

        信号量的计数值表示系统中可用的资源数目,任务必须先获取到信号量才能获取资源访问权,当信号量的计数值为零时表示系统没有可用的资源,但是要注意,在使用完资源的时候必须归还信号量,否则当计数值为 0 的时候任务就无法访问该资源了。(计数值大小,为系统可访问资源的次数)

2.1 计数信号量的创建

        创建计数信号量的API具体如下所示:

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
#endif

        可以看到,创建计数信号量的底层函数,并不是调用的消息队列创建函数,而是一个新的实现xQueueCreateCountingSemaphore(本质还是调用了xQueueGenericCreate,只是加了几个安全性操作),具体如下:

#if( ( configUSE_COUNTING_SEMAPHORES == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )

	QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount, const UBaseType_t uxInitialCount )
	{
		QueueHandle_t xHandle;

		xHandle = xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE );

		if( xHandle != NULL )
		{
			( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;
		}
		return xHandle;
	}

#endif

参数:

uxMaxCount:   要创建计数信号量队列的长度;

uxInitialCount :计数信号量队列初始化的长度;


xQueueCreateCountingSemaphore具体做了一下操作:

1、首先此函数调用了xQueueGenericCreate通用消息队列创建函数,并将创建好的队列句柄返回给了临时变量xHandle;

2、创建好的队列,将其队列中的消息个数初始化为传入形参的大小,最后返回队列句柄;


总结:

        此函数底层就是调用了消息队列创建函数,与二值信号量创建不同的一点就是,它的队列长度>=1,且被初始了消息个数;

        计数信号量的其他操作均和二值信号量相同(信号量的释放/接受),只不过它的队列长度可能要长一些。所以在消耗计数信号量时,计数信号量就不会被一次性消耗完毕,这个特性也造就了计数信号量可以被作为 “资源管理工具/用于事件计数” 来使用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值