天天看點

FreeRTOS學習 信号量

信号量

FreeRTOS 學習倉庫:​​https://gitee.com/killerp/free-rtos_-study​​

在深入了解了消息隊列後,信号量也就很容易學習了。因為信号量就是使用消息隊列實作的。

信号量是特殊的消息隊列,其隻部分利用了隊列結構體,信号量沒有隊列存儲區域,是以信号量不能用來傳遞任務間的資料。但可以利用消息隊列的其他特點如:對共享資料的保護,阻塞等待機制等,實作任務之間的同步,對共享資料的互斥通路。

一、信号量的分類

不同類型的信号量有各自的用途。

二值信号量

二值信号量隻有 0 和 1 兩個狀态,通常用于任務之間、任務和中斷之間的同步。二值信号量初始化時為0,當任務or中斷釋放二值信号量,信号量值為1,接收任務解除阻塞進入就緒。通常是一方固定釋放信号量,一方固定讀取信号量,實作雙方的同步。

互斥信号量

互斥信号量主要用于對共享資料的互斥通路,也就是保證任務對資源的獨占性,在同一時刻,一個共享資料隻能被一個任務通路。

是以互斥信号量在初始化時,需要初始化為1,表示資源的存在。

任務擷取互斥信号信号成功後,就能放心地操作資源。其他任務擷取互斥信号就會失敗,而進入阻塞。通常情況下,多個任務通路一個共享資源,每個任務都需要先擷取互斥信号量,在處理完資料後,釋放互斥信号量。這與二值信号量不一樣。

遞歸互斥信号量

遞歸互斥信号量是在互斥信号的基礎上,允許擁有信号量的任務遞歸地擷取/釋放信号量。

計數信号量

計數信号量類似于互斥信号量,用于對資源的保護,通常該資源可被若幹個任務同時擁有。

二、信号量的建立、釋放、擷取

信号量的實作依賴于隊列,在FreeRTOS中,僅用一個semphr.h頭檔案,通過宏定義來實作信号量的建立,發送,接收。

2.1、建立信号量

建立二值信号量 實際上就是建立一個長度為1,大小為0的隊列 ,此時信号量的值為0

#define xSemaphoreCreateBinary()    xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )      

2.2、優先級反轉

首先了解簡單的優先級反轉:

A的優先級為1,B為10,C為5,A擷取到信号量,正在操作資源。此時中斷導緻任務排程,切換到B執行,B因為請求信号量失敗而進入阻塞。此時發生任務排程,C運作知道讓出CPU,A繼續運作釋放信号量,最後B才能繼續運作。

假設沒有C任務,那麼A、B的運作是正常的。A首先擷取資源,把資源處理完成後再由B下一步處理,這種情況下,A、B執行的時間是可确定的,符合實時作業系統的要求。

但是第三者C的加入,使得任務A、B的執行時間不确定了。C有時會搶占A的CPU,使A、B的執行時間變得長;有時又不止一個C、可能有其他D、E、F、G來搶占A,是以A、B的執行時間變得不可确定,這對實時作業系統是災難性的。

解決的辦法是使用優先級繼承:即把B的優先級暫時複制給A,這樣C就無法插足A、B之間的感情了。需要注意的是,任務可能發生多次的優先級反轉,是以優先級繼承一定要繼承最高優先級。

2.3、擷取信号量

由于互斥信号量的優先級繼承是發生在信号量擷取的過程中,是以擷取信号量的實作需要關注優先級繼承邏輯。代碼如下

