天天看點

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

目錄

  • 1、 資源使用概況
  • 2、互斥方法之一:基本臨界區
    • 2.1、taskENTER_CRITICAL_FROM_ISR() 和taskEXIT_CRITICAL_FROM_ISR()
  • 3、互斥方法之二:挂起或鎖定排程程式
    • 3.1 vTaskSuspendAll()
    • 3.2 xTaskResumeAll()
  • 4 互斥方法三:互斥信号量(和二進制信号量)
    • 4.1 xSemaphoreCreateMutex()
    • 4.2 執行個體
    • 4.3 什麼叫優先級繼承
    • 4.4 關于死鎖
    • 4.5 遞歸互斥鎖
    • 4.6 互斥鎖和任務排程
  • 5、網守任務(Gatekeeper Tasks)
  • 總結

1、 資源使用概況

在多任務系統中,如果一個任務開始通路資源,但在退出運作狀态之前未完成其通路,則可能會出錯。如果任務使資源處于不一緻狀态,則任何其他任務或中斷對同一資源的通路都可能導緻資料損壞或其他類似問題。

以上的問題是很淺顯的。對外設的通路,對記憶體的通路等都可能出現以上的問題。

互斥

為了確定始終保持資料一緻性,必須使用“互斥”技術管理對任務之間或任務與中斷之間共享的資源的通路。目标是確定,一旦任務開始通路非可重入且非線程安全的共享資源,同一任務就可以獨占通路該資源,直到資源傳回到一緻狀态。

FreeRTOS提供了幾個可用于實作互斥的功能,但最好的互斥方法是(在可能的情況下,因為通常不實用)以不共享資源的方式設計應用程式,并且每個資源隻能從單個任務通路。

2、互斥方法之一:基本臨界區

什麼是臨界區:基本臨界區是分别被宏 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 調用包圍的代碼區域。

以這種方式實作的臨界區是提供互斥的一種非常粗略的方法。它們的工作方式是完全禁用中斷,或者禁用達到configMAX_SYSCALL_INTERRUPT_PRIORITY 設定的中斷優先級(這取決于所使用的 FreeRTOS 端口)的中斷。

由于搶先式上下文切換隻能在中斷中發生,是以,隻要中斷保持禁用狀态,調用 taskENTER_CRITICAL() 的任務就可以保證保持在運作狀态,直到退出臨界區。

在調用 taskENTER_CRITICAL() 和調用 taskEXIT_CRITICAL() 之間不能切換到另一個任務。中斷仍然可以在允許中斷嵌套的 FreeRTOS 端口上執行,但僅限于邏輯優先級高于配置設定給 configMAX_SYSCALL_INTERRUPT_PRIORITY 常量的值的中斷——并且不允許這些中斷調用 FreeRTOS API 函數。

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

臨界區必須保持非常短,否則将對中斷響應時間産生不利影響。對taskENTER_CRITICAL()的每次調用都必須與對taskEXIT_CRITICAL的調用緊密配對。

臨界區嵌套是安全的,因為核心會計算嵌套深度。隻有當嵌套深度歸零時,臨界區才會退出——即對之前的每一次 taskENTER_CRITICAL() 調用都執行了一次 taskEXIT_CRITICAL() 調用。

調用 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 是任務更改運作 FreeRTOS 的處理器的中斷啟用狀态的唯一合法方法。通過任何其他方式更改中斷啟用狀态将使宏的嵌套計數無效

2.1、taskENTER_CRITICAL_FROM_ISR() 和taskEXIT_CRITICAL_FROM_ISR()

taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 不會以“FromISR”結尾,是以不得從中斷服務例程中調用。

taskENTER_CRITICAL_FROM_ISR() 是 taskENTER_CRITICAL() 的中斷安全版本,taskEXIT_CRITICAL_FROM_ISR() 是 taskEXIT_CRITICAL() 的中斷安全版本。中斷安全版本僅适用于允許嵌套中斷的 FreeRTOS 端口——它們在不允許中斷嵌套的端口中已過時。

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

傳回值

傳回調用taskENTER_CRITIC_FROM_ISR()時的中斷掩碼狀态。必須儲存傳回值,以便将其傳遞到對taskEXIT_CRITICAL_FROM_ISR()的比對調用中。

