以下內容轉載自安富萊電子: http://forum.armfly.com/forum.php
本章節開始講解 RTX 的另一個重要的資源共享機制---互斥信號量(Mutex,即 Mutual Exclusion
的縮寫)。 注意,建議初學者學習完上個章節的信號量后再學習本章節的互斥信號量。
一定要多思考,二值信號會造成優先級翻轉,所以在優先級有嚴格要求的場合,請使用互斥信號。
互斥信號量的概念及其作用
互斥信號量就是信號量的一種特殊形式,也就是信號量初始值為 1 的情況。 有些 RTOS 中也將信號量
初始值設置為 1 的情況稱之為二值信號量。 為什么叫二值信號量呢?因為信號量資源被獲取了,信號量值
就是 0,信號量資源被釋放,信號量值就是 1,把這種只有 0 和 1 兩種情況的信號量稱之為二值信號量。
互斥信號量的主要作用就是對資源實現互斥訪問。 下面舉一個通過二值信號量實現資源獨享,即互斥訪問
的例子,讓大家有一個形象的認識
運行條件:
讓兩個任務 Task1 和 Task2 都有運行串口打印 printf,這里我們就對函數 printf 通過二值信號量實
現互斥訪問。 如果不對函數 printf 進行互斥訪問,串口打印容易出現亂碼。
用信號量實現二值信號量只需將信號量的初始值設置為 1 即可。
互斥信號量跟二值信號量又有什么區別呢?互斥信號量可以防止優先級翻轉,而二值信號量不支持,下面我們就講解一下優先級翻轉問題。
運行條件:
創建 3 個任務 Task1,Task2 和 Task3,優先級分別為 3,2,1。 也就是 Task1 的優先級最高
任務 Task1 和 Task3 互斥訪問串口打印 printf,采用二值信號實現互斥訪問。
起初 Task3 通過二值信號量正在調用 printf,被任務 Task1 搶占,開始執行任務 Task1,也就是上圖
的起始位置。
運行過程描述如下:
任務 Task1 運行的過程需要調用函數 printf,發現任務 Task3 正在調用,任務 Task1 會被掛起,等
待 Task3 釋放函數 printf。
在調度器的作用下,任務 Task3 得到運行,Task3 運行的過程中,由於任務 Task2 就緒,搶占了 Task3
的運行。 優先級翻轉問題就出在這里了,從任務執行的現象上看,任務 Task1 需要等待 Task2 執行
完畢才有機會得到執行,這個與搶占式調度正好反了,正常情況下應該是高優先級任務搶占低優先級
任務的執行,這里成了高優先級任務 Task1 等待低優先級任務 Task2 完成。 所以這種情況被稱之為
優先級翻轉問題。
任務 Task2 執行完畢后,任務 Task3 恢復執行,Task3 釋放互斥資源后,任務 Task1 得到互斥資源,
從而可以繼續執行。
上面就是一個產生優先級翻轉問題的現象。
RTX 互斥信號量的實現
RTX 互斥信號量是怎么實現的呢?其實相比二值信號量就是解決了一下優先級翻轉的問題。 下面我們
通過如下的框圖來說明一下 RTX 互斥信號量的實現,讓大家有一個形象的認識。
運行條件:
創建 2 個任務 Task1 和 Task2,優先級分別為 1 和 3,也就是任務 Task2 的優先級最高
任務 Task1 和 Task2 互斥訪問串口打印 printf。
使用 RTX 的互斥信號量實現串口打印 printf 的互斥訪問。
運行過程描述如下:
低優先級任務 Task1 執行過程中先獲得互斥資源 printf 的執行。 此時任務 Task2 搶占了任務 Task1
的執行,任務 Task1 被掛起。 任務 Task2 得到執行。
任務 Task2 執行過程中也需要調用互斥資源,但是發現任務 Task1 正在訪問,此時任務 Task1 的優
先級會被提升到跟 Task2 同一個優先級,也就是優先級 3,這個就是所謂的優先級繼承(Priority
inheritance),這樣就有效的防止了優先級翻轉問題。 任務 Task2 被掛起,任務 Task1 有新的優先
級繼續執行。
任務 Task1 執行完畢並釋放互斥資源后,優先級恢復到原來的水平。 由於互斥資源可以使用,任務
Task2 獲得互斥資源后開始執行。
上面就是一個簡單 RTX 互斥信號量的實現過程。
互斥信號量僅支持用在 RTX 的任務中,中斷函數中不可使用。
互斥信號量 API 函數
使用如下 3 個函數可以實現 RTX 的互斥信號量:
os_mut_init
os_mut_release
os_mut_wait
函數 os_mut_init
函數原型:
void os_mut_init (
OS_ID mutex ); /* OS_MUT 類型變量 */
函數描述:
函數 os_mut_init 用於互斥信號量的初始化並設置初始值。
第 1 個參數填寫數據類型為 OS_MUT 的變量,同時也作為 ID 標識
使用這個函數要注意以下問題:
1. 函數的參數必須是 OS_MUT 類型的。
函數 os_mut_wait
函數原型:
OS_RESULT os_mut_wait (
OS_ID mutex, /* OS_MUT 類型變量 */
U16 timeout ); /* 超時時間 */
函數描述:
函數 os_mut_wait 用於獲取互斥信號量資源,如果互斥資源可用,那么調用函數 os_mut_wait 后可以成
功獲取互斥資源,在此函數的源碼將計數值加 1(互斥信號量源碼的實現上跟信號量不同)。如果互斥資
源不可用,調用此函數的任務將由運行態轉到掛起態,等待信號量資源可用,也就是計數值為 0 的時候。
如果一個低優先級的任務通過互斥信號量正在訪問互斥資源,那么當一個高優先級的任務也通過互斥
信號量訪問這個互斥資源的話,會將這個低優先級任務的優先級提升到和高優先級任務一樣的優先級,這
就是所謂的優先級繼承,通過優先級繼承可以有效防止優先級翻轉問題。 當低優先級任務釋放了互斥資源
之后,重新恢復到原來的優先級。
第 1 個參數填寫數據類型為 OS_MUT 的變量,同時也作為 ID 標識
第 2 個參數表示設在的等待時間,范圍 0-0xFFFF,當參數設置為 0-0xFFFE 時,表示等待這么多個
時鍾節拍,參數設置為 0xFFFF 時表示無限等待直到互斥資源可用。
函數返回 OS_R_MUT 表示函數設置的超時時間范圍內收到互斥信號量可用資源。
函數返回 OS_R_TMO 表示超時。
函數返回 OS_R_OK 表示無需等待,立即獲得互斥資源。
使用這個函數要注意以下問題:
1. 使用此函數前一定要調用函數 os_mut_init 進行初始化。
函數 os_mut_release
函數原型:
OS_RESULT os_mut_release (
OS_ID mutex ); /* OS_MUT 類型變量 */
函數描述:
函數 os_mut_release 用於釋放互斥資源,調用此函數會將計數值減 1。只有當計數值減到 0 的時候其它
的任務才可以獲取互斥資源。 也就是說如果用戶調用 os_mut_wait 和 os_mut_release,需要配套使用。
通過函數 os_mut_wait 實現互斥信號量計數值加 1,通過函數 os_mut_release 實現互斥信號量計數值減
1 操作,這樣的話,這兩個函數可以實現嵌套調用,但是一定要保證成對調用,要不會造成互斥資源無法
正確釋放。
如果擁有互斥資源的任務的優先級被提升了,那么此函數會恢復任務以前的優先級。
第 1 個參數參數填寫數據類型為 OS_MUT 的變量,同時也作為 ID 標識。
返回值 OS_R_OK,表示互斥信號量成功釋放。
返回值 OS_R_NOR,表示互斥信號量的內部計數值已經是 0 或者調用此函數的任務不是互斥資源的
擁有者。
使用這個函數要注意以下問題:
1. 使用此函數前一定要調用函數 os_mut_init 進行初始化。
實驗練習場:
實驗目的:
1. 學習 RTX 的互斥信號量
實驗內容:
在調用 printf 函數的地方都加上互斥信號量,防止多個任務調用此函數造成沖突,以至於串口打印出現亂碼。
可以看出我們的代碼是為了保護printf函數這個共享函數(資源)的。
注意,互斥信號,創建的時候初始值為1。
這里要說明的是,都采取的是永久等待,可根據具體項目需要更改等待時間。獲取(等待)互斥信號和釋放互斥信號應該在同一個任務中成對出現(雖然這里可以有其他“黑科技”,但我並不想讓更多的人知道,因為那樣通常沒有什么好處,按照官方的參考demo寫,一定更規范和正確)。
簡要說明程序流程:先創建互斥信號,初始化默認是1,這樣其他任務調用wait函數(獲取也叫等待)時,第一個調用wait函數並調用printf函數的任務一定會完整不受干擾執行printf的打印,其他也有調用printf函數的必須等待,在第一個調用任務執行完保護的函數之后,要釋放互斥信號,即release函數,這樣其他調用printf的任務才不至於永久等待。
程序輸出: