二进制信号量( B i n a r y S e m a p h o r e s Binary\quad Semaphores BinarySemaphores),计数信号量( C o u n t i n g S e m a p h o r e s Counting\quad Semaphores CountingSemaphores),互斥量( M u t e x e s Mutexes Mutexes)以及递归互斥量( R e c u r s i v e M u t e x e s Recursive\quad Mutexes RecursiveMutexes),它们并不是完全独立,毫无关联的。至少在实现的数据结构来说,它们使用的都是队列的数据结构只是在细节上有一定的差异。
二进制信号量常用于互斥和同步,从前面的介绍可以看出二进制信号量,互斥量和递归互斥量所占用的存储空间是一样的,其实互斥量和递归互斥量是一种特殊的二进制信号量。正是因为优先级继承机制,二进制信号量更适用于任务与任务间或任务与中断之间的同步而互斥量更适用于简单的互斥操作。很多的信号量(这里包括二进制信号量,计数信号量,互斥量和递归互斥量)接口函数都有一个阻塞时间参数,比如在 T a k e Take Take操作的时候这个参数表示当试图 T a k e Take Take信号量的时候,信号量此时 I s n o t a v a i l a b l e Is\quad not\quad \quad available Isnotavailable,此时进行 T a k e Take Take操作的任务就要进入阻塞状态,这个阻塞时间参数就表示处于阻塞状态的最长时间。这里就存在两种情况,一种情况是还没有到达这个阻塞参数规定的最大阻塞时间的时候,信号量就已经 I s a v a i l a b l e Is\quad available Isavailable,此时阻塞的任务会自动退出阻塞状态(假如此时没有优先级更高的任务在等待同一个信号量 I s a v a i l a b l e Is\quad available Isavailable,因为如果有多个任务在等待同一信号量的话,信号量 I s a v a i l a b l e Is\quad available Isavailable的时候优先级最高的任务会得到信号量,其它任务会继续等待),另一种情况是到了阻塞参数规定的最大阻塞时间的时候,等待的信号量还是没有 I s a v a i l a b l e Is\quad available Isavailable,此时等待的任务会自动退出阻塞状态。 G i v e Give Give操作的接口一般没有这个阻塞时间参数。
前面也介绍过二进制信号量(也包括计数信号量,互斥量和递归互斥量)的队列里面的数据项是不占用存储空间的,创建队列的接口的 u x I t e m S i z e uxItemSize uxItemSize参数的值为0,因此二进制信号量(也包括计数信号量,互斥量和递归互斥量)是不关心具体的消息是什么,只关心消息的有(一个或多个)与无(0个),消息的有与无和队列一样,用表示队列状态的结构体里面的元素 u x M e s s a g e s W a i t i n g uxMessagesWaiting uxMessagesWaiting来表示,如图8所示。二进制信号量(也包括计数信号量,互斥量和递归互斥量)的 G i v e Give Give相当于队列的写操作,二进制信号量(也包括计数信号量,互斥量和递归互斥量)的 T a k e Take Take相当于队列的读操作,只不过没有实际数据的写入以及读出,只是对元素 u x M e s s a g e s W a i t i n g uxMessagesWaiting uxMessagesWaiting进行加一或减一的操作。对于二进制信号量,互斥量和递归互斥量元素 u x M e s s a g e s W a i t i n g uxMessagesWaiting uxMessagesWaiting的值只能为1或0,对于计数信号量元素 u x M e s s a g e s W a i t i n g uxMessagesWaiting uxMessagesWaiting的值就没有这个限制,可以大于1。
下面我们来看一个二进制信号量应用的例子。假设一个任务用来服务一个外设,当外设有需求的时候会告诉这个任务来处理。如果这个任务对外设进行轮序操作来检查外设是否有服务需求的话,这种操作是非常浪费 C P U CPU CPU资源的并且让其它比这个任务的优先级低的任务无法执行。最好的方案是在外设没有服务请求的时候都让这个任务处于阻塞的状态(这样其它比这个任务的优先级低的任务就可以执行),等到外设有服务请求的时候再唤醒这个任务来处理。这种方案可以通过二进制信号量来实现,当任务试图通过 T a k e Take Take操作获取信号量来对外设进行服务的时候,如果此时信号量 I s n o t a v a i l a b l e Is\quad not\quad \quad available Isnotavailable且 T a k e Take Take操作指定了阻塞时间(如果阻塞时间设置为 p o r t M A X _ D E L A Y portMAX\_DELAY portMAX_DELAY且 I N C L U D E _ v T a s k S u s p e n d INCLUDE\_vTaskSuspend INCLUDE_vTaskSuspend宏在 F r e e R T O S C o n f i g . h FreeRTOSConfig.h FreeRTOSConfig.h中定义为1,那么任务会一直阻塞直到等待的信号量 I s a v a i l a b l e Is\quad available Isavailable),此时该任务就会进入阻塞状态。每当外设有服务请求的时候可以触发相应的中断来对信号量进行 G i v e Give Give操作,此时信号量就变成 I s a v a i l a b l e Is\quad available Isavailable从而唤醒阻塞的任务来对外设进行服务。外设中断对于二进制信号量永远只有 G i v e Give Give操作而没有 T a k e Take Take操作,服务于外设的任务永远只有 T a k e Take Take操作而没有 G i v e Give Give操作。这种行为对于互斥量和递归互斥量是不行的, T a k e Take Take操作之后必须要有对应的 G i v e Give Give操作,这个后面在介绍互斥量和递归互斥量的时候会介绍。还有就是任务通知( T a s k N o t i f i c a t i o n s Task\quad Notifications TaskNotifications)在某些应用场景下也可以用来替代二进制信号量的应用,它也比二进制信号量更快且更轻量化,这个后面介绍任务通知( T a s k N o t i f i c a t i o n s Task\quad Notifications TaskNotifications)的时候再说。
基于上面描述的二进制信号量应用的例子,我们来看一个实际的例子。在这个例子中,一个二进制信号量用来通知任务 U S A R T 1 USART1 USART1是否已经收到了上位机发送的字符,如果任务对这个二进制信号量的 T a k e Take Take操作成功了则表明 U S A R T 1 USART1 USART1已经收到了上位机发送的字符,这时任务会打印出这个收到的字符(这里为了简单起见,上位机每次只发送一个字符),如果 T a k e Take Take操作没有成功,则任务会进入无限阻塞状态,下次 U S A R T 1 USART1 USART1收到上位机发送的字符的时候会触发接收中断并在中断中对二进制信号量进行 G i v e Give Give操作,这样会唤醒处于阻塞状态的任务来打印出接收到的字符,当这个任务再次尝试对这个二进制信号量进行 T a k e Take Take操作的时候,如果此时上位机没有接收到字符,那么任务会再次进入无限阻塞状态,直到下次上位机再次接收到字符的时候才会把它唤醒。主要的代码如下。
uint8_t received_character=0;
SemaphoreHandle_t xSemaphore = NULL;
/* Define a task that performs an action each time the USART1 interrupt occurs(USART1 has received characters). The Interrupt processing is deferred to this task. The task is synchronized with the interrupt using a semaphore. */
void Usart1ReceivingProcessingTask( void * pvParameters )
{
/* It is assumed the semaphore has already been created outside of this task. */
while(1)
{
printf("This the USART1 data processing task.\r\n");
/* Wait for the next event. */
if( xSemaphoreTake( xSemaphore, portMAX_DELAY ) == pdTRUE )
{
/* The event has occurred, process it here. */
printf("USART1 received character is %c.\r\n",received_character);
/* Processing is complete, return to wait for the next event. */
}
}
}
/* The USART1 ISR that defers its received characters processing to a task by using a semaphore to indicate when events that require processing have occurred. */
void USART1_IRQHandler( void * pvParameters )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* The event has occurred, use the semaphore to unblock the task so the task can process the event. */
xSemaphoreGiveFromISR( xSemaphore, &xHigherPriorityTaskWoken );
/* Clear the interrupt here. */
received_character=USART_ReceiveData(USART1);
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
/* Now the task has been unblocked a context switch should be performed if
xHigherPriorityTaskWoken is equal to pdTRUE. NOTE: The syntax required to perform
a context switch from an ISR varies from port to port, and from compiler to
compiler. Check the web documentation and examples for the port being used to
find the syntax required for your application. */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
int main(void)
{
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4);
NVIC_InitStruct.NVIC_IRQChannel=USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=7;
NVIC_InitStruct.NVIC_IRQChannelSubPriority=0;
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStruct);
uart_init(115200);
printf("Binary semaphore demo start.\r\n");
/* The semaphore is created to hold a maximum of 3 structures of type Data_t. */
xSemaphore = xSemaphoreCreateBinary();
if( xSemaphore != NULL )
{
/* Create the task that will process the received character of USART1. */
xTaskCreate( Usart1ReceivingProcessingTask, "Receiver", 1000, NULL, 1, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
}
else
{
/* The semaphore could not be created. */
}
/* If all is well then main() will never reach here as the scheduler will
now be running the tasks. If main() does reach here then it is likely that
there was insufficient heap memory available for the idle task to be created.
Chapter 2 provides more information on heap memory management. */
while(1);
}
这种把部分处理放在任务中进行的操作( U S A R T 1 USART1 USART1的数据接收放在中断中进行,但是数据的处理放在任务中进行)在 F r e e R T O S FreeRTOS FreeRTOS的官方文档里面有一个专门的称呼 D e f e r r e d I n t e r r u p t P r o c e s s i n g Deferred\quad Interrupt\quad Processing DeferredInterruptProcessing,如图9所示。前面的实际的例子中还有一点需要注意的是 N V I C _ P r i o r i t y G r o u p C o n f i g ( N V I C _ P r i o r i t y G r o u p _ 4 ) ; NVIC\_PriorityGroupConfig( NVIC\_PriorityGroup\_4); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);的调用要放在 N V I C _ I n i t ( & N V I C _ I n i t S t r u c t ) ; NVIC\_Init(\& NVIC\_InitStruct); NVIC_Init(&NVIC_InitStruct);的前面,因为 N V I C _ I n i t ( & N V I C _ I n i t S t r u c t ) ; NVIC\_Init(\& NVIC\_InitStruct); NVIC_Init(&NVIC_InitStruct);里面的初始化配置需要用到 N V I C _ P r i o r i t y G r o u p C o n f i g ( N V I C _ P r i o r i t y G r o u p _ 4 ) ; NVIC\_PriorityGroupConfig( NVIC\_PriorityGroup\_4); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);的配置,否则可能会有坑。
/*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
计数信号量和二进制信号量区别不大,最大的区别是创建的队列的长度不再限制为1,而是可以大于1,其它基本没有太大区别。计数信号量主要用于两种场景:
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,UBaseType_t uxInitialCount );
SemaphoreHandle_t xCountingSemaphore = NULL;
/* Define a task that try getting an available parking space . */
static void Take_Task(void* parameter)
{
BaseType_t xReturn = pdTRUE;
while (1)
{
if( Key_Scan(1) == KEY1_PRES )
{
xReturn = xSemaphoreTake(xCountingSemaphore,0);
if ( pdTRUE == xReturn )
printf( "KEY1 has been pressed,get an available parking space.\r\n" );
else
printf( "KEY1 has been pressed,but all parking spaces are full.\r\n" );
}
vTaskDelay(pdMS_TO_TICKS(20));
}
}
/* Define a task that try releasing a parking space. */
static void Give_Task(void* parameter)
{
BaseType_t xReturn = pdTRUE;
while (1)
{
if( Key_Scan(1) == KEY0_PRES )
{
xReturn = xSemaphoreGive(xCountingSemaphore);
if ( pdTRUE == xReturn )
printf( "KEY0 has been pressed,release a parking space successfully.\r\n" );
else
printf( "KEY0 has been pressed,but the maximum number of parking spaces is 5 and now the number of available parking spaces is 5.Parking spaces release failed.\r\n" );
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
int main(void)
{
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4);
KEY_Init();
uart_init(115200);
printf("Counting semaphore demo start.\r\n");
/* The counting semaphore is created to have a maximum count value of 5, and an initial count value of 5 for 5 available resources. */
xCountingSemaphore = xSemaphoreCreateCounting(5,5);
if( xCountingSemaphore != NULL )
{
xTaskCreate( Take_Task, "Take", 1000, NULL, 2, NULL );
xTaskCreate( Give_Task, "Give", 1000, NULL, 3, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
}
else
{
/* The semaphore could not be created. */
}
/* If all is well then main() will never reach here as the scheduler will
now be running the tasks. If main() does reach here then it is likely that
there was insufficient heap memory available for the idle task to be created.
Chapter 2 provides more information on heap memory management. */
while(1);
}
/*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
前面也提到过二进制信号量更适用于任务与任务间或任务与中断之间的同步而互斥量更适用于简单的互斥操作。当互斥量用于互斥操作的时候,互斥量可以看做是和一个被两个或多个任务共享的资源(这里和前面的计数信号量里面介绍的资源管理不同的是这里的资源就一个,如果这个资源被某个任务获取了权限使用的话,其它任务就得等待该任务使用完之后才能使用,而计数信号量里面介绍的资源管理里面的资源是可以有多个的)相关的令牌( T o k e n Token Token),当某个任务想要获取该资源的时候必须首先获取该令牌(对应于 T a k e Take Take操作),成为该令牌的持有者,然后才有权限访问该资源,当使用完该资源之后,任务必须(这一点也是互斥量和二进制信号量的一个区别点,对于互斥量同一个任务 T a k e Take Take操作成功之后必须要有对应的 G i v e Give Give操作来释放该资源以便其它任务可以使用,但是对于二进制信号量来说的话就没有强制同一个任务 T a k e Take Take操作成功之后必须要有对应的 G i v e Give Give操作)返回该令牌(对应于 G i v e Give Give操作),只有这样其它想要获取该资源的任务才能获取到该令牌,成为该令牌的持有者,从而获得该资源的访问权限。 F r e e R T O S FreeRTOS FreeRTOS的官方文档里面的图10到图15比较详细的描述了以上流程。
互斥量是一种特殊的二进制信号量,它们之间的另一个较大的区别是互斥量支持优先级继承机制而二进制信号量不支持。这里和优先级继承机制相关的还有优先级翻转的概念,因此我们首先来看一下优先级翻转。这里我们直接上 F r e e R T O S FreeRTOS FreeRTOS官方文档的图,如图16所示。这里一种有三个任务,任务 H P HP HP,任务 M P MP MP和任务 L P LP LP,任务 H P HP HP的优先级最高,任务 M P MP MP的优先级中等,任务 L P LP LP的优先级最低,这三个任务共享一个互斥资源且此时用二进制信号量来对互斥资源进行管理。现在假设有这样一种场景:
那究竟什么是优先级翻转,优先级翻转就是高优先级的任务反而需要等待低优先级的任务,这个是和 F r e e R T O S FreeRTOS FreeRTOS的设计理念相违背的,因为设计上高优先级的任务要优先执行且可以抢占低优先级的任务。我们在设计嵌入式系统应用的时候应该尽量避免优先级翻转的出现。互斥量的优先级继承机制就是为了减少优先级翻转造成的高优先级的任务等待低优先级的任务的时间,注意这里是减少等待的时间并不是完全消除。还有互斥量不能在中断中使用,因为这里的优先级继承机制的优先级指的是任务的优先级并不是中断的优先级,还有就是中断里面是不允许阻塞时间的。那么优先级继承机制是如何实现这种等待时间的减少的,假设现在有一个低优先级的任务获得了互斥量管理的共享资源的权限且还没有使用完该资源,与此同时有多个比它优先级高的任务试图获取管理的共享资源的互斥量来访问这个共享资源,因为这个互斥量已经被低优先级的任务占有了,因此这些高优先级的任务会进入阻塞等待状态,那么优先级继承机制核心点是此时该获得了互斥量的低优先级的任务的优先级暂时会被设置为所有因为等待被低优先级的任务占有的互斥量的高优先级任务中优先级最高的任务的优先级。图16的例子是使用二进制信号量来进行互斥资源管理的,二进制信号量是不支持优先级继承机制的,下面我们来看一下使用具有优先级继承机制的互斥量之后,图16中的例子的运行情况又是什么样子的,这里假设任务 H P HP HP的优先级为5,任务 M P MP MP的优先级为4,任务 L P LP LP的优先级为3。这里我们直接上 F r e e R T O S FreeRTOS FreeRTOS官方文档的图,如图17所示。
对比图16和图17中的两个例子,图17中在具有优先级继承机制下,任务 H P HP HP等待任务 L P LP LP释放管理共享资源的互斥量的时间明显减少。
/*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
但是互斥量也不是完美的,在使用互斥量用于管理对互斥资源访问的时候有可能会造成死锁( D e a d l o c k Deadlock Deadlock)的情况。那什么是死锁,我们来看一下下面的场景:
以上场景最后的结果是任务 A A A等待任务 B B B释放互斥量 Y Y Y而任务 B B B又在等待任务 A A A释放互斥量 X X X,这样搞得最后两个任务都动不了。和优先级翻转一样,死锁最好的解决办法就是在设计之初从系统设计上保保证不出现死锁。还有就是 T a k e Take Take操作或类似的操作的参数 x T i c k s T o W a i t xTicksToWait xTicksToWait最好不要设置为 p o r t M A X _ D E L A Y portMAX\_DELAY portMAX_DELAY,而是设置为比理论上需要等待的时间的最大值大一点,这样超时的时候可以根据返回的错误状态来检测到设计的错误。
以上死锁的场景发生在两个任务之间,但是死锁也有可能发生在同一个任务,这种情况发生在当一个任务已经通过 T a k e Take Take操作成功获得了互斥量的使用权限,但是此时又一次或多次的进行 T a k e Take Take操作,此时这个任务就会处于等待它自己释放它自己占用的互斥量的死锁状态。递归互斥量可以避免一个任务陷入等待它自己释放它自己占用的互斥量的死锁状态。占有递归互斥量的任务可以对递归互斥量多次进行 T a k e Take Take操作,但是如果要释放递归互斥量,那么 G i v e Give Give操作要和 T a k e Take Take操作的次数一样,比如占有递归互斥量的任务已经对递归互斥量进行了10次 T a k e Take Take操作,那么只有这个占有递归互斥量的任务对这个递归互斥量同样进行10次 G i v e Give Give操作之后这个任务占有的递归互斥量才算是释放了。递归互斥量和互斥量的属性基本一样,除了可以多次进行 T a k e Take Take操作和 G i v e Give Give操作( T a k e Take Take操作会将图18中的递归互斥量的队列信息结构体的元素 u x R e c u r s i v e C a l l C o u n t uxRecursiveCallCount uxRecursiveCallCount的值加一, G i v e Give Give操作会将图18中的递归互斥量的队列信息结构体的元素 u x R e c u r s i v e C a l l C o u n t uxRecursiveCallCount uxRecursiveCallCount的值减一,当元素 u x R e c u r s i v e C a l l C o u n t uxRecursiveCallCount uxRecursiveCallCount的值为0的时候就说明这个递归互斥量被释放了,元素 x M u t e x H o l d e r xMutexHolder xMutexHolder就是占用互斥量或递归互斥量的任务的任务句柄),递归互斥量也具有优先级继承机制且不能在中断中使用。
本小节的讲解就到这里,更多细节可以参考野火的讲解,图19中的文档的第6章和第7章以及图20中和信号量相关的 F r e e R T O S FreeRTOS FreeRTOS的接口以及使用介绍。本小节上面涉及到的实际的例子的工程代码在这里。