FreeRTOS 信号量

发布时间 2023-05-25 13:51:39作者: wxk1213

二值信号量

  二值信号量通常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是还是有一些细微的差别,互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。

  和队列一样,信号量 API 函数允许设置一个阻塞时间,阻塞时间是当任务获取信号量的时候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同一个信号量上的话那么优先级最高的哪个任务优先获得信号量,这样当信号量有效的时候高优先级的任务就会解除阻塞状态。 

  二值信号量其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空的。任务和中断使用这个特殊队列不用在乎队列中存的是什么消息,只需要知道这个队列是满的还是空的。可以利用这个机制来完成任务与中断之间的同步。

 

计数型信号量

1、事件计数

  在这个场合中,每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数值),其他任务会获取信号量(信号量计数值减一,信号量值就是队列结构体成员变量uxMessagesWaiting)来处理事件。在这种场合中创建的计数型信号量初始计数值为 0。 

2、资源管理

  在这个场合中,信号量值代表当前资源的可用数量,比如停车场当前剩余的停车位数量。一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减一。当信号量值为 0 的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量值会加一。在这个场合中创建的计数型信号量初始值应该是资源的数量,比如停车场一共有 100 个停车位,那么创建信号量的时候信号量值就应该初始化为 100。 

 

函数 xQueueCreateCountingSemaphore()

QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount,
                                                const UBaseType_t uxInitialCount )
{
    QueueHandle_t xHandle;
    configASSERT( uxMaxCount != 0 );
    configASSERT( uxInitialCount <= uxMaxCount );
    xHandle = xQueueGenericCreate( uxMaxCount,\ //创建一个队列,长度为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;
}

 

 

优先级反转

在使用二值信号量的时候会遇到很常见的一个问题——优先级翻转,优先级翻转在可剥夺内核中是非常常见的,在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能会导致严重的后果.

(1) 任务 H 和任务 M 处于挂起状态,等待某一事件的发生,任务 L 正在运行。
(2) 某一时刻任务 L 想要访问共享资源,在此之前它必须先获得对应该资源的信号量。
(3) 任务 L 获得信号量并开始使用该共享资源。
(4) 由于任务 H 优先级高,它等待的事件发生后便剥夺了任务 L 的 CPU 使用权。
(5) 任务 H 开始运行。
(6) 任务 H 运行过程中也要使用任务 L 正在使用着的资源,由于该资源的信号量还被任务L 占用着,任务 H 只能进入挂起状态,等待任务 L 释放该信号量。
(7) 任务 L 继续运行。
(8) 由于任务 M 的优先级高于任务 L,当任务 M 等待的事件发生后,任务 M 剥夺了任务L 的 CPU 使用权。
(9) 任务 M 处理该处理的事。
(10) 任务 M 执行完毕后,将 CPU 使用权归还给任务 L。
(11) 任务 L 继续运行。
(12) 最终任务 L 完成所有的工作并释放了信号量,到此为止,由于实时内核知道有个高优先级的任务在等待这个信号量,故内核做任务切换。
(13) 任务 H 得到该信号量并接着运行。

  在这种情况下,任务 H 的优先级实际上降到了任务 L 的优先级水平。因为任务 H 要一直等待直到任务 L 释放其占用的那个共享资源。由于任务 M 剥夺了任务 L 的 CPU 使用权,使得任务 H 的情况更加恶化,这样就相当于任务 M 的优先级高于任务 H,导致优先级翻转。 

 

 

 

互斥信号量

  互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。在互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。

  当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。 

  优先级继承并不能完全的消除优先级翻转,它只是尽可能的降低优先级翻转带来的影响。硬实时应用应该在设计之初就要避免优先级翻转的发生。互斥信号量不能用于中断服务函数中,原因如下:
  ● 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
  ● 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。 

 

  释 放 互 斥 信 号 量 的 时 候 和 二 值 信 号 量 、 计 数 型 信 号 量 一 样 , 都 是 用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 ) //当前操作的是互斥信号量
            {
                xReturn = xTaskPriorityDisinherit( ( void * ) pxQueue->pxMutexHolder );
                //调用函数xTaskPriorityDisinherit()处理互斥信号量的优先级继承问题。
                pxQueue->pxMutexHolder = NULL; 
                //互斥信号量释放以后,互斥信号量就不属于任何任务了,所以 pxMutexHolder 要指向NULL。
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* configUSE_MUTEXES */
    }
    /*********************************************************************/
    /*************************省略掉其他处理代码**************************/
    /*********************************************************************/
    pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;
    return xReturn;
}

 

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

BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder )
{
    TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;
    BaseType_t xReturn = pdFALSE;
    if( pxMutexHolder != NULL )
    {
        //当一个任务获取到互斥信号量以后就会涉及到优先级继承的问题,正在释放互斥
        //信号量的任务肯定是当前正在运行的任务 pxCurrentTCB。
        configASSERT( pxTCB == pxCurrentTCB );
        configASSERT( pxTCB->uxMutexesHeld );
        ( pxTCB->uxMutexesHeld )--; //可能获取多个互斥信号量,没事放一次减一次
        //是否存在优先级继承?如果存在的话任务当前优先级肯定和任务基优先级不同。
        if( pxTCB->uxPriority != pxTCB->uxBasePriority ) 
            {
                //当前任务只获取到了一个互斥信号量
                if( pxTCB->uxMutexesHeld == ( UBaseType_t ) 0 ) 
                {
                    if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) //当前任务恢复为原来的优先级以后再加入到就绪表中
                    { 
                        taskRESET_READY_PRIORITY( pxTCB->uxPriority ); 
                        //如果任务继承来的这个优先级对应的就绪表中没有其他任务的话就将取消这个优先级的就绪态
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                    //使用新的优先级将任务重新添加到就绪列表中
                    traceTASK_PRIORITY_DISINHERIT( pxTCB, pxTCB->uxBasePriority );
                    pxTCB->uxPriority = pxTCB->uxBasePriority; //重新设置任务的基优先级
                    /* 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 ), \ //复位任务的事件列表项
                    ( TickType_t ) configMAX_PRIORITIES - \
                    ( TickType_t ) pxTCB->uxPriority );
                    prvAddTaskToReadyList( pxTCB ); //将优先级恢复后的任务重新添加到任务就绪表中
                    xReturn = pdTRUE; //返回pdTRUE,表示需要进行任务调度
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
    return xReturn;
}

 

获取互斥信号量xSemaphoreTake()(实际执行信号量获取的函数是 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 ) (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 ) //获取互斥信号量
                {
                    if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
                    {
                        pxQueue->pxMutexHolder = //成功,标记互斥信号量所有者
                         ( 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 );
                        //此函数会判断当前任务的任务优先级是否比正在拥有互斥信号量的那个任务的任务优先级高,如果是的话就
                        //会把拥有互斥信号量的那个低优先级任务的优先级调整为与当前任务相同的优先级
                    }
                    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();
            }
        }
    }
}

 我们举个例子来简单的演示一下这个过程,假设现在有两个任务 HighTask 和 LowTask,HighTask 的任务优先级为 4,LowTask 的任务优先级为 2。这两个任务都会操同一个互斥信号量 Mutex,LowTask 先获取到互斥信号量 Mutex。此时任务 HighTask 也要获取互斥信号量 Mutex,任务 HighTask 调用xSemaphoreTake()尝试获取互斥信号量 Mutex,发现此互斥信号量正在被任务 LowTask 使用,并LowTask 的任务优先级为 2,比自己的任务优先级小,因为任务 HighTask 就会将 LowTask的任务优先级调整为与自己相同的优先级,即 4,然后任务 HighTask 进入阻塞态等待互斥信号量有效。