void vAnInterruptServiceRoutine( void )
{

	UBaseType_t uxSavedInterruptStatus;  //這個變量存儲taskENTER_CRITICAL_FROM_ISR()的傳回值
 	/*ISR中的這部分可以被更高優先級的中斷所中斷. */
 	/* 使用 taskENTER_CRITICAL_FROM_ISR()保護ISR的這個區域。存儲taskENTER_CRITICAL_FROM_ISR()傳回值,并被傳遞給對應的 	 taskEXIT_CRITICAL_FROM_ISR(). */
 	
 	uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
 
 	/* 這部分内容處于taskENTER_CRITICAL_FROM_ISR()與 taskEXIT_CRITICAL_FROM_ISR()之間,是以隻會被優先級高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 所設定的值的中斷所中斷。 */
 
 	taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
 }
           

基本的臨界區進入非常快,退出非常快,而且總是确定性的,是以當受保護的代碼區域非常短時,它們的使用非常理想。

3、互斥方法之二:挂起或鎖定排程程式

這是另一種建立臨界區的方法。

可以通過暫停排程程式(指令vTaskSuspendAll())來建立臨界區域。暫停排程程式有時也稱為“鎖定”排程程式。

基本臨界區保護代碼區域不被其他任務和中斷通路。通過挂起排程程式實作的臨界區僅保護代碼區域不被其他任務通路,因為中斷仍然啟用。

如果關鍵部分太長,無法通過簡單地禁用中斷來實作,則可以通過暫停排程器來實作。然而,當排程程式被挂起時,中斷活動會使恢複(或“取消挂起”)排程程式成為一個相對較長的操作,是以必須考慮在每種情況下使用哪種方法最好。

3.1 vTaskSuspendAll()

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

排程程式通過調用vTaskSuspendAll()被挂起。挂起排程程式可防止發生上下文切換,但會啟用中斷。如果在排程程式挂起時中斷請求上下文切換,則該請求保持挂起狀态,并且僅在排程程式恢複(未挂起)時執行。

3.2 xTaskResumeAll()

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

描述

本函數恢複排程器活動,跟在先前調用的vTaskSuspendAll(),使排程器由挂起狀态過渡為活動狀态。

傳回值

pdTRUE : 排程器轉為活動狀态。這個轉換引起了阻塞中的上下文進行切換。

pdFALSE :排程程式轉換到 Active 狀态并且轉換沒有導緻發生上下文切換,或者排程程式由于對vTaskSuspendAll() 的嵌套調用而留在 Suspended 狀态。

注意

調用vTaskSuspendAll()使排程器被挂起。當排程器被挂起中斷仍然可用,但上下文切換不再進行。如果排程器挂起時有一個上下文切換請求,那麼這個請求将被保持挂起直到排程器被恢複。

對 vTaskSuspendAll() 和 xTaskResumeAll() 的調用嵌套是安全的,因為核心會計算嵌套深度。隻有當嵌套深度歸零時,排程程式才會恢複——也就是說,對于之前對 vTaskSuspendAll() 的每一次調用,都會執行一次xTaskResumeAll() 調用。

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

4 互斥方法三:互斥信号量(和二進制信号量)

Mutex 是一種特殊類型的二進制信号量,用于控制對兩個或多個任務之間共享的資源的通路。 MUTEX 一詞源于“MUTual EXclusion”。 configUSE_MUTEXES 必須在 FreeRTOSConfig.h 中設定為 1 才能使用互斥鎖。

在互斥場景中使用互斥鎖時,可以将互斥鎖視為與共享資源相關聯的令牌。對于合法通路資源的任務,它必須首先成功“擷取”令牌(成為令牌持有者)。當令牌持有者用完資源後,它必須“歸還”令牌。隻有當令牌被傳回後,另一個任務才能成功擷取令牌,然後安全地通路相同的共享資源。除非擁有令牌,否則不允許任務通路共享資源。

4.1 xSemaphoreCreateMutex()

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

描述:

該函數建立一個互斥類型的信号量,并傳回一個可以引用互斥的句柄。

每個互斥類型的信号量都需要少量的RAM來儲存信号量的狀态。如果使用SemaphoreCreateMutex()建立互斥體,則所需的RAM将自動從FreeRTOS堆中配置設定。如果使用xSemaphoreCreateMutexStatic()建立互斥鎖,則RAM由應用程式編寫器提供,這需要額外的參數,但允許在編譯時靜态配置設定RAM。

傳回值

NULL : 如果無法建立信号量則傳回NULL,因為沒有足夠的堆記憶體供FreeRTOS配置設定信号量資料結構。

除NULL外的其它值:已成功建立信号量。傳回的值是一個句柄,通過它可以引用建立的信号量。

注意事項

二進制信号量和互斥量非常相似,但确實有一些細微的差別。

