信号量
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;
}