freeRTOS(3)信号量及API分析

本文深入介绍了信号量在操作系统中的应用,包括二值信号量、计数型信号量和互斥信号量。二值信号量用于简单的互斥访问或同步,计数型信号量适用于事件计数和资源管理,而互斥信号量带有优先级继承机制,适用于保护资源的互斥访问。创建、释放和获取信号量的API函数如xSemaphoreCreateBinary()、xSemaphoreGive()和xSemaphoreTake()等在文中进行了详细阐述。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

信号量简介

信号量常常用于控制对共享资源的访问任务同步
共享资源访问:两个例子
(计数型信号量)公共停车场100个车位,当有车开出停车场的时候停车数量就会减一,也就是说信号量减一,此时你就可以把车
停进去了,你把车停进去以后停车数量就会加一,也就是信号量加一。
(二值信号量)使用公共电话,我们知道一次只能一个人使用电话,这个时候公共电话就只可能有两个状态:
使用或未使用,如果用电话的这两个状态作为信号量的话,那么这个就是二值信号量。

任务同步:用于任务与任务或中断与任务之间的同步。
举例:当中断发生的时候就释放信号量,中断服务函数不做具体的处理。具体的处理过程做成一个任务,这个任务会获取信号量,如果获取到信号量就说明中断发生了,那么就开始完成相应的处理,这样做的好处就是中断执行时间非常短。

二值信号量

二值信号量通常用于互斥访问或同步, 二值信号量和互斥信号量非常类似,但是还是有一些细微的差别, 互斥信号量拥有优先级继承机制, 二值信号量没有优先级继承。 因此二值信号另更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适合用于简单的互斥访问。
二值信号量其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空的。

创建信号量 API 函数

  • xSemaphoreCreateBinary()
    // 动态创建,使用此函数创建二值信号量的话信号量所需要的 RAM 是由 FreeRTOS 的内存管理部分来动态分配的
    此函数创建好的二值信号量默认是空的,也就是说刚创建好的二值信号量使用函数 xSemaphoreTake()是获取不到的,此函数也是个宏, 具体创建过程是由函数 xQueueGenericCreate() 来完成的, 函数原型如下:
#define xSemaphoreCreateBinary() 				\
		xQueueGenericCreate( ( UBaseType_t ) 1, \
		semSEMAPHORE_QUEUE_ITEM_LENGTH, 		\
		queueQUEUE_TYPE_BINARY_SEMAPHORE ) 

二值信号量创建函数也是使用函数 xQueueGenericCreate()来创建一个类型为 queueQUEUE_TYPE_BINARY_SEMAPHORE、长度为 1、队列项长度为 0 的队列。在成功创建二值信号量以后不调用函数 xSemaphoreGive() 释放二值信号量。 也就是说新版函数创建的二值信号量默认是无效的。

  • xSemaphoreCreateBinaryStatic()
    // 静态创建,使用此函数创建二值信号量的话信号量所需要的RAM 需要由用户来分配
    此函数是个宏,具体创建过程是通过函数 xQueueGenericCreateStatic()来完成的,函数原型如下:
    SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer )
    参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。

释放信号量API函数

  • xSemaphoreGive()// 任务级信号量释放函数
    此函数用于释放二值信号量、计数型信号量或互斥信号量, 此函数是一个宏,真正释放信号量的过程是由函数 xQueueGenericSend() 来完成的, 函数原型如下: BaseType_t xSemaphoreGive( xSemaphore )
    xSemaphore: 要释放的信号量句柄
#define xSemaphoreGive( xSemaphore ) \
		xQueueGenericSend( (QueueHandle_t) (xSemaphore), \
												  NULL,  \
									semGIVE_BLOCK_TIME,  \
                                     queueSEND_TO_BACK   ) 

可以看出任务级释放信号量就是向队列发送消息的过程,只是这里并没有发送具体的消息,阻塞时间为 0 ( 宏 semGIVE_BLOCK_TIME 为 0),入队方式采用的后向入队。入队的时候队列结构体成员变量 uxMessagesWaiting 会加一,对于二值信号量通过判断 uxMessagesWaiting 就可以知道信号量是否有效了,当 uxMessagesWaiting 为 1 的话说明二值信号量有效

  • xSemaphoreGiveFromISR()// 中断级信号量释放函数
    此函数只能用来释放二值信号量和计数型信号量,绝对不能用来在中断服务函数中释放互斥信号量! 此函数是一个宏,真正执行的是函数xQueueGiveFromISR(), 此函数和中断级通用入队函数 xQueueGenericSendFromISR() 类似(函数在队列章节做了详细分析)
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
								  BaseType_t * pxHigherPriorityTaskWoken )
// xSemaphore: 			   要释放的信号量句柄
// pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置的,用户不用进行设置,用户		   
//                             只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要
//							   进行一次任务切换。

