精华内容
下载资源
问答
  • FreeRTOS信号量详解第四讲(全网最全)——互斥信号量
    2021-10-25 20:40:25

    一、互斥信号量简介

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

    二、互斥信号量相关API

    FreeRTOS提供了两个互斥信号量创建函数,如下表所示:

    函数描述
    xSemaphoreCreateMutex()使用动态方法创建互斥信号量
    xSemaphoreCreateMutexStatic()使用静态方法创建互斥信号量

    1、函数xSemaphoreCreateMutex()
    此函数用于创建一个互斥信号量,所需要的内存通过动态内存管理方法分配。此函数本质是一个宏,真正完成信号量创建的是函数xQueueCreateMutex(),此函数原型如下:

    SemaphoreHandle_t xSemaphoreCreateMutex(void)
    
    参数描述
    返回值NULL:互斥信号量创建失败。其他值:创建成功的互斥信号量的句柄。

    2、函数xSemaphoreCreateMutexStatic()
    此函数也是创建互斥信号量的,只不过使用此函数创建互斥信号量的话信号量所需要的,RAM需要由用户来分配,此函数是个宏,具体创建过程是通过函数xQueueCreateMutexStatic()来完成的,函数原型如下:

    SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t*pxMutexBuffer)
    
    参数描述
    pxMutexBuffer此参数指向一个StaticSemaphore_t类型的变量,用来保存信号量结构体。
    返回值NULL:互斥信号量创建失败。其他值:创建成功的互斥信号量的句柄。

    三、互斥信号量相关函数详细分析

    1、互斥信号量创建过程分析
    动态创建互斥信号量函数xSemaphoreCreateMutex(),此函数是个宏,定义如下:

    #define xSemaphoreCreateMutex() xQueueCreateMutex(queueQUEUE_TYPE_MUTEX)
    

    可以看出,真正干事的是函数xQueueCreateMutex(),此函数在文件queue.c中有如下定义,

    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 );(1)
    	prvInitialiseMutex( pxNewQueue );(2)
    
    	return pxNewQueue;
    }
    
    • (1)、调用函数xQueueGenericCreate()创建一个队列,队列长度为1,队列项长度为0,队列类型为参数ucQueueType。由于本函数是创建互斥信号量的,所以参数ucQueueType为queueQUEUE_TYPE_MUTEX。
    • (2)、调用函数prvInitialiseMutex()初始化互斥信号量。

    其中prvInitialiseMutex()代码如下:

    static void prvInitialiseMutex( Queue_t *pxNewQueue )
    {
    	if( pxNewQueue != NULL )
    	{
    /*虽然创建队列的时候会初始化队列结构体的成员变量,但是现在是创建互斥信号量,有些成员需要重新赋值,特别是用于优先级继承的*/
    		pxNewQueue->pxMutexHolder = NULL;(1)
    		pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;(2)
    
    		//如果是递归互斥信号量
    		pxNewQueue->u.uxRecursiveCallCount = 0;(3)
    
    		traceCREATE_MUTEX( pxNewQueue );
    
    		//释放互斥信号量
    		( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK );
    	}
    	else
    	{
    		traceCREATE_MUTEX_FAILED();
    	}
    }
    

    (1)和(2)、这里大家可能会疑惑,队列结构体Queue_t中没有pxMutexHolder和uxQueueType这两个成员变量?这两个东西是哪里来的妖孽?这两个其实是宏,专门为互斥信号量准备的,在文件queue.c中有如下定义:

    #define pxMutexHolder pcTail
    #define uxQueueType pcHead
    #define queueQUEUE_IS_MUTEX NULL
    

    当Queue_t用于表示队列的时候pcHead和pcTail指向队列的存储区域,当Queue_t用于表示互斥信号量的时候就不需要pcHead和pcTail了。当用于互斥信号量的时候将pcHead指向NULL来表示pcTail保存着互斥队列的所有者,pxMutexHolder指向拥有互斥信号量的那个任务的任务控制块。重命名pcTail和pcHead就是为了增强代码的可读性。
    (3)、如果创建的互斥信号量是互斥信号量的话,还需要初始化队列结构体中的成员变量u.uxRecursiveCallCount。
    互斥信号量创建成功以后会调用函xQueueGenericSend()释放一次信号量,说明互斥信号量默认就是有效的!互斥信号量初始化完成如下图所示:
    在这里插入图片描述
    2、释放互斥信号量过程分析
    释放互斥信号量的时候和二值信号量、计数型信号量一样,都是用的函数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 */
    	}
    	/**********省略其他代码************/
    	
    
    • (1)、当前操作的是互斥信号量。
    • (2)、调用函数xTaskPriorityDisinherit()处理互斥信号量的优先级继承问题。
    • (3)、互斥信号量释放以后,互斥信号量就不属于任何任务了,所以pxMutexHolder要指向NULL。

    现在来看一下函数xTaskPriorityDisinherit()是怎么具体的处理优先级继承的,函数xTaskPriorityDisinherit0代码如下:

    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)
    		{
    			/* 当前任务只获取到一个互斥信号量 */
    			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)
    				listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxTCB->uxPriority );(8) 
    				prvAddTaskToReadyList( pxTCB );(9)
    				xReturn = pdTRUE;(10)
    			}
    			else
    			{
    				mtCOVERAGE_TEST_MARKER();
    			}
    		}
    		else
    		{
    			mtCOVERAGE_TEST_MARKER();
    		}
    	}
    	else
    	{
    		mtCOVERAGE_TEST_MARKER();
    	}
    
    	return xReturn;
    }
    
    • (1)、函数的参数pxMutexHolder表示拥有此互斥信号量任务控制块,所以要先判断此互斥信号量是否已经被其他任务获取。
    • (2)、有的任务可能会获取多个互斥信号量,所以就需要标记任务当前获取到的互斥信号量个数,任务控制块结构体的成员变量uxMutexesHeld用来保存当前任务获取到的互斥信号量个数。任务每释放一次互斥信号量,变量uxMutexesHeld肯定就要减一。
    • (3)、判断是否存在优先级继承,如果存在的话任务的当前优先级肯定不等于任务的基优先级。
    • (4)、判断当前释放的是不是任务所获取到的最后一个互斥信号量,因为如果任务还获取了其他互斥信号量的话就不能处理优先级继承。优先级继承的处理必须是在释放最后一个互斥信号量的时候。
    • (5)、优先级继承的处理说白了就是将任务的当前优先级降低到任务的基优先级,所以要把当前任务先从任务就绪表中移除。当任务优先级恢复为原来的优先级以后再重新加入到就绪表中。
    • (6)、如果任务继承来的这个优先级对应的就绪表中没有其他任务的话就将取消这个优先级的就绪态。
    • (7)、重新设置任务的优先级为任务的基优先级uxBasePriority。
    • (8)、复位任务的事件列表项。
    • (9)、将优先级恢复后的任务重新添加到任务就绪表中。
    • (10)、返回pdTRUE,表示需要进行任务调度。

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

    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 )(1)
    			{
    				pcOriginalReadPosition = pxQueue->u.pcReadFrom;
    
    				prvCopyDataFromQueue( pxQueue, pvBuffer );(2)
    
    				if( xJustPeeking == pdFALSE )(3)
    				{
    					traceQUEUE_RECEIVE( pxQueue );
    
    					//移除消息
    					pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;(4)
    
    					#if ( configUSE_MUTEXES == 1 )(5)
    					{
    						if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
    						{
    							pxQueue->pxMutexHolder = ( int8_t * ) pvTaskIncrementMutexHeldCount(); (6)
    						}
    						else
    						{
    							mtCOVERAGE_TEST_MARKER();
    						}
    					}
    					#endif /* configUSE_MUTEXES */
    /*查看是否有任务因为入队而阻塞,如果有的话就需要解除阻塞态。*/
    					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )(7)
    					{
    						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
    						{
    /*如果解除任务阻塞的任务优先级比当前任务优先级高的话就需要进行一次任务切换*/
    							queueYIELD_IF_USING_PREEMPTION();
    						}
    						else
    						{
    							mtCOVERAGE_TEST_MARKER();
    						}
    					}
    					else
    					{
    						mtCOVERAGE_TEST_MARKER();
    					}
    				}
    				else(8)
    				{
    					traceQUEUE_PEEK( pxQueue );
    //读取队列中的消息以后需要删除消息
    					pxQueue->u.pcReadFrom = pcOriginalReadPosition;
    //如果有任务因为出队而阻塞的话就解除任务的阻塞态
    					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )(9)
    					{
    						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
    						{
    //如果解除阻塞的任务优先级比当前任务优先级高的话就需要进行一次任务切换
    							queueYIELD_IF_USING_PREEMPTION();
    						}
    						else
    						{
    							mtCOVERAGE_TEST_MARKER();
    						}
    					}
    					else
    					{
    						mtCOVERAGE_TEST_MARKER();
    					}
    				}
    
    				taskEXIT_CRITICAL();
    				return pdPASS;
    			}
    			else //队列为空 (10)
    			{
    				if( xTicksToWait == ( TickType_t ) 0 )
    				{
    	/*队列为空,如果阻塞时间为0的话就直接返回errQUEUE_EMPTY*/
    					taskEXIT_CRITICAL();
    					traceQUEUE_RECEIVE_FAILED( pxQueue );
    					return errQUEUE_EMPTY;
    				}
    				else if( xEntryTimeSet == pdFALSE )
    				{
    	//队列为空且设置了阻塞时间,需要初始化时间状态结构体。
    					vTaskSetTimeOutState( &xTimeOut );
    					xEntryTimeSet = pdTRUE;
    				}
    				else
    				{
    					/* Entry time was already set. */
    					mtCOVERAGE_TEST_MARKER();
    				}
    			}
    		}
    		taskEXIT_CRITICAL();
    		vTaskSuspendAll();
    		prvLockQueue( pxQueue );
    
    //更新时间状态结构体,并且检查超时是否发送。
    		if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )(11)
    		{
    			if( prvIsQueueEmpty( pxQueue ) != pdFALSE )(12)
    			{
    				traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );
    
    				#if ( configUSE_MUTEXES == 1 )
    				{
    					if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )(13)
    					{
    						taskENTER_CRITICAL();
    						{
    							vTaskPriorityInherit( ( void * ) pxQueue->pxMutexHolder );(14)
    						}
    						taskEXIT_CRITICAL();
    					}
    					else
    					{
    						mtCOVERAGE_TEST_MARKER();
    					}
    				}
    				#endif
    
    				vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );(15)
    				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)、调用函数prvCopyDataFromQueue()使用数据拷贝的方式从队列中提取数据。
    • (3)、数据读取以后需要将数据删除掉。
    • (4)、队列的消息数量计数器uxMessagesWaiting减一,通过这一步就将数据删除掉了。
    • (5)、表示此函数是用于获取互斥信号量的。
    • (6)、获取互斥信号量成功,需要标记互斥信号量的所有者,也就是给pxMutexHolder赋值,pxMutexHolder应该是当前任务的任务控制块。但是这里是通过函数pvTaskIncrementMutexHeldCount()来赋值的,此函数很简单,只是将任务控制块中的成员变量uxMutexesHeld加一,表示任务获取到了一个互斥信号量,最后此函数返回当前任务的任务控制块。
    • (7)、出队成功以后判断是否有任务因为入队而阻塞的,如果有的话就需要解除任务的阻塞态,如果解除阻塞的任务优先级比当前任务的优先级高还需要进行一次任务切换。
    • (8)、出队的时候不需要删除消息。
    • (9)、如果出队的时候不需要删除消息的话那就相当于刚刚出队的那条消息接着有效!既然还有有效的消息存在队列中,那么就判断是否有任务因为出队而阻塞,如果有的话就解除任务的阻塞态。同样的,如果解除阻塞的任务优先级比当前任务的优先级高的话还需要进行一次任务切换。
    • (10)、上面分析的都是队列不为空的时候,那当队列为空的时候该如何处理呢?处理过程和队列的任务级通用入队函数xQueueGenericSend()类似。如果阻塞时间为0的话就就直接返回errQUEUE_EMPTY,表示队列空,如果设置了阻塞时间的话就进行相关的处理。
    • (11)、检查超时是否发生,如果没有的话就需要将任务添加到队列的xTasksWaitingToReceive列表中。
    • (12)、检查队列是否继续为空?如果不为空的话就会在重试一次出队。
    • (13)、表示此函数是用于获取互斥信号量的。
    • (14)、调用函数vTaskPriorityInherit()处理互斥信号量中的优先级继承问题,如果函数xQueueGenericReceive()用于获取互斥信号量的话,此函数执行到这里说明互斥信号量正在被其他的任务占用。此函数和函数xTaskPriorityDisinherit()过程相反。此函数会判断当前任务的任务优先级是否比正在拥有互斥信号量的那个任务的任务优先级高,如果是的话就会把拥有互斥信号量的那个低优先级任务的优先级调整为与当前任务相同的优先级
    • (15)、经过(12)步判断,队列依旧为空,那么就将任务添加到列表xTasksWaitingToReceive中。

    互斥信号量的讲解就到这里啦!!!谢谢大家收看!!!

    更多相关内容
  • OSMutexCreate()创建互斥信号量2.OSMutexPend()请求互斥信号量3.OSMutexPost()函数释放互斥信号量总结 一、信号量简介 FreeRTOS相同信号量像是一种上锁机制,代码必须获得对应的钥匙才能继续执行,一旦获

    UCOS操作系统

    一、信号量简介

    与FreeRTOS相同信号量像是一种上锁机制,代码必须获得对应的钥匙才能继续执行,一旦获得了钥匙,也就意味着该任务具有进入被锁部分代码的权限。一旦执行至被锁代码段,则任务一直等待,直到对应被锁部分代码的钥匙被再次释放才能继续执行。
    信号量用于控制对共享资源的保护,但是现在基本用来做任务同步用。
    要想获取资源的任务必须执行“等待”操作,如果该资源对应的信号量有效值大于1,则任务可以获得该资源,任务继续运行。如果该信号量的有效值为0,则任务加入等待信号量的任务表中。如果等待时间超过某一个设定值,该信号量仍然没有被释放掉,则等待信号量的任务就进入就绪态,如果将等待时间设置为0的话任务就将一直等待该信号量。
    信号量通常分为两种:二进制信号量和计数型信号量。

    1、二进制信号量

    某一资源对应的信号量为 1 的时候,那么就可以使用这一资源,如果对应资源的信号量为0,那么等待该信号量的任务就会被放进等待信号量的任务表中。在等待信号量的时候也可以设置超时,如果超过设定的时间任务没有等到信号量的话那么该任务就会进入就绪态。任务以“发信号”的方式操作信号量。可以看出如果一个信号量为二进制信号量的话,一次只能一个任务使用共享资源。

    2、计数型信号量

    有时候我们需要可以同时有多个任务访问共享资源,这个时候二进制信号量就不能使用了,计数型信号量就是用来解决这个问题的。比如某一个信号量初始化值为 10,那么只有前 10 个请求该信号量的任务可以使用共享资源,以后的任务需要等待前 10 个任务释放掉信号量。每当有任务请求信号量的时候,信号量的值就会减 1,直到减为 0。当有任务释放掉信号量的时候信号量的值就会加 1。

    二、使用信号量

    1.相关API函数

    OSSemCreate() 创建一个信号量
    OSSemDel() 删除一个信号量
    OSSemPend() 等待一个信号量
    OSSemPendAbort() 取消等待
    OSSemPost() 释放一个信号量
    OSSemSet() 强制设置一个信号量的值
    

    2.OSSemCreate()创建信号量

    void OSSemCreate ( OS_SEM *p_sem,
     CPU_CHAR *p_name,
     OS_SEM_CTR cnt,
     OS_ERR *p_err)
    

    p_sem: 指向信号量控制块,我们需要按照如下所示方式定义一个全局信号量,并将
    这个信号量的指针传递给函数 OSSemCreate()。
    OS_SEM TestSem;
    p_name: 指向信号量的名字。
    cnt: 设置信号量的初始值,如果此值为 1,代表此信号量为二进制信号量,如果大于 1
    的话就代表此信号量为计数型信号量。
    p_err: 保存调用此函数后的返回的错误码。
    二值信号量:
    在这里插入图片描述
    计数信号量:
    在这里插入图片描述

    3.OSSemPend()请求信号量

    当一个任务需要独占式的访问某个特定的系统资源时,需要与其他任务或中断服务程序同
    步,或者需要等待某个事件的发生,应该调用函数 OSSemPend(),函数原型如下:
    前面不是提到信号量就像一把锁,只有获取到了才会执行下面得内容,否则就一直等待。

    OS_SEM_CTR OSSemPend ( OS_SEM *p_sem,
     OS_TICK timeout,
     OS_OPT opt,
     CPU_TS *p_ts,
     OS_ERR *p_err)
    

    p_sem: 指向一个信号量的指针。
    timeout: 指定等待信号量的超时时间(时钟节拍数),如果在指定时间内没有等到信号量则允许任务恢复执行。如果指定时间为 0 的话任务就会一直等待下去,直到等到信号量。
    opt: 用于设置是否使用阻塞模式,有下面两个选项。
    OS_OPT_PEND_BLOCKING 指定信号量无效时,任务挂起以等待信号量。
    OS_OPT_PEND_NON_BLOCKING 信号量无效时,任务直接返回。
    p_ts: 指向一个时间戳,用来记录接收到信号量的时刻,如果给这个参数赋值 NULL,
    则说明用户没有要求时间戳。
    p_err: 保存调用本函数后返回的错误码。
    在这里插入图片描述

    OSSemPend(&MY_SEM,0,OS_OPT_PEND_BLOCKING,0,&err); 	//请求信号量
    

    4.OSSemPost()释放信号量

    任务获得信号量以后就可以访问共享资源了,在任务访问完共享资源以后必须释放信号量,释放信号量也叫发送信号量,使用函数 OSSemPost()发送信号量。如果没有任务在等待该信号量的话则 OSSemPost()函数只是简单的将信号量加 1,然后返回到调用该函数的任务中继续运行。如果有一个或者多个任务在等待这个信号量,则优先级最高的任务将获得这个信号量,然后由调度器来判定刚获得信号量的任务是否为系统中优先级最高的就绪任务,如果是,则系统将进行任务切换,运行这个就绪任务,OSSemPost()函数原型如下:

    OS_SEM_CTR OSSemPost ( OS_SEM *p_sem,
     OS_OPT opt,
     OS_ERR *p_err)
    

    p_sem: 指向一个信号量的指针
    opt: 用来选择信号量发送的方式。
    OS_OPT_POST_1 仅向等待该信号量的优先级最高的任务发送信号量。
    OS_OPT_POST_ALL 向等待该信号量的所有任务发送信号量。
    OS_OPT_POST_NO_SCHED 该选项禁止在本函数内执行任务调度操作。即使
    该函数使得更高优先级的任务结束挂起进入就绪状态,也不会执行任务调度,而是会
    在其他后续函数中完成任务调度。
    p_err: 用来保存调用此函数后返回的错误码

    三、优先级反转

    例如:三个不同优先级的任务——低任务、中任务、高任务
    创建二值信号量,然后释放一次信号量。低任务获取信号量,长时间不释放。
    高任务获取信号量,但是此时信号量被低任务占用着,高任务只能等待。但是等待过程中,中任务是一直运行的。出现了优先级反转。
    可以看我之前的博客,写的很详细

    四、互斥信号量

    与FreeRTOS一样,使用互斥信号量可以完美的避开这个问题。
    在这里插入图片描述
    不同于二值信号量的是互斥信号量具有优先级继承的特性。当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。

    1. OSMutexCreate()创建互斥信号量

    创建互斥信号量使用函数 OSMutexCreate(),函数原型如下:

    void OSMutexCreate (OS_MUTEX *p_mutex,
     CPU_CHAR *p_name,
     OS_ERR *p_err)
    

    p_mutex: 指向互斥型信号量控制块。互斥型信号量必须有用户应用程序进行实际分配
    OS_MUTEX MyMutex;
    p_name: 互斥信号量的名字
    p_err: 调用此函数后返回的错误码。

    2.OSMutexPend()请求互斥信号量

    当一个任务需要对资源进行独占式访问的时候就可以使用函数 OSMutexPend(),如果该互斥信号量正在被其他的任务使用,那么 UCOSIII 就会将请求这个互斥信号量的任务放置在这个互斥信号量的等待表中。任务会一直等待,直到这个互斥信号量被释放掉,或者设定的超时时间到达为止。如果在设定的超时时间到达之前信号量被释放,UCOSIII 将会恢复所有等待这个信号量的任务中优先级最高的任务
    注意!如果占用该互斥信号量的任务比当前申请该互斥信号量的任务优先级低的话,
    OSMutexPend()函数会将占用该互斥信号量的任务的优先级提升到和当前申请任务的优先级一样。当占用该互斥信号量的任务释放掉该互斥信号量以后,恢复到之前的优先级。OSMutexPend()函数原型如下:

    void OSMutexPend (OS_MUTEX *p_mutex,
     OS_TICK timeout,
     OS_OPT opt,
     CPU_TS *p_ts,
     OS_ERR *p_err)
    

    p_mutex: 指向互斥信号量。
    timeout: 指定等待互斥信号量的超时时间(时钟节拍数),如果在指定的时间内互斥信
    号量没有释放,则允许任务恢复执行。该值设置为 0 的话,表示任务将会一直等待下去,直到信号量被释放掉。
    opt: 用于选择是否使用阻塞模式。
    OS_OPT_PEND_BLOCKING 指定互斥信号量被占用时,任务挂起等待该互斥信号量。
    OS_OPT_PEND_NON_BLOCKING 指定当互斥信号量被占用时,直接返回任务。
    注意!当设置为 OS_OPT_PEND_NON_BLOCKING,是 timeout 参数就没有
    意义了,应该设置为 0。
    p_ts: 指向一个时间戳,记录发送、终止或删除互斥信号量的时刻。
    p_err: 用于保存调用此函数后返回的错误码。

    3.OSMutexPost()函数释放互斥信号量

    我们可以通过调用函数 OSMutexPost()来释放互斥型信号量,只有之前调用过函数
    OSMutexPend()获取互斥信号量,才需要调用 OSMutexPost()函数来释放这个互斥信号量,函数原型如下:

    void OSMutexPost (OS_MUTEX *p_mutex,
     OS_OPT opt,
     OS_ERR *p_err)
    

    p_mutex: 指向互斥信号量。
    opt: 用来指定是否进行任务调度操作
    OS_OPT_POST_NONE 不指定特定的选项
    OS_OPT_POST_NO_SCHED 禁止在本函数内执行任务调度操作。
    p_err: 用来保存调用此函数返回的错误码

    总结

    信号量与互斥信号量需要结合实验例程来理解,下一篇博客会提到每个知识点对应的实验。

    展开全文
  • 多线程程序设计中,用于实现互斥管理。对Windows临界区,内核事件,互斥量,信号量四种方式进行对比介绍
  • 为了解决这个问题,引出了互斥信号量的概念。 1:信号量   信号量是一种上锁机制,该任务必须获得相应的钥匙才能进入,直至代码执行结束,释放钥匙,程序才退出。信号量分两种,一种是二进制信号量,一种是计数型

    前言:
      在ucosiii中有可能会有多个任务访问共享资源,同时对这个共享资源操作时会出错,因此信号量最早是用来控制任务存取共享资源。现在信号量被用来实现任务之间的同步和Isr之间的同步。在可剥夺的内核中,当任务独占式使用共享资源时就会出现低优先级任务先于高优先任务运行的现象,这个现象称之为优先级翻转现象。为了解决这个问题,引出了互斥信号量的概念。

    1:信号量
      信号量是一种上锁机制,该任务必须获得相应的钥匙才能进入,直至代码执行结束,释放钥匙,程序才退出。信号量分两种,一种是二进制信号量,一种是计数型信号量。二进制信号量只有0和1两个值,计数型信号量可以有多个值。
    (1)二进制信号量
      当某个资源对应的信号量为1时,则表示这个资源可用;当为0时,则表示这个资源不可用。如果某个任务一直在等待,在超过等待时间后进入就绪状态。可以看出使用二进制信号量的资源,一次只能有一个任务使用这个资源。
    (2)计数型信号量
      多线程任务就会出现多个任务同时访问同一资源,这个时候二进制信号量就比较受限,不能多个任务同时访问,比较耗时间。如果使用的是计数型信号量就可以允许多个任务同时访问。每当有任务访问时 信号量数值加1,任务释放时减1.

    2:优先级反转现象
    优先级反转在可剥夺型内核中是非常常见的,在实时任务系统中不允许出现这样的情况出现,这样会破坏任务的预期顺序。先来看看到底什么是优先级反转现象:
    优先级反转示意图
    (1)任务H和任务M处于挂起状态,等待某一事件的发生,任务L正在运行
    (2)某一时刻任务L想要访问共享资源,在此之前必须获取对应资源的信号量
    (3)任务L获取信号量后开始使用共享资源
    (4)由于任务H的优先级高,他在某个事件发生后,剥夺了任务L的cpu使用权
    (5)任务H开始运行
    (6)任务H运行过程中也要 使用任务L正在使用的共享资源,由于信号量还在被任务L占用着,任务 H只能挂起等待信号量的到来
    (7)任务L继续运行
    (8)由于任务M的优先级也比 任务L的优先级高,所以,任务M剥夺了任务L的cpu使用权
    (9)任务M执行
    (10)任务M执行完后,释放cpu使用权,任务L继续执行
    (11)任务L执行
    (12)任务L执行完后,释放信号量,在切换到任务H
    (13)任务H得到信号量后,继续执行

      在以上给出例子中我们可以看到,实际上任务H的优先级是最高的,理论上他应该最先执行完,在等待任务L释放信号的时候,被任务M抢去了cpu的使用权,使得任务H等待的时间更加增长,这就相当于任务M的优先级高于任务H的优先级,导致优先级反转。

    3:互斥信号量
      为了避免优先级反转现象,ucosiii支持一种特殊的二进制信号量,叫互斥信号量。他究竟是怎么避免的呢?我们取个例子来看:
    使用互斥信号量访问共享资源
    (1)任务H和任务M处于挂起状态,等待某一事件的发生,任务L正在运行
    (2)某一时刻任务L想要访问共享资源,在此之前必须获取对应资源的信号量
    (3)任务L获取信号量后开始使用共享资源
    (4)由于任务H的优先级高,他在某个事件发生后,剥夺了任务L的cpu使用权
    (5)任务H开始运行
    (6)任务H运行过程中也要使用正在被任务L占有的共享资源,考虑到任务L正占用资源,ucosiii会把任务L的优先级提升到跟任务H一样的优先级,使得任务L能继续执行,不会被其他中等优先级任务打断。
    (7)任务L以任务H的优先级运行,此时任务H并没有运行,因为共享资源的信号量在任务L手中,任务H还在等待它释放信号量
    (8)任务L完成操作后,释放掉互斥信号量,ucosiii会将任务L的优先级恢复到提升之前的值,并释放了信号量
    (9)任务H得到信号量后继续执行
    (10)任务H访问完共享资源后,释放掉互斥信号量,
    (11)此时没有优先级高于任务H的任务,所以继续执行任务H
    (12)等任务H完成后,某一事件发生后,执行任务M
    (13)任务M继续执行
      互斥信号量只有任务才能使用,在中断服务程序中则不可以。ucosiii允许用户嵌套使用互斥信号量,一旦一个任务获得了一个互斥信号量,那么该任务可以对该互斥信号量嵌套使用250次,当然释放的时候也要释放相同的次数才能真正释放这个互斥信号量。

    展开全文
  • 信号量&同步与互斥

    2018-04-08 11:28:19
    在介绍信号量&同步与互斥之前,我们先来了解一下生产者消费者模型。 生产者将数据放入缓冲区,消费者将数据从缓冲区取走消费。 生产者消费者的模型提出了三种关系,两种角色,一个场所 三种关系: ...

    在介绍信号量&同步与互斥之前,我们先来了解一下生产者与消费者模型。
    这里写图片描述

    生产者将数据放入缓冲区,消费者将数据从缓冲区取走消费。
    

    生产者消费者的模型提出了三种关系,两种角色,一个场所

    三种关系:
    - 生产者之间的互斥关系
    - 消费者之间的竞互斥关系
    - 生产者和消费者之间互斥和同步关系(同一时刻只能有一个,要么在生产,要么在消费,这就是互斥关系,只能在生产者生产完了之后才能消费,这就是同步关系)

    两个角色:一般是用进程或线程来承担生产者或消费者

    一个场所:有效的内存区域。(如单链表,数组)
    我们就可以把这个想象成生活中的超市供货商,超市,顾客的关系,超市供货商供货,超市是摆放货物的场所,然后用户就是消费的。


    信号量主要用于同步和互斥,那么什么是同步与互斥呢?
    进程互斥:

    • 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系称为互斥。
    • 系统中的某些资源一次只允许一个进程使用,这样的资源称为临界资源或者互斥资源。
    • 进程中涉及到互斥资源的程序段叫临界区。

    进程同步
    进程同步指的是多个进程需要相互配合共同完成一项任务。

    司机 p1                               售票员 p2
    while(1)                            while1{                                   {
        启动车辆;                            关门;
        正常运行;                            售票;
        到站停车;                            开门;
    }                                   }
    信号量和P、V原语
    • 信号量和p、v原语由大佬Dijkstra(迪杰斯特拉)提出
    • 信号量

      互斥:P、V在同一进程中
      同步:P、V在不同进程中

    • 信号量值的含义

      s > 0: s表示可用资源的个数
      s = 0: 表示无可用资源,无等待进程
      s < 0: |s|表示等待队列中进程个数

    信号量结构体的伪代码
    信号量本质上是一个计数器
    struct semaphore
    {
        int value;
        pointer_PCB queue;
    }

    P原语

    p(s){
        s.value = s.value--;
        if(s.value < 0){
            该进程状态设置为等待状态;
            将该进程的PCB插入相应等待队列s.queue末尾;
        }
    }

    V原语

    V(s){
        s.value = s.value++;
        if(s.value <= 0){
            唤醒相应等待队列s.queue中等待的一个进程
            改变其状态为就绪态
            将其插入就绪队列    
        }
    }

    简单的介绍之后,就让我们来用代码测试一下同步与互斥吧!

    这里,我们按照二元信号量来测试。
    comm.h

    #pragma once
    
    #include<stdio.h>
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/sem.h>
    #include<unistd.h>
    #include<sys/wait.h>
    
    #define PATHNAME "."
    #define PROJ_ID 0x6666
    
    union semun{
        int val;
        struct semid_ds *buf;
        unsigned short *array;
        struct seminfo *_buf;
    };
    
    int createSemSet(int nums);
    int initSem(int semid, int nums, int initVal);
    int getSemSet(int nums);
    int P(int semid,int who);
    int V(int semid,int who);
    int destroySemSet(int semid);
    

    comm.c

    #include"comm.h"
    
    static int commSemSet(int nums,int flags){
        key_t _key = ftok(PATHNAME,PROJ_ID);
        if(_key < 0){
            perror("ftok");
            return -1;
        }
        int semid = semget(_key, nums, flags);
        if(semid < 0){
            perror("semget");
            return -2;
        }
    
        return semid;
    }
    
    int createSemSet(int nums){
        return commSemSet(nums, IPC_CREAT|IPC_EXCL|0666);
    }
    
    int getSemSet(int nums){
        return commSemSet(nums, IPC_CREAT);
    }
    
    int initSem(int semid, int nums, int initVal){
        union semun _un;
        _un.val = initVal;
        if(semctl(semid, nums, SETVAL, _un) < 0){
            perror("semctl");
            return -1;
        }
    
        return 0;
    }
    
    static int commPV(int semid, int who, int op){
        struct sembuf _sf;
        _sf.sem_num = who;
        _sf.sem_op = op;
        _sf.sem_flg = 0;
        if(semop(semid, &_sf, 1) < 0){
            perror("semop");
            return -1;
        }
        return 0;
    }
    
    int P(int semid, int who){
        return commPV(semid, who, -1);
    }
    
    int V(int semid, int who){
        return commPV(semid, who, 1);
    }
    
    int destroySemSet(int semid){
        if(semctl(semid, 0, IPC_RMID) < 0){
           perror("semctl");
            return -1;
        }
        return 0;
    }
    

    test_sem.c

    #include"comm.h"
    
    int main(){
        int semid = createSemSet(1);
        initSem(semid, 0, 1);
        pid_t id = fork();
        if(id == 0){
            int _semid = getSemSet(0);
            while(1){
                P(_semid,0);
                printf("A");
                fflush(stdout);
                usleep(123456);
                printf("A");
                fflush(stdout);
                usleep(321456);
                V(_semid, 0);
            }
        }
        else{
            while(1){
                P(semid, 0);
                printf("B");
                fflush(stdout);
                usleep(223456);
                printf("B");
                fflush(stdout);
                usleep(12346);
                V(semid, 0);
            }
            wait(NULL);
        }
        destroySemSet(semid);
        return 0;
    }
    

    最后奉上Makefile

    test_sem:comm.c test_sem.c
        gcc -o $@ $^
    
    .PHONY:clean
    clean:
        rm -f test_sem
    

    此时,显示器只有一个,两个进程同时打印,显示器称为临界资源,使用二元信号量(互斥锁)进行保护

    执行效果如下:
    这里写图片描述

    所有AB都是成对出现,不会出现交叉的情况,那如果去掉PV呢?

    这里写图片描述
    出现打印的内容相互影响

    在ctrl + c之后使用ipcs -s 查看信号量,及ipcsrm -s + semid删除信号量
    关于ipcs&ipcrm的使用,参照我得博客:ipcs和ipcrm的使用
    注意,删除信号量不是必须通过手动删除,这里只是为了演示相关指令,删除IPC资源是进程应该做的事。


    由于个人能力有限,以上的说明可能有Bug或者有什么不妥之处,欢迎发邮件到我的邮箱( Cyrus_wen@163.com )批评指正!

    展开全文
  • 【UCOSIII】UCOSIII的互斥信号量

    万次阅读 2018-07-02 21:18:54
    信号量用于控制对共享资源的保护,但是现在基本用来做任务同步用(不太清楚的可以参考链接:【UCOSIII】UCOSIII的信号量)。   优先级反转 优先级反转在可剥夺内核中是非常常见的,在实时系统中不允许出现这种...
  • 文章目录前言一、创建互斥信号量二、释放互斥信号量三、获取互斥信号量四、互斥信号量实验五、递归互斥信号量*1、创建递归互斥信号量**2、释放递归互斥信号量*3、获取递归互斥信号量*总结 前言 互斥信号量其实就是...
  • 例如说串口,USB、IO、定时器等,这个时候我们就要用到互斥信号量了。 互斥信号量时信号量的一个特例,信号量的计数值只能为0和1。 但是互斥信号量和信号量之间还是有区别的。互斥信号量需要解决优先级反转的问题...
  • 在前一篇文章基础背景下,我们为了解决对共享资源访问出现线程冲突的问题,引入了几个概念,分别是计数型信号量互斥信号量。接下来我会根据自己的理解为大家一一进行讲解。 首先,什么叫做信号量? 还记不记得...
  • 互斥量的实现进程中的信号量(无名信号量)是类似的,当然,信号量也可以用于线程,区别在于初始化的时候,其本质都是P/V操作。编译时,记得加上-lpthread或-lrt哦。  有关进程间通信(消息队列)见:进程间通信之...
  • } 操作系统正是利用信号量的状态来对进程和资源进行管理的   1.3 利用信号量实现进程之间的互斥   设置一个互斥信号量mutex,初值为1,表示该临界资源空闲。   调用P(mutex)申请临界资源——mutex变为0。  ...
  • 信号量实现同步互斥经典案例

    千次阅读 2021-01-11 19:48:06
    这一篇在上篇的基础上得以延伸,介绍一下相对复杂的进程互斥、同步的案例。 问题描述 系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并...
  • 信号量与互斥量的区别

    千次阅读 2019-11-07 18:53:15
    信号量:那是多线程同步用的,一个线程完成了某一个动作就通过信号告诉别的...信号量与普通整型变量的区别: 信号量(semaphore)是非负整型变量,除了初始化之外,它只能通过两个标准原子操作:wait(semap), sign...
  • 互斥信号量

    2019-02-13 16:24:00
    互斥信号量是 uC/OS 操作系统的一个内核对象,多值信号量非常相似,但它是二值的,只能是 0 或 1,所以也叫二值信号量,主要用于保护资源。 如果想要使用互斥信号量,就必须事先使能互斥信号量互斥信号量的使能...
  • 什么是进程? 进程是一个程序正在执行的实例。... 当内核产生一个新的PID,生成对应的用于管理的数据结构,并为运行程序代码分配了必要的资源,一个新的进程就产生了。 什么是线程? 线程是进程的一个
  • 1. uCOS ...队列大小为1的消息队列邮箱的作用是一样的。 在uCOS-III中,取消了邮箱的概念,只有消息队列,在代码中,有OSCfg_MsgPool是一个消息池,每次 2. rt-thread RTT中有邮箱和消息队列两个概念
  • UCOSIII操作系统UCOSIII操作系统——信号量互斥量篇(1)互斥量信号量的API函数创建互斥信号量->OSMutexCreate()()删除互斥信号量-> OSMutexDel()等待一个互斥信号量->OSMutexPend()释放一个互斥信号量-&...
  • 互斥量的实现进程中的信号量(无名信号量)是类似的,当然,信号量也可以用于线程,区别在于初始化的时候,其本质都是P/V操作。编译时,记得加上-lpthread或-lrt哦。  一、互斥量  1、初始化销毁:  ...
  • 二进制信号量互斥量之间的区别

    千次阅读 2019-12-20 09:55:08
    二进制信号量互斥量之间是否有任何区别,或者它们基本相同?
  • 自旋锁、互斥锁和信号量

    千次阅读 2018-10-09 11:26:29
    信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。 自旋锁为什么广泛用于内核 自旋锁是一种轻量级的...
  • UCOSIII操作系统UCOSIII操作系统——信号量与互斥量篇(1)信号量二进制信号量计数型信号量信号量的API函数创建信号量->OSSemCreate()删除信号量-> OSSemDel()释放一个信号量->OSSemPost()等待一个信号量-&...
  • 事实上,Dijkstra及其同事发明了信号量,作为同步有关的所有工作的唯一原语。你会看到,可以使用信号量作为锁和条件变量。 信号量的定义 信号量是有一个整数值的对象,可以用两个函数来操作它。在PO
  • FreeRTOS 队列 信号量 互斥

    千次阅读 2019-12-03 23:41:30
    文章目录前言Queue 队列semaphore 信号量Mutex 互斥量微信公众号 前言 FreeRTOS STM32CubeMX配置 内存管理 任务管理 上节介绍了用STM32CubeMX生成带FreeRTOS的工程, 细心的同学可能发现, 已创建任务的函数为例, ...
  • 一、记录锁 1、概念 我们首先来看记录锁,记录... 记录锁其实是不同进程间进行同步的一种锁,它主要针对的是两个不同的进程,而信号量互斥量的着眼点在于多线程(可以是同一进程的,也可以是不同进程的)。 ...
  • 信号量 记录锁 互斥量之间的区别 如果多个进程间共享一个资源,则可以使用这三种技术中的一种来协调访问. 我们可以使用映射到两个进程地址空间中的信号量,记录锁或者互斥 量. 对于这三种技术两两...
  • 本文主要介绍互斥量、信号量、事件集在任务间同步过程中起到的作用,并通过对其概念、控制块结构和接口设计的讲解帮助开发者更好的理解其在操作系统中的应用。 ...
  • 本篇也是一篇老文,发布于2015年5月,文章比较长,算老博客看的比较多的了,贴到这儿大家分享,以求多多交流探讨。</p> 前言现代操作系统采用多道程序设计机制,多个进程可以并发执行,CPU在进程之间来回...
  • 二、什么是进程互斥? 临界资源:一个时间段内只允许一个进程使用的资源 为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则: 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程...
  • 看了《嵌入式实时操作系统uC/OS-II》也快一个星期了,期间断断续续的边工作边看,依照书的目录,大致细分了系统各个功能:任务管理,时间管理,事 件控制,信号量管理互斥信号量管理,事件标志组管理,信息邮箱...
  • 信号量互斥量解析

    千次阅读 2014-10-31 10:09:09
    首先应弄清PV操作的含义:PV操作由P操作原语和V操作原语组成(原语是不可中断的过程),对信号量进行操作,具体定义如下:  P(S):①将信号量S的值减1,即S=S-1;  ②如果S³0,则该进程继续执行;否则该进程...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 45,324
精华内容 18,129
关键字:

互斥信号量管理与信号量管理

友情链接: Linux_broadcast.rar