互斥包含優先級繼承機制,而二進制信号量不包含。這使得二進制信号量成為實作同步(任務之間或任務與中斷之間)的更好選擇,而互斥體則是實作簡單互斥的更好選擇。

二進制信号量–用于同步的二進制信号量在成功“擷取”(獲得)後無需“傳回”。通過讓一個任務或中斷“給出”信号量,另一個任務“接受”信号量來實作任務同步。

互斥鎖–如果另一個優先級較高的任務試圖獲得相同的互斥鎖,則持有互斥鎖的任務的優先級将提高。已經持有互斥鎖的任務被稱為“繼承”試圖“擷取”同一互斥鎖任務的優先級。當互斥體被傳回時,繼承的優先級将被“取消繼承”(在持有互斥體時繼承了更高優先級的任務在傳回互斥體時将傳回其原始優先級)。

獲得互斥對象的任務必須始終傳回( xSemaphoreTake())互斥對象,否則任何其他任務都無法獲得相同的互斥對象。

互斥和二進制信号量都是使用SemaphoreHandle_t類型的變量引用的,并且可以在任何使用該類型參數的API函數中使用。

configSUPPORT_DYNAMIC_ALLOCATION必須在FreeRTOSConfig.h中設定為1,或者幹脆不定義,才能使用此函數。

4.2 執行個體

此示例建立了一個名為prvNewPrintString()的vPrintString()新版本,然後從多個任務調用新函數。prvNewPrintString()在功能上與vPrintString()相同,但使用互斥鎖而不是鎖定排程器來控制對标準輸出的通路。prvNewPrintString()的實作如下所示。

static void prvNewPrintString( const char *pcString )
{
 	/* 互斥信号量xMutex已在排程器啟動前被建立,具體見其它代碼. 
 	這裡用了portMAX_DELAY,使得如果得不到xMutex時,就一直阻塞。是以這裡要麼阻塞,要麼獲得信号量,是以這裡可以不用在其後去判斷獲得信号量	
 	是否成功。如果後面的阻塞等待時間參數不是portMAX_DELAY,那麼就需要在其後判斷擷取信号量是否成功 */
 	xSemaphoreTake( xMutex, portMAX_DELAY );
 	{
 		/* 隻有成功擷取互斥對象後,才會執行以下行。标準輸出現在可以自由通路,因為任何時候隻有一個任務可以擁有互斥鎖。 */
 		printf( "%s", pcString );
 		fflush( stdout );
 	
 	}
 	xSemaphoreGive( xMutex );/* 互斥鎖必須被主動給回。 */
}
           

prvNewPrintString()由prvPrintTask()實作的任務的兩個執行個體重複調用。每個調用之間使用随機延遲時間。task參數用于向任務的每個執行個體傳遞唯一字元串。prvPrintTask()的實作如下所示。

static void prvPrintTask( void *pvParameters )
{
	char *pcStringToPrint;
	const TickType_t xMaxBlockTimeTicks = 0x20;
 	pcStringToPrint = ( char * ) pvParameters;
 	for( ;; )
 	{
 		prvNewPrintString( pcStringToPrint );
 		
 		vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );
	 }
 }
           

prvPrintTask() 的兩個執行個體以不同的優先級建立,是以較低優先級的任務有時會被較高優先級的任務搶占。由于使用互斥鎖來確定每個任務都可以互斥地通路終端,是以即使發生搶占,顯示的字元串也将是正确的,不會被破壞。可以通過減少任務在阻塞狀态下花費的最長時間來增加搶占的頻率,該時間由 xMaxBlockTimeTicks 常量設定。

int main( void )
{
 	xMutex = xSemaphoreCreateMutex(); //建立互斥鎖,用于對标準輸出資源的管理
 	/* Check the semaphore was created successfully before creating the tasks. */
 	if( xMutex != NULL )
 	{
 	/* 建立兩個寫入stdout任務的執行個體。兩個執行個體被傳入不同的字元串做為參數,用于任務的列印輸出。兩個任務優先級不同,使他們能互相搶占。*/
 	xTaskCreate( prvPrintTask, "Print1", 1000, 
 	"Task 1 ***************************************\r\n", 1, NULL );
 	xTaskCreate( prvPrintTask, "Print2", 1000, 
 	"Task 2 ---------------------------------------\r\n", 2, NULL );
 	
 	vTaskStartScheduler();
 	}
 	for( ;; );
}
           

以上代碼的輸出結果哪下:

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

4.3 什麼叫優先級繼承

要明白互斥信号量所具有的優先級繼承概念,首先要知道什麼叫優先級反轉。

優先級反轉

當較低優先級的任務擁有了互斥鎖後,則按常理較高優先級的任務必須等待這個較低優先級的任務放棄對互斥體的控制。是以更高優先級的任務是以這種方式被較低優先級任務延遲稱為“優先級反轉”。如果中等優先級任務開始執行而高優先級任務正在等待信号量(結果将是高優先級任務等待低優先級任務),則這種不良行為将被進一步誇大,而低優先級任務甚至無法執行。執行。這種最壞的情況如下圖 所示。

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

1、三個不同的優先級任務。LP,MP,HP分别指低優先級,中等優先級,最高優先級。LP在執行時在被HP搶占前就獲得了互斥鎖。

2、HP搶占了LP後,想要去獲得互斥鎖,但因為此時是LP占用互斥鎖,是以HP隻能進入阻塞狀态去等待LP釋放互斥鎖。

3、LP繼續執行,但是在釋放互斥鎖之前,又被 MP搶占了時間片。

4、這時HP繼續等待LP釋放互斥鎖,但是LP現在卻不能執行,更不能釋放互斥鎖,因為此時是MP在不斷搶占執行。

以上的這個“優先級反轉”會破壞任務間的正常排程,造成系統的不确定性。是以這裡必須引入“優先級繼承”的技術來解決這個問題。

優先級繼承

FreeRTOS 互斥體和二進制信号量非常相似——不同之處在于互斥體包含基本的“優先級繼承”機制,而二進制信号量則沒有。優先級繼承是一種将優先級反轉的負面影響降至最低的方案。它不會“修複”優先級反轉,而隻是通過確定返轉總是有時間限制來減輕其影響。但是,優先級繼承使系統時序分析變得複雜,依賴它來進行正确的系統操作并不是一個好的做法。

優先級繼承的工作原理是将互斥鎖持有者的優先級臨時提高到試圖獲得相同互斥鎖的最高優先級任務的優先級。持有互斥鎖的低優先級任務“繼承”等待互斥鎖的任務的優先級。下圖 示範了這一點。互斥鎖持有者的優先級在将互斥鎖歸還時自動重置為其原始值。

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

1、三個不同的優先級任務。LP,MP,HP分别指低優先級,中等優先級,最高優先級。LP在執行時在被HP搶占前就獲得了互斥鎖。

2、HP搶占了LP後,想要去獲得互斥鎖,但因為此時是LP占用互斥鎖,是以HP隻能進入阻塞狀态去等待LP釋放互斥鎖。

3、 LP 任務阻止 HP 任務執行,是以繼承了HP 任務的優先級。 LP 任務現在不能被 MP 任務搶占,是以優先級反轉存在的時間量被最小化。當 LP 任務将互斥鎖傳回時,它會傳回其原始優先級。

4、傳回互斥鎖的 LP 任務導緻 HP 任務作為互斥鎖持有者退出阻塞狀态。當 HP 任務使用互斥體完成時,它會将其傳回。 MP 任務僅在 HP 任務傳回 Blocked 狀态時執行,是以 MP 任務永遠不會阻塞 HP 任務。

4.4 關于死鎖

死鎖(或緻命擁抱)

“死鎖”是使用互斥鎖進行互斥的另一個潛在陷阱。死鎖有時也以更具戲劇性的名稱“緻命擁抱”而聞名。

當兩個任務因為都在等待對方持有的資源而無法繼續時,就會發生死鎖。考慮以下場景,其中任務 A 和任務 B 都需要擷取互斥鎖

X 和互斥鎖 Y 才能執行操作:

1、 任務 A 執行并成功擷取 mutex X。

2、 任務 A 被任務 B 搶占。

3、 任務 B 在嘗試同時擷取互斥體 X 之前成功地擷取了互斥體 Y——但互斥體 X 由任務 A 持有,是以對任務 B 不可用。任務 B 選擇

進入阻塞狀态以等待互斥體 X 被釋放。

4、 任務A繼續執行。它嘗試擷取互斥體 Y——但互斥體 Y 由任務 B 持有,是以對任務 A 不可用。任務 A 選擇進入阻塞狀态以等待

互斥體 Y 被釋放。

在這個場景結束時,任務 A 正在等待任務 B 持有的互斥鎖,而任務 B 正在等待任務 A 持有的互斥鎖。由于兩個任務都無法繼續,

是以發生了死鎖。

與優先級倒置一樣,避免死鎖的最佳方法是在設計時考慮其潛力,并設計系統以確定不會發生死鎖。特别是,正如前面所述,任務無限期地等待(沒有逾時)以獲得互斥鎖通常是不好的做法。

4.5 遞歸互斥鎖

遞歸互斥鎖

任務也可能與自身發生死鎖。如果一個任務嘗試多次使用同一個互斥鎖,而不首先傳回互斥鎖,就會發生這種情況。考慮以下場

景:

1、任務成功擷取互斥量。

2、 在持有互斥鎖的同時,任務調用庫函數。

3、庫函數的實作嘗試取同一個互斥體,進入阻塞狀态等待互斥體可用。

在這個場景結束時,任務處于阻塞狀态等待互斥鎖傳回,但任務已經是互斥鎖持有者。發生死鎖是因為任務處于阻塞狀态等待自

己。

可以通過使用遞歸互斥鎖代替标準互斥鎖來避免這種類型的死鎖。遞歸互斥鎖可以被同一個任務多次“擷取”,并且隻有在對“

擷取”遞歸互斥鎖的每個先前調用都執行了一次“給予”遞歸互斥鎖的調用後才會傳回。标準互斥鎖和遞歸互斥鎖的建立和使用方式類似:

->使用 xSemaphoreCreateMutex() 建立标準互斥鎖。遞歸互斥鎖是使用 xSemaphoreCreateRecursiveMutex() 建立的。這兩個API 函數有相同的原型。

->使用 xSemaphoreTake()“擷取”标準互斥鎖。遞歸互斥鎖是使用 xSemaphoreTakeRecursive() 來“擷取”的。這兩個 API 函數具有相同的原型。

->使用 xSemaphoreGive() “給定”标準互斥鎖。遞歸互斥鎖是使用 xSemaphoreGiveRecursive() “給定”的。這兩個 API 函數具有相同的原型。

示範了如何建立和使用遞歸互斥鎖。

SemaphoreHandle_t xRecursiveMutex;  //遞歸互斥鎖的變量