获取信号量API函数

  • xSemaphoreTake()
    此函数用于获取二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正获取信号量的过程是由函数 xQueueGenericReceive () 来完成的,函数原型如下:
/*参数:xSemaphore: 要获取的信号量句柄  xBlockTime:阻塞时间 */
#define xSemaphoreTake( xSemaphore, xBlockTime ) \
		xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )

获取信号量的过程其实就是读取队列的过程,只是这里并不是为了读取队列中的消息。在队列章节讲解函数 xQueueGenericReceive() 的时候说过如果队列为空并且阻塞时间为 0 的话就立即返回 errQUEUE_EMPTY,表示队列满。如果队列为空并且阻塞时间不为 0 的话就将任务添加到延时列表中。 如果队列不为空的话就从队列中读取数据(获取信号量不执行这一步),数据读取完成以后还需要将队列结构体成员变量 uxMessagesWaiting 减一, 然后解除某些因为入队而阻塞的任务,最后返回 pdPASS 表示出队成功。

  • xSemaphoreTakeFromISR ()
    此函数用于在中断服务函数中获取信号量, 此函数用于获取二值信号量和计数型信号量,绝对不能使用此函数来获取互斥信号量!真正执行的是函数 xQueueReceiveFromISR (),此函数原型如下:
    BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, BaseType_t * pxHigherPriorityTaskWoken)
    xSemaphore: 要获取的信号量句柄。
    pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。

在中断中获取信号量真正使用的是函数 xQueueReceiveFromISR (),这个函数就是中断级出队函数。 当队列不为空的时候就拷贝队列中的数据(用于信号量的时候不需要这一步),然后将队列结构体中的成员变量 uxMessagesWaiting 减一,如果有任务因为入队而阻塞的话就解除阻塞态,当解除阻塞的任务拥有更高优先级的话就将参数 pxHigherPriorityTaskWoken 设置为pdTRUE,最后返回 pdPASS 表示出队成功。 如果队列为空的话就直接返回 pdFAIL 表示出队失败。

计数型信号量

计数型信号量通常用于如下两个场合:
1、事件计数
在这个场合中, 每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数值),其他任务会获取信号量(信号量计数值减一,信号量值就是队列结构体成员变量 uxMessagesWaiting) 来处理事件。在这种场合中创建的计数型信号量初始计数值为0。
2、资源管理在这个场合中,信号量值代表当前资源的可用数量,比如停车场当前剩余的停车位数量。一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减一。当信号量值为 0 的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量值会加一。在这个场合中创建的计数型信号量初始值应该是资源的数量,比如停车场一共有 100 个停车位,那么创建信号量的时候信号量值就应该初始化为 100。

创建计数型信号量 API

  • xSemaphoreCreateCounting()
    使用动态方法创建计数型信号量,此函数的本质是一个宏,真正完成信号量创建的是函数 xQueueCreateCountingSemaphore()
    函数的原型如下:
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount)

uxMaxCount: 计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。
uxInitialCount: 计数信号量初始值

找到函数的定义如下:

QueueHandle_t xQueueCreateCountingSemaphore(const UBaseType_t uxMaxCount,const UBaseType_t uxInitialCount)
{
	QueueHandle_t xHandle;
	configASSERT( uxMaxCount != 0 );
	configASSERT( uxInitialCount <= uxMaxCount );
	xHandle = xQueueGenericCreate(uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH,
											  queueQUEUE_TYPE_COUNTING_SEMAPHORE );
	if( xHandle != NULL) {
		( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;
		traceCREATE_COUNTING_SEMAPHORE();
	}
	else {
			traceCREATE_COUNTING_SEMAPHORE_FAILED();
	} return xHandle;}

计数型信号量的创建过程实质也是一个队列,所以首先调用了一个队列的创建函数,队列的长度是 uxMaxCount,队列项长度为 queueSEMAPHORE_QUEUE_ITEM_LENGTH,队列的类型为 queueQUEUE_TYPE_COUNTING_SEMAPHORE,表示是个计数型信号量。队列结构体的成员变量 uxMessagesWaiting 用来计数型信号量的计数。

  • xSemaphoreCreateCountingStatic()
    使用静态方式创建一个计数型信号量,内存需要用户分配,此函数的本质是一个宏,真正执行的是 xQueueCreateCountingSemaphoreStatic()
    函数原型如下:
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount, 
												  UBaseType_t uxInitialCount,
												  StaticSemaphore_t * pxSemaphoreBuffer )

uxMaxCount: 计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。
uxInitialCount: 计数信号量初始值。
pxSemaphoreBuffer: 指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。

释放API

同二值信号量

互斥信号量

互斥信号量其实就是一个拥有优先级继承的二值信号量。当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级, 这个过程就是优先级继承。互斥信号量不能用于中断服务函数中,原因如下:
● 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
● 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。