BaseType_t xQueueSemaphoreTake( QueueHandle_t xQueue,
                                TickType_t xTicksToWait )
{
    BaseType_t xEntryTimeSet = pdFALSE;
    TimeOut_t xTimeOut;
    Queue_t * const pxQueue = xQueue;

    BaseType_t xInheritanceOccurred = pdFALSE;  //優先級繼承是否發送的标志

    configASSERT( ( pxQueue ) );

    //uxItemSize為0時,隊列才表示為信号量
    configASSERT( pxQueue->uxItemSize == 0 );

    //進入循環
    for( ; ; )
    {
        //進入臨界區
        taskENTER_CRITICAL();
        {

            //uxMessagesWaiting 就是信号量的值
            const UBaseType_t uxSemaphoreCount = pxQueue->uxMessagesWaiting;


            //檢查信号量是否有效
            if( uxSemaphoreCount > ( UBaseType_t ) 0 )
            {
                traceQUEUE_RECEIVE( pxQueue );

                //信号量有效、數值減一
                pxQueue->uxMessagesWaiting = uxSemaphoreCount - ( UBaseType_t ) 1;
                
                    {
                        //若是互斥信号量,還需要做優先級繼承
                        if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
                        {
                            //記錄目前信号量的擁有者
                            pxQueue->u.xSemaphore.xMutexHolder = pvTaskIncrementMutexHeldCount();
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                

                //檢測是否有任務阻塞在信号量釋放隊列,若有則恢複任務
                if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
                {
                    if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
                    {
                        //恢複的任務優先級更高,發送搶占
                        queueYIELD_IF_USING_PREEMPTION();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
                //成功擷取 退出函數
                taskEXIT_CRITICAL();
                return pdPASS;
            }
            else    //信号量被使用完了
            {
                //不需要阻塞等待信号量
                if( xTicksToWait == ( TickType_t ) 0 )
                {
                                 
                        {
                            //若優先級反轉已經發生,那麼調用者必須有一個阻塞時間,故此時xInheritanceOccurred必須為false
                            configASSERT( xInheritanceOccurred == pdFALSE );
                        }
                    
                    //不需要等待直接退出
                    taskEXIT_CRITICAL();
                    traceQUEUE_RECEIVE_FAILED( pxQueue );
                    return errQUEUE_EMPTY;
                }
                else if( xEntryTimeSet == pdFALSE )
                {
                    //初始化逾時結構體
                    vTaskInternalSetTimeOutState( &xTimeOut );
                    xEntryTimeSet = pdTRUE;
                }
                else
                {
                    /* Entry time was already set. */
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        }
        taskEXIT_CRITICAL();
        
        vTaskSuspendAll();
        prvLockQueue( pxQueue );    //鎖定隊列,因為需要操作連結清單

        //逾時時間未到
        if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
        {

            //信号量為空
            if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
            {
                traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );
                
                    {
                        //若是互斥信号量、在進入阻塞前、需要做一次優先級繼承
                        if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
                        {
                            taskENTER_CRITICAL();
                            {
                                xInheritanceOccurred = xTaskPriorityInherit( pxQueue->u.xSemaphore.xMutexHolder );
                            }
                            taskEXIT_CRITICAL();
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                
                //将目前任務挂起到等待連結清單
                vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
                prvUnlockQueue( pxQueue );  //解鎖隊列

                //目前任務進入等待、恢複任務排程,切換下一個任務運作
                if( xTaskResumeAll() == pdFALSE )
                {
                    portYIELD_WITHIN_API();
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                //逾時時間未到且信号量有效了,重新進入循環擷取信号量,這種情況可能是其他任務釋放了信号量進而喚醒目前任務
                prvUnlockQueue( pxQueue );
                ( void ) xTaskResumeAll();
            }
        }
        else    //阻塞時間到了 此時任務已被核心移出等待隊列
        {
            /* Timed out. */
            prvUnlockQueue( pxQueue );
            ( void ) xTaskResumeAll();

            //若信号量仍然為空,則退出阻塞
            if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
            {                
                    {

                        //若發生了優先級反轉,需要恢複任務的優先級
                        if( xInheritanceOccurred != pdFALSE )
                        {
                            taskENTER_CRITICAL();
                            {
                                UBaseType_t uxHighestWaitingPriority;

                                //擷取隊列等待連結清單中的最高優先級、
                                uxHighestWaitingPriority = prvGetDisinheritPriorityAfterTimeout( pxQueue );
                                //恢複隊列的優先級為該最高優先級or任務自身優先級
                                vTaskPriorityDisinheritAfterTimeout( pxQueue->u.xSemaphore.xMutexHolder, uxHighestWaitingPriority );
                            }
                            taskEXIT_CRITICAL();
                        }
                    }
                

                traceQUEUE_RECEIVE_FAILED( pxQueue );
                //傳回擷取失敗
                return errQUEUE_EMPTY;
            }
            else    //阻塞時間到了,但同時信号量也有效,重新進入循環擷取信号量
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
    } /*lint -restore */
}      

2.4、釋放信号量

釋放信号量就是向隊列發送一個空消息、且不需要進入阻塞,隊列滿說明信号量有效,是以不需要阻塞。同時若是互斥信号量,則任務會恢複到原來的優先級。

#define xSemaphoreGive( xSemaphore )    xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )      

在中斷中釋放信号量與任務中的差別是:進入臨界區需要儲存中斷狀态,以及在中斷中不能進入阻塞,且隊列鎖定時不能操作隊列的連結清單。

BaseType_t xQueueGiveFromISR( QueueHandle_t xQueue,
                              BaseType_t * const pxHigherPriorityTaskWoken )
{
    BaseType_t xReturn;
    UBaseType_t uxSavedInterruptStatus; //儲存中斷狀态
    Queue_t * const pxQueue = xQueue;

    configASSERT( pxQueue );

    //用于信号量的釋放,故為0
    configASSERT( pxQueue->uxItemSize == 0 );

    //通常一個互斥信号量不會在中斷中釋放、而是是任務中。因為互斥信号量是資源的鎖,在中斷中一般不會通路共享的資源。
    configASSERT( !( ( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ) && ( pxQueue->u.xSemaphore.xMutexHolder != NULL ) ) );

    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
    //進入臨界區,儲存中斷狀态
    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
    {
        const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;

        //檢測是否能繼續釋放信号量
        if( uxMessagesWaiting < pxQueue->uxLength )
        {
            const int8_t cTxLock = pxQueue->cTxLock;

            traceQUEUE_SEND_FROM_ISR( pxQueue );

            //信号量值+1
            pxQueue->uxMessagesWaiting = uxMessagesWaiting + ( UBaseType_t ) 1;

            //隊列未鎖,可以操作連結清單
            if( cTxLock == queueUNLOCKED )
            {
                    {
                        //将等待信号量的任務恢複就緒
                        if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
                        {
                            if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
                            {
                                //需要退出中斷後進行任務排程
                                if( pxHigherPriorityTaskWoken != NULL )
                                {
                                    *pxHigherPriorityTaskWoken = pdTRUE;
                                }
                                else
                                {
                                    mtCOVERAGE_TEST_MARKER();
                                }
                            }
                            else
                            {
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
            }
            else
            {
                configASSERT( cTxLock != queueINT8_MAX );
                //隊列鎖定 标記
                pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );
            }

            xReturn = pdPASS;
        }
        else    //隊列滿,無法釋放信号量
        {
            traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );
            xReturn = errQUEUE_FULL;
        }
    }
    //退出臨界區 恢複中斷狀态
    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

    return xReturn;
}      

2.5、優先級繼承相關函數

a、優先級繼承

任務擷取互斥信号量時,進入阻塞,需要優先級繼承,信号量的擁有者繼承目前任務的優先級(若目前任務優先級較高)。

BaseType_t xTaskPriorityInherit(TaskHandle_t const pxMutexHolder)
{
    TCB_t *const pxMutexHolderTCB = pxMutexHolder;  //擷取鎖的擁有者
    BaseType_t xReturn = pdFALSE;   //傳回值表示是否發送繼承

    if (pxMutexHolder != NULL)
    {

        //擁有鎖的任務優先級低于目前(請求擷取信号量)任務的優先級 需要繼承優先級
        if (pxMutexHolderTCB->uxPriority < pxCurrentTCB->uxPriority)
        {
            //若擁有鎖的任務 所在的事件連結清單是按優先級排序的,則需要修改事件節點的值
            if ((listGET_LIST_ITEM_VALUE(&(pxMutexHolderTCB->xEventListItem)) & taskEVENT_LIST_ITEM_VALUE_IN_USE) == 0UL)
            {
                //設定節點的值為新的優先級
                listSET_LIST_ITEM_VALUE(&(pxMutexHolderTCB->xEventListItem), (TickType_t)configMAX_PRIORITIES - (TickType_t)pxCurrentTCB->uxPriority); 
            }
            else
            {
                //否則什麼也不做
                mtCOVERAGE_TEST_MARKER();
            }

            //若擁有鎖的任務是在就緒連結清單中,則需要将其移動到新的優先級的就緒連結清單
            if (listIS_CONTAINED_WITHIN(&(pxReadyTasksLists[pxMutexHolderTCB->uxPriority]), &(pxMutexHolderTCB->xStateListItem)) != pdFALSE)
            {
                if (uxListRemove(&(pxMutexHolderTCB->xStateListItem)) == (UBaseType_t)0)
                {
                    portRESET_READY_PRIORITY(pxMutexHolderTCB->uxPriority, uxTopReadyPriority);
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }

                //繼承目前任務優先級 加入就緒連結清單
                pxMutexHolderTCB->uxPriority = pxCurrentTCB->uxPriority;
                prvAddTaskToReadyList(pxMutexHolderTCB);
            }
            else
            {
                //擁有鎖的任務不在就緒連結清單中,隻需要繼承目前任務優先級
                pxMutexHolderTCB->uxPriority = pxCurrentTCB->uxPriority;
            }

            traceTASK_PRIORITY_INHERIT(pxMutexHolderTCB, pxCurrentTCB->uxPriority);

            //優先級繼承發送
            xReturn = pdTRUE;
        }
        else
        {
            //擁有鎖的任務高于目前任務的優先級,但上一次優先級小于目前任務,說明該任務之前被提升了一次優先級
            if (pxMutexHolderTCB->uxBasePriority < pxCurrentTCB->uxPriority)
            {
                //标記優先級反轉
                xReturn = pdTRUE;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    return xReturn;
}      

b、重置優先級

當等待擷取互斥信号的任務阻塞時間到達,需要退出等待時,調用以下函數,重新設定信号量擁有者的優先級。

void vTaskPriorityDisinheritAfterTimeout(TaskHandle_t const pxMutexHolder,
                                         UBaseType_t uxHighestPriorityWaitingTask)
{
    TCB_t *const pxTCB = pxMutexHolder;
    UBaseType_t uxPriorityUsedOnEntry, uxPriorityToUse; 
    const UBaseType_t uxOnlyOneMutexHeld = (UBaseType_t)1;

    if (pxMutexHolder != NULL)
    {
        configASSERT(pxTCB->uxMutexesHeld); //必須擁有鎖

        //uxPriorityToUse是新的優先級。新的優先級應該是等待鎖的任務中優先級最高的那個,這樣才能保證最高優先級的任務能在鎖釋放後第一時間得到運作
        if (pxTCB->uxBasePriority < uxHighestPriorityWaitingTask)
        {
            uxPriorityToUse = uxHighestPriorityWaitingTask;
        }
        else
        {
            uxPriorityToUse = pxTCB->uxBasePriority;
        }

        /* Does the priority need to change? */
        //優先級需要修改
        if (pxTCB->uxPriority != uxPriorityToUse)
        {

            if (pxTCB->uxMutexesHeld == uxOnlyOneMutexHeld)
            {

                configASSERT(pxTCB != pxCurrentTCB);


                traceTASK_PRIORITY_DISINHERIT(pxTCB, uxPriorityToUse);
                //儲存舊優先級
                uxPriorityUsedOnEntry = pxTCB->uxPriority;
                //設定新的優先級
                pxTCB->uxPriority = uxPriorityToUse;    


                //設定事件節點值
                if ((listGET_LIST_ITEM_VALUE(&(pxTCB->xEventListItem)) & taskEVENT_LIST_ITEM_VALUE_IN_USE) == 0UL)
                {
                    listSET_LIST_ITEM_VALUE(&(pxTCB->xEventListItem), (TickType_t)configMAX_PRIORITIES - (TickType_t)uxPriorityToUse);
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }

                //若鎖的擁有者處于就緒連結清單,由于改變了優先級,是以需要修改其優先級連結清單
                if (listIS_CONTAINED_WITHIN(&(pxReadyTasksLists[uxPriorityUsedOnEntry]), &(pxTCB->xStateListItem)) != pdFALSE)
                {
                    if (uxListRemove(&(pxTCB->xStateListItem)) == (UBaseType_t)0)
                    {
                        portRESET_READY_PRIORITY(pxTCB->uxPriority, uxTopReadyPriority);
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                    //插入新的就緒連結清單
                    prvAddTaskToReadyList(pxTCB);
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}      

c、恢複優先級

BaseType_t xTaskPriorityDisinherit(TaskHandle_t const pxMutexHolder)
{
    TCB_t *const pxTCB = pxMutexHolder;
    BaseType_t xReturn = pdFALSE;   //傳回真,需要任務排程

    if (pxMutexHolder != NULL)
    {
        //檢查輸入參數
        configASSERT(pxTCB == pxCurrentTCB);
        configASSERT(pxTCB->uxMutexesHeld); //必須擁有鎖
        (pxTCB->uxMutexesHeld)--;   //擁有互斥信号的數量-1


        //優先級不同則出現了優先級反轉
        if (pxTCB->uxPriority != pxTCB->uxBasePriority)
        {
            //當不持有互斥信号時才能恢複任務優先級
            if (pxTCB->uxMutexesHeld == (UBaseType_t)0)
            {

                //移出就緒連結清單
                if (uxListRemove(&(pxTCB->xStateListItem)) == (UBaseType_t)0)
                {
                    portRESET_READY_PRIORITY(pxTCB->uxPriority, uxTopReadyPriority);
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }

                traceTASK_PRIORITY_DISINHERIT(pxTCB, pxTCB->uxBasePriority);
                //恢複優先級
                pxTCB->uxPriority = pxTCB->uxBasePriority;

                //設定事件節點的值
                listSET_LIST_ITEM_VALUE(&(pxTCB->xEventListItem), (TickType_t)configMAX_PRIORITIES - (TickType_t)pxTCB->uxPriority); 
                //添加到新的就緒表
                prvAddTaskToReadyList(pxTCB);

                /* Return true to indicate that a context switch is required.
                     * This is only actually required in the corner case whereby
                     * multiple mutexes were held and the mutexes were given back
                     * in an order different to that in which they were taken.
                     * If a context switch did not occur when the first mutex was
                     * returned, even if a task was waiting on it, then a context
                     * switch should occur when the last mutex is returned whether
                     * a task is waiting on it or not. */
                //todo 傳回真使任務排程
                xReturn = pdTRUE;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    return xReturn;
}      

繼續閱讀