/* 以下是建立和使用一個遞歸互斥鎖的任務 */
void vTaskFunction( void *pvParameters )
{
	const TickType_t xMaxBlock20ms = pdMS_TO_TICKS( 20 );
 	
 	xRecursiveMutex = xSemaphoreCreateRecursiveMutex(); //建立遞歸互斥鎖
 
 	configASSERT( xRecursiveMutex ); //檢查互斥鎖是否被建立成功
 	for( ;; )
 	{
 		/* ... */
 		
 		if( xSemaphoreTakeRecursive( xRecursiveMutex, xMaxBlock20ms ) == pdPASS )  //獲得遞歸互斥鎖
 		{
 			/*已成功擷取遞歸互斥對象。該任務現在可以通路互斥鎖正在保護的資源。此時,遞歸調用計數(這是對xSemaphoreTakeRecursive()
 			的嵌套調用數)為1,因為遞歸互斥體隻執行了一次。
			當它已經持有遞歸互斥鎖時,任務将再次擷取互斥鎖。在實際應用程式中,這隻可能發生在該任務調用的子函數中,因為沒有實際的理由
			故意多次使用相同的互斥鎖。調用任務已經是互斥體持有者,是以對xSemaphoreTakeRecursive()的第二次調用隻會将遞歸調用計
			數增加到2。*/
 			xSemaphoreTakeRecursive( xRecursiveMutex, xMaxBlock20ms );
 			/* ... */
 
 			/* 任務在完成對互斥鎖所保護的資源的通路後傳回互斥鎖。此時,遞歸調用計數為2,是以對xSemaphoreGiveRecursive()的第一次
 			調用不會傳回互斥體。相反,它隻是将遞歸調用計數減回到1。*/
 			xSemaphoreGiveRecursive( xRecursiveMutex );
 			/* 下一次對xSemaphoreGiveRecursive()的調用将遞歸調用計數減為0,是以這次傳回遞歸互斥。*/
 			xSemaphoreGiveRecursive( xRecursiveMutex );
 			/*現在,對 xSemaphoreTakeRecursive()的每一次調用都執行了一次xSemaphoreGiveRecursive(),是以該任務不再是互斥體
 			持有者。 
 		}
 	} 
 }

           

4.6 互斥鎖和任務排程

如果兩個不同優先級的任務使用同一個互斥鎖,那麼 FreeRTOS 排程政策會明确任務執行的順序;能夠運作的優先級最高的任務将被選為進入運作狀态的任務。例如,如果一個高優先級任務處于阻塞狀态以等待一個低優先級任務持有的互斥鎖,那麼一旦低優先級任務傳回互斥鎖,高優先級任務就會搶占低優先級任務 . 然後,高優先級任務将成為互斥鎖持有者。

然而,當任務具有相同優先級時,通常會錯誤地假設任務将執行的順序。如果 Task 1 和 Task 2 具有相同的優先級,并且 Task 1 處

于 Blocked 狀态以等待 Task 2 持有的互斥鎖,那麼當 Task 2“給予”互斥鎖時,Task 1 不會搶占 Task 2。相反,任務 2 将保持在運

行狀态,而任務 1 将簡單地從阻塞狀态移動到就緒狀态。這種情況如下圖所示,其中垂直線标記發生滴答中斷的時間。

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

1、Task2運作一個時間片,在此期間拿到一個互斥鎖。

2、在下一個時間片,Task1開始執行。

3、Task1試圖拿到互斥鎖,此時互斥鎖被Task2擁有,是以task1進入阻塞等待。

4、在t2-t3剩下的時間片裡,Task2運作,并且持續到下一個時間片t3-t4

5、在t3-t4時間片裡,Task2交出互斥鎖,這裡task1立即從阻塞狀态切換到ready狀态。

6、task1要到t3-t4時間片結束後,才能進入running狀态。

在上圖 所示的場景中,一旦互斥鎖可用,FreeRTOS 排程程式就不會立即使Task1 成為運作狀态任務,因為:

1、 任務 1 和任務 2 具有相同的優先級,是以除非任務 2 進入 Blocked 狀态,否則在下一次滴答中斷之前不應切換到任務 1(假設

FreeRTOSConfig.h 中的 configUSE_TIME_SLICING 設定為 1)。

2、 如果任務在緊密循環中使用互斥鎖,并且每次任務“給予”互斥鎖時都會發生上下文切換,那麼任務隻會在短時間内保持運作狀

态。如果兩個或多個任務在緊密循環中使用相同的互斥鎖,則在任務之間快速切換會浪費處理時間。

如果多個任務在緊密循環中使用互斥鎖,并且使用互斥鎖的任務具有相同的優先級,則必須注意確定任務獲得大緻相等的處理時間。

下圖說明了任務可能無法獲得相等處理時間的原因,下圖顯示了如果以相同優先級建立如下代碼所示任務的兩個執行個體,可能會發生的執行順序。

/* 在緊密循環中使用互斥鎖的任務的實作。該任務在本地緩沖區中建立一個文本字元串,然後将該字元串寫入顯示器。對顯示器的通路受到互斥鎖的保護*/
void vATask( void *pvParameter )
{
	extern SemaphoreHandle_t xMutex;
	char cTextBuffer[ 128 ];
 	for( ;; )
 	{
 		/* 生成字元串--這是一個快速的操作 */
 		vGenerateTextInALocalBuffer( cTextBuffer );
 		/* 獲得互斥鎖,用于保護顯示器的通路. */
 		xSemaphoreTake( xMutex, portMAX_DELAY );
		 /* 将生成的字元串寫到顯示器---這是一個慢速的操作. */
 		vCopyTextToFrameBuffer( cTextBuffer );
		 /* 字元串寫到顯示器後,傳回互斥鎖 */
 		xSemaphoreGive( xMutex );
 } }
           

在這個任務中,建立字元串是一項快速操作,而更新顯示是一項緩慢的操作。是以,由于在更新顯示時保持互斥鎖,任務将在其大部分運作時間保持互斥鎖。

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

1、Task2 在t1-t2時間片裡處于運作狀态,同時在此期間獲得了互斥鎖。

2、Task1在下一個時間片開始運作。

3、Task1運作期間,想要擷取互斥鎖,但task2并沒有釋放互斥鎖,是以task1進入阻塞狀态

4、task2在t2-t3的剩餘時間裡開始運作,并保持到下一個時間片t3-t4。

5、task2在此處傳回互斥鎖。立即使task1退出阻塞狀态,進入ready狀态。但由于進間片還未完成,是以task2繼續運作。

6、在task2繼續運作的進間片裡,task2又獲得互斥鎖。

7、在t4時間片開始時,task1進入運作狀态,但立即又因為拿不到互斥鎖而進入阻塞狀态。

上圖中的第 7 步顯示任務 1 重新進入阻塞狀态—這發生在 xSemaphoreTake() API 函數内部。

上圖 展示了任務 1 将被阻止獲得互斥鎖,直到時間片的開始與任務 2 不是互斥鎖持有者的短周期之一重合。

通過在調用 xSemaphoreGive() 之後添加對 taskYIELD() 的調用,可以避免上圖 中所示的情況。這在下面的代碼中進行了示範,如

果在任務持有互斥鎖時滴答計數發生變化,則調用 taskYIELD()。

void vFunction( void *pvParameter )
{
	extern SemaphoreHandle_t xMutex;
	char cTextBuffer[ 128 ];
	TickType_t xTimeAtWhichMutexWasTaken;
 	for( ;; )
 	{
 		/* 生成字元串--這是一個快速的操作,時間很短。 */
 		vGenerateTextInALocalBuffer( cTextBuffer );
 		/* 獲得互斥鎖,用于保護對顯示器的通路。 */
 		xSemaphoreTake( xMutex, portMAX_DELAY );
 		/* 記錄獲得互斥鎖的時間。 */
 		xTimeAtWhichMutexWasTaken = xTaskGetTickCount();
 		/* 将字元串寫入到顯示器--這是一個慢速的操作,時間較長 */
 		vCopyTextToFrameBuffer( cTextBuffer );
 		/* 完成寫入顯示器後,傳回互斥鎖. */
 		xSemaphoreGive( xMutex );
 		/* 如果在每次疊代中調用taskYIELD(),則此任務隻會在短時間内保持運作狀态,并且快速切換任務會浪費處理時間。是以,
 		隻有在互斥鎖被持有時滴答計數發生變化時,才調用taskYIELD()。 */
 		if( xTaskGetTickCount() != xTimeAtWhichMutexWasTaken )
 		{
 			taskYIELD();
 		}
 	} 
 }
           

5、網守任務(Gatekeeper Tasks)

網守任務提供了一種實作互斥的幹淨方法,沒有優先級反轉或死鎖的風險。

網守任務是對資源擁有唯一所有權的任務。隻有網守任務被允許直接通路資源——任何其他需要通路資源的任務隻能通過使用網守的服務間接通路。

接下來的示例 , 重寫 vPrintString() 以使用網守任務。示例為 vPrintString() 提供了另一種替代實作。這一次,gatekeeper 任務用于管理對标準輸出的通路。當一個任務想要将消息寫入标準輸出時,它不會直接調用列印函數,而是将消息發送給gatekeeper。

網守任務使用 FreeRTOS 隊列來序列化對标準輸出的通路。任務的内部實作不必考慮互斥,因為它是唯一允許直接通路标準的任務。

網守任務大部分時間都處于阻塞狀态,等待消息到達隊列。當消息到達時,網守隻需将消息寫入标準輸出,然後傳回阻塞狀态以等待下一條消息。

中斷可以發送到隊列,是以中斷服務例程也可以安全地使用網守的服務将消息寫入終端。在這個例子中,一個ticks鈎子函數用于每 200 個ticks寫出一條消息。

tick鈎子函數在tick中斷的上下文中執行,是以必須保持非常短,必須僅使用适量的堆棧空間,并且不得調用任何不以“FromISR()”結尾的 FreeRTOS API 函數。

排程程式總是在tick鈎子函數之後立即執行,是以從tick鈎子調用的中斷安全 FreeRTOS API 函數不需要使用它們的pxHigherPriorityTaskWoken 參數,該參數可以設定為 NULL。

static void prvStdioGatekeeperTask( void *pvParameters )
{
	char *pcMessageToPrint;
 	/* 這是唯一允許寫入标準輸出的任務。任何其他想要将字元串寫入輸出的任務都不會直接通路标準輸出,而是将字元串發送到此任務。
 	由于隻有此任務通路标準輸出,是以在任務本身的實作中不需要考慮互斥或串行化問題。 */
 	for( ;; )
 	{
 		/* 接收隊列内的資訊 */
 		xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY );
 		printf( "%s", pcMessageToPrint );
 		fflush( stdout );
  	}
}
           

寫入隊列的任務下 所示。如前所述,建立了兩個單獨的任務執行個體,并使用 task 參數将任務寫入隊列的字元串傳遞給任務。

static void prvPrintTask( void *pvParameters )
{
	int iIndexToString;
	const TickType_t xMaxBlockTimeTicks = 0x20;
 	/* 該任務的兩個執行個體。任務的參數用于傳送字元串數組的索引。*/
 	iIndexToString = ( int ) pvParameters;
 	for( ;; )
 	{
 		/* 不直接列印字元串,而是通過隊列将指向字元串的指針傳遞給看門人任務。隊列是在排程程式啟動之前建立的,是以在該任務第一次
 		執行時已經存在。未指定塊時間,因為隊列中應始終有空間。*/
 		xQueueSendToBack( xPrintQueue, &( pcStringsToPrint[ iIndexToString ] ), 0 );
 		vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );
 	}
 }
           

tick鈎子函數計算它被調用的次數,每次計數達到 200 時将其消息發送給網守任務。僅出于示範目的,tick鈎子寫入隊列的前面,任務寫入後面的隊列。tick鈎子實作如下所示。

tick鈎子(或滴答回調)是核心在每次滴答中斷期間調用的函數。要使用tick挂鈎功能:

  1. 在 FreeRTOSConfig.h 中将 configUSE_TICK_HOOK 設定為 1。
  2. 使用的鈎子函數的原型如:
    【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結
void vApplicationTickHook( void )
{
	static int iCount = 0;
 	/* 每200個tick列印一個資訊。資訊并不是直接輸出,而是發送給了gatekeeper任務。 */
 	iCount++;
 	if( iCount >= 200 )
 	{
 		xQueueSendToFrontFromISR( xPrintQueue, 
 								  &( pcStringsToPrint[ 2 ] ), 
 								  NULL );
  		iCount = 0;
 	} 
 }
           

像往常一樣, main() 建立運作示例所需的隊列和任務,然後啟動排程程式。 main() 的實作如清單 131 所示。

/* 用于列印的字元串數組,用于傳送給網守任務 gatekeeper. */
static char *pcStringsToPrint[] =
{
 "Task 1 ****************************************************\r\n",
 "Task 2 ----------------------------------------------------\r\n",
 "Message printed from the tick hook interrupt ##############\r\n"
};
/*-----------------------------------------------------------*/
/* 聲明QueueHandle_t類型的變量。該隊列用于從列印任務發送消息,并将tick中斷發送到看門人任務。*/
QueueHandle_t xPrintQueue;
/*-----------------------------------------------------------*/
int main( void )
{
 	 xPrintQueue = xQueueCreate( 5, sizeof( char * ) );
 	/* Check the queue was created successfully. */
 	if( xPrintQueue != NULL )
 	{
 	/* 建立兩個向網關守衛發送消息的任務執行個體。任務使用的字元串的索引通過任務參數(xTaskCreate()的第四個參數)傳遞給任務。
 	任務以不同的優先級建立,是以較高優先級的任務偶爾會搶占較低優先級的任務。*/
 		xTaskCreate( prvPrintTask, "Print1", 1000, ( void * ) 0, 1, NULL );
 		xTaskCreate( prvPrintTask, "Print2", 1000, ( void * ) 1, 2, NULL );
 		/* 建立看門人任務。這是唯一允許直接通路标準輸出的任務。 */
 		xTaskCreate( prvStdioGatekeeperTask, "Gatekeeper", 1000, NULL, 0, NULL );
  		vTaskStartScheduler();
 	}
  	for( ;; );
}
           

執行示例時産生的輸出如圖 70 所示。可以看出,源自任務的字元串和源自中斷的字元串都正确列印出來,沒有損

壞。

【ESP32+freeRTOS學習筆記-(八)資源管理】1、 資源使用概況2、互斥方法之一:基本臨界區3、互斥方法之二:挂起或鎖定排程程式4 互斥方法三:互斥信号量(和二進制信号量)5、網守任務(Gatekeeper Tasks)總結

網守任務的優先級低于列印任務,是以發送到網守的消息将保留在隊列中,直到兩個列印任務都處于阻塞狀态。在

某些情況下,為網守配置設定更高的優先級是合适的,是以消息會立即得到處理——但這樣做的代價是網守延遲較低優

先級的任務,直到它完成對受保護資源的通路。

總結

多任務系統中,任務對資源占用以及協調是非常重要的。即要保證占用資源的任務能不被幹擾的使用,也要防止出現死鎖。FreeRTOS針對不同類型的使用場景,提供了從禁用中斷,禁用排程這類系統級的資源使用手段。也提供了互斥信号量,嵌套互斥信号量等應用級的資源使用手段。也提供了Gatekeeper Tasks(網守任務)方式的資源使用方案。

因為任務排程模式導至的多種使用資源可能出現死鎖的場景,FreeRTOS提供了優先級繼承,遞歸互斥鎖的機制解決。分析了對于多個任務在緊密循環中使用互斥鎖,并且使用互斥鎖的任務具有相同的優先級時可能出現的問題以及解決的方法。