创建互斥信号量 API

xSemaphoreCreateMutex()
动态创建,真正完成信号量创建的是函数 xQueueCreateMutex(),函数原型如下:无参数

SemaphoreHandle_t   xSemaphoreCreateMutex(void)

我们找到 xQueueCreateMutex() 的定义如下:

QueueHandle_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, ucQueueType);
	prvInitialiseMutex(pxNewQueue);
	return pxNewQueue;
}

首先创建了一个队列,队列的长度长度为 1,队列项长度为0,队列类型参数为 ucQueueType,实际传输进来的参数为 queueQUEUE_TYPE_MUTEX,表示互斥信号量。接着调用函数 prvInitialiseMutex() 初始化互斥信号量,我们进到函数内部看看具体做了些什么

static void prvInitialiseMutex( Queue_t *pxNewQueue )
{
	if( pxNewQueue != NULL ){
		pxNewQueue->pxMutexHolder = NULL;
		pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;
		pxNewQueue->u.uxRecursiveCallCount = 0;
		traceCREATE_MUTEX(pxNewQueue);
		(void) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK );
		} else {traceCREATE_MUTEX_FAILED();}
	}
#define pxMutexHolder pcTail
#define uxQueueType pcHead
#define queueQUEUE_IS_MUTEX NULL

当 Queue_t 用于表示队列的时候 pcHead 和 pcTail 指向队列的存储区域,当 Queue_t 用于表示互斥信号量的时候就不需要 pcHead 和 pcTail 了。如果创建的互斥信号量是互斥信号量的话, 还需要初始化队列结构体中的成员变量u.uxRecursiveCallCount。互斥信号量创建成功以后会调用函数 xQueueGenericSend()释放一次信号量,说明互斥信号量默认就是有效的!

释放互斥信号量 API

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

获取互斥信号量 API

实际调用的也是 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 );/* 调用函数 prvCopyDataFromQueue()使用数据拷贝的方式从队列中提取数据 */
				if( xJustPeeking == pdFALSE ) { /* 数据读取以后需要将数据删除掉 */
					traceQUEUE_RECEIVE( pxQueue );
					pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;/* 队列的消息数量计数器 uxMessagesWaiting 减一,通过这一步就将数据删除掉*/

					#if ( configUSE_MUTEXES == 1 ){ /*表示此函数是用于获取互斥信号量的*/
						if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){
						/* 获取互斥信号量成功,需要标记互斥信号量的所有者,也就是给 pxMutexHolder 赋值 */
						/* 通过 pvTaskIncrementMutexHeldCount()来赋值的 */
							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{   /* 当队列为空的时候 */
			/* 处理过程和队列的任务级通用入队函数 xQueueGenericSend()类似。如果阻塞时间为 0 的话就就直接返回
			   errQUEUE_EMPTY,表示队列空,如果设置了阻塞时间的话就进行相关的处理 */
				if( xTicksToWait == ( TickType_t ) 0 ){
					taskEXIT_CRITICAL();
					traceQUEUE_RECEIVE_FAILED( pxQueue );
					return errQUEUE_EMPTY;
				}else if( xEntryTimeSet == pdFALSE ){
					vTaskSetTimeOutState( &xTimeOut );
					xEntryTimeSet = pdTRUE;
				}
				else{mtCOVERAGE_TEST_MARKER();}
			}
		}
		taskEXIT_CRITICAL();
		vTaskSuspendAll();
		prvLockQueue( pxQueue );
		/* 检查超时是否发生,如果没有的话就需要将任务添加到队列的 xTasksWaitingToReceive 列表中 */
		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()处理互斥信号量中的优先级继承问题, 如果函数 xQueueGenericReceive() 
				   用于获取互斥信号量的话, 此函数执行到这里说明互斥信号量正在被其他的任务占用 */
							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();}
		}}
}

总结一下:
(1)判断队列是否是空的。
(2)如果队列不为空,使用函数 prvCopyDataToQueue() 从队列中拷贝数据。
(3)检查是否有任务因为队列满而进入阻塞态,如果有就接触阻塞态。此功能通过函数 xTaskRemoveFromEventList () 来完成。有两种处理方式,如果任务调度器没有挂起,从相应的事件列表和状态列表中移除,并且将任务添加到就绪列表中。如果任务调度器挂起的话,不会从转态列表中移除,并将任务添加到 xPendingReadyList 列表中。当通过函数 xTaskResumeAll() 恢复任务调度器时,添加到 xPendingReadyList 列表中的任务就会被处理。
(4)如果队列为空,当阻塞时间为0时直接返回,返回错误值为 errQUEUE_EMPTY。
(5)如果队列为空,当阻塞时间不为 0 ,通过函数 vTaskPlaceOneEventList() 将任务添加到相应的时间列表和延时列表中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值