文章目录
- 一、互斥信号量简介
- 二、创建互斥信号量
- 1. xSemaphoreCreateMutex()
- 2. xSemaphoreCreateMutexStatic()
- 三、互斥信号量创建过程分析
- 四、释放互斥信号量
- 五、获取互斥信号量
一、互斥信号量简介
互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。在互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。
互斥信号量使用和二值信号量相同的 API 操作函数,所以互斥信号量也可以设置阻塞时间,不同于二值信号量的是互斥信号量具有优先级继承的特性。当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。
优先级继承并不能完全的消除优先级翻转,它只是尽可能的降低优先级翻转带来的影响。硬实时应用应该在设计之初就要避免优先级翻转的发生。互斥信号量不能用于中断服务函数中,原因如下:
● 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
● 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
二、创建互斥信号量
FreeRTOS 提供了两个互斥信号量创建函数,如下表所示:
1. xSemaphoreCreateMutex()
此函数用于创建一个互斥信号量,所需要的内存通过动态内存管理方法分配。此函数本质是一个宏,真正完成信号量创建的是函数 xQueueCreateMutex(),此函数原型如下:
SemaphoreHandle_t xSemaphoreCreateMutex( void )
参数:
无。
返回值:
NULL: 互斥信号量创建失败。
其他值: 创建成功的互斥信号量的句柄。
2. xSemaphoreCreateMutexStatic()
此函数也是创建互斥信号量的,只不过使用此函数创建互斥信号量的话信号量所需要的RAM 需要由用户来分配,此函数是个宏,具体创建过程是通过函数 xQueueCreateMutexStatic ()来完成的,函数原型如下:
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer )
参数:
pxMutexBuffer: 此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:
NULL: 互斥信号量创建失败。
其他值: 创建成功的互斥信号量的句柄。
三、互斥信号量创建过程分析
这里只分析动态创建互斥信号量函数 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,\ (1)
ucQueueType );
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)、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()释放一次信号量,说明互斥信号量默认就是有效的!互斥信号量创建完成以后如下图所示:
四、释放互斥信号量
释放互斥信号量的时候和二值信号量 、计数型信号量一样,都是 用的函数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 */
}
/*********************************************************************/
/*************************省略掉其他处理代码**************************/
/*********************************************************************/
pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;
return xReturn;
}
(1)、当前操作的是互斥信号量。
(2)、调用函数 xTaskPriorityDisinherit()处理互斥信号量的优先级继承问题。
(3)、互斥信号量释放以后,互斥信号量就不属于任何任务了,所以 pxMutexHolder 要指向NULL。
在 来 看 一 下 函 数 xTaskPriorityDisinherit() 是怎么具体的处理优先级继承的,函数xTaskPriorityDisinherit()代码如下:
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)
/* Reset the event list item value. It cannot be in use for
any other purpose if this task is running, and it must be
running to give back the mutex. */
listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), \ (8)
( TickType_t ) configMAX_PRIORITIES - \
( TickType_t ) pxTCB->uxPriority );
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,表示需要进行任务调度。
五、获取互斥信号量
获取互斥信号量的函数同获取二值信号量和计数型信号量的函数相同,都是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 = (6)
( int8_t * ) pvTaskIncrementMutexHeldCount();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_MUTEXES */
//查看是否有任务因为入队而阻塞,如果有的话就需要解除阻塞态。
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == (7)
pdFALSE )
{
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 ) ) == (9)
pdFALSE )
{
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
{
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 ), (15)
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)、调用函数 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()用于获取互斥信号量的话,此函数执行到这里说明互斥信号量正在被其他的任务占用。此函数和 14.8.4 小节中的函数 xTaskPriorityDisinherit()过程相反。此函数会判断当前任务的任务优先级是否比正在拥有互斥信号量的那个任务的任务优先级高,如果是的话就会把拥有互斥信号量的那个低优先级任务的优先级调整为与当前任务相同的优先级!
(15)、经过(12)步判断,队列依旧为空,那么就将任务添加到列表 xTasksWaitingToReceive中。
在上面的分析中,红色部分就是当函数 xQueueGenericReceive()用于互斥信号量的时候的处理过程,其中(13)和(14)条详细的分析了互斥信号量优先级继承的过程。我们举个例子来简单的演示一下这个过程,假设现在有两个任务 HighTask 和 LowTask,HighTask 的任务优先级为 4,LowTask 的任务优先级为 2。这两个任务都会操同一个互斥信号量 Mutex,LowTask 先获取到互斥信号量 Mutex。此时任务 HighTask 也要获取互斥信号量 Mutex,任务HighTask 调用函数xSemaphoreTake()尝试获取互斥信号量 Mutex,发现此互斥信号量正在被任务 LowTask 使用,并且 LowTask 的任务优先级为 2,比自己的任务优先级小,因为任务 HighTask 就会将 LowTask的任务优先级调整为与自己相同的优先级,即 4,然后任务 HighTask 进入阻塞态等待互斥信号量有效。