FreeRTOS的信號量包括2進制信號量、計數信號量、互斥信號量(以后簡稱互斥量)和遞歸互斥信號量(以后簡稱遞歸互斥量)。關于它們的區分可以參考《 FreeRTOS系列第19篇---FreeRTOS信號量》1文。
信號量API函數實際上都是宏,它使用現有的隊列機制。這些宏定義在semphr.h文件中。如果使用信號量或互斥量,需要包括semphr.h頭文件。
2進制信號量、計數信號量和互斥量信號量的創建API函數是獨立的,但是獲得和釋放API函數都是相同的;遞歸互斥信號量的創建、獲得和釋放API函數都是獨立的。
在《FreeRTOS高級篇5---FreeRTOS隊列分析》中,我們分析了隊列的實現進程,包括隊列創建、入隊和出隊操作。在那篇文章中我們說過,創建隊列API函數實際是調用通用隊列創建函數xQueueGenericCreate()來實現的。其實,不但創建隊列實際調用通用隊列創建函數,2進制信號量、計數信號量、互斥量和遞歸互斥量也都直接或間接使用這個函數,如表1⑴所示。表1⑴中紅色字體表示是間接調用xQueueGenericCreate()函數。
表1⑴:隊列、信號量和互斥量創建宏與直接(間接)履行函數
2進制信號量創建實際上是直接使用通用隊列創建函數xQueueGenericCreate()。創建2進制信號量API接口實際上是1個宏,定義以下:
通過這個宏定義我們知道創建2進制信號量實際上是創建了1個隊列,隊列項有1個,但是隊列項的大小為0(宏semSEMAPHORE_QUEUE_ITEM_LENGTH定義為0)。
有了隊列創建的知識,我們可以很容易的畫出初始化后的2進制信號量內存,如圖1⑴所示。
圖1⑴:初始化后的2進制信號量對象內存
也許不止1人像我1樣奇怪,創建1個沒有隊列項存儲空間的隊列,信號量用甚么表示?其實2進制信號量的釋放和獲得都是通過操作隊列結構體成員uxMessageWaiting來實現的(圖1⑴紅色部份,uxMessageWaiting表示隊列中當前隊列項的個數)。經過初始化后,變量uxMessageWaiting為0,這說明隊列為空,也就是信號量處于無效狀態。在使用API函數xSemaphoreTake()獲得信號之前,需要先釋放1個信號量。后面講到2進制信號量釋放和獲得時還會詳細介紹。
創建計數信號量間接使用通用隊列創建函數xQueueGenericCreate()。創建計數信號量API接口一樣是個宏定義:
創建計數信號量API接口有兩個參數,含義以下:
我們來看1下函數xQueueCreateCountingSemaphore()如何實現的:
從代碼可以看出,創建計數信號量依然調用通用隊列創建函數xQueueGenericCreate()來創建1個隊列,隊列項的數目由參數uxMaxCount指定,每一個隊列項的大小由宏queueSEMAPHORE_QUEUE_ITEM_LENGTH指出,我們找到這個宏定義發現,這個宏被定義為0,也就是說創建的隊列只有隊列數據結構存儲空間而沒有隊列項存儲空間。
如果隊列創建成功,則將隊列結構體成員uxMessageWaiting設置為初始計數信號量值。初始化后的計數信號量內存如圖3⑴所示。
圖1⑵:初始化后的計數信號量對象內存
創建互斥量間接使用通用隊列創建函數xQueueGenericCreate()。創建互斥量API接口一樣是個宏,定義以下:
其中,宏queueQUEUE_TYPE_MUTEX用于通用隊列創建函數,表示創建隊列的類型是互斥量,在文章《FreeRTOS高級篇5---FreeRTOS隊列分析》關于通用隊列創建函數參數說明中提到了這個宏。
我們來看1下函數xQueueCreateMutex()是如何實現的:
這個函數是帶條件編譯的,只有將宏configUSE_MUTEXES定義為1才會編譯這個函數。
函數首先調用通用隊列創建函數xQueueGenericCreate()來創建1個隊列,隊列項數目為1,隊列項大小為0,說明創建的隊列只有隊列數據結構存儲空間而沒有隊列項存儲空間。
如果隊列創建成功,通用隊列創建函數還會依照通用隊列的方式 初始化所有隊列結構體成員。但是這里要創建的是互斥量,所以有1些結構體成員必須重新賦值。在這段代碼中,可能你會疑惑,隊列結構體成員中,并沒有pxMutexHolder和uxQueueType!其實這兩個標識符只是宏定義,是專門為互斥量而定義的,以下所示:
當隊列結構體用于互斥量時,成員pcHead和pcTail指針就不再需要,并且將pcHead指針設置為NULL,表示pcTail指針實際指向互斥量持有者任務TCB(如果有的話)。
最后調用函數xQueueGenericSend()釋放1個互斥量,相當于互斥量創建后是有效的,可以直接使用獲得信號量API函數來獲得這個互斥量。如果某資源同時只準1個任務訪問,可以用互斥量保護這個資源。這個資源1定是存在的,所以創建互斥量時會先釋放1個互斥量,表示這個資源可使用。任務想訪問資源時,先獲得互斥量,等使用完資源后,再釋放它。也就是說互斥量1旦創建好后,要先獲得,后釋放,要在同1個任務中獲得和釋放。這也是互斥量和2進制信號量的1個重要區分,2進制信號量可以在隨意1個任務中獲得或釋放,然后也能夠在任意1個任務中釋放或獲得。互斥量不同于2進制信號量的還有:互斥量具有優先級繼承機制,2進制信號量沒有,互斥量不可以用于中斷服務程序,2進制信號量可以。
初始化后的互斥量內存如圖1⑶所示。
圖1⑶:初始化后的互斥量對象內存
創建遞歸互斥量間接使用通用隊列創建函數xQueueGenericCreate()。創建遞歸互斥量API接口一樣是個宏,定義以下:
其中,宏queueQUEUE_TYPE_RECURSIVE_MUTEX用于通用隊列創建函數,表示創建隊列的類型是遞歸互斥量,在文章《FreeRTOS高級篇5---FreeRTOS隊列分析》關于通用隊列創建函數參數說明中提到了這個宏。
創建互斥量和創建遞歸互斥量是調用的同1個函數xQueueCreateMutex(),至于參數queueQUEUE_TYPE_RECURSIVE_MUTEX,我們在FreeRTOS1文中已知道,它只是用于可視化調試,因此創建互斥量和創建遞歸互斥量可以看做是1樣的,初始化后的遞歸互斥量對象內存也和互斥量1樣,如圖1⑶所示。
不管2進制信號量、計數信號量還是互斥量,它們都使用相同的獲得和釋放API函數。釋放信號量用于使信號量有效,分為不帶中斷保護和帶中斷保護兩個版本。
用于釋放1個信號量,不帶中斷保護。被釋放的信號量可以是2進制信號量、計數信號量和互斥量。注意遞歸互斥量其實不能使用這個API函數釋放。其實信號量釋放是1個宏,真正調用的函數是xQueueGenericSend(),宏定義以下:
可以看出釋放信號量實際上是1次入隊操作,并且阻塞時間為0(由宏semGIVE_BLOCK_TIME定義)。
對2進制信號量和計數信號量,根據上1章的內容可以總結出,釋放1個信號量的進程實際上可以簡化為兩種情況:第1,如果隊列未滿,隊列結構體成員uxMessageWaiting加1,判斷是不是有阻塞的任務,有的話消除阻塞,然后返回成功信息(pdPASS);第2,如果隊列滿,返回毛病代碼(err_QUEUE_FULL),表示隊列滿。
對互斥量要復雜些,由于互斥量具有優先級繼承機制。
優先級繼承是個甚么進程呢?我們舉個例子。某個資源X同時只能有1個任務訪問,現在有任務A和任務C都要訪問這個資源,任務A的優先級為1,任務C的優先級為10,所以任務C的優先級大于任務A的優先級。我們用互斥量保護資源X,并且當前任務A正在訪問資源X。在任務A訪問資源X的進程中,來了1個中斷,中斷事件使得任務C履行。任務C履行的進程中,也想訪問資源X,但是由于資源X還被任務A獨占著,所以任務C沒法獲得互斥量,會進入阻塞狀態。此時,低優先級任務A會繼承高優先級任務C的優先級,任務A的優先級臨時的被提升,優先級變成10。這個機制能夠將已產生的優先級反轉影響下降到最小。
那末甚么是優先級反轉呢?還是看上面的例子,任務C的優先級高于任務A,但是任務C由于沒有取得互斥量而進入阻塞,只能等待低優先級的任務A釋放互斥量后才能運行,這類情況就是優先級反轉。
那為何優先級繼承可以下降優先級反轉的影響呢?還是看上面的例子,不過我們再增加1個優先級為5的任務B,這3個任務都處于就緒狀態。如果沒有優先級繼承機制,3個任務的優先級順序為任務C>任務B>任務A。當任務C由于得不到互斥量而阻塞后,任務B會獲得CPU權限,等到任務B主動或被動讓出CPU后,任務A才會履行,任務A釋放互斥量后,任務C才能得到運行。再看1下有優先級繼承的情況,當任務C由于得不到互斥量而阻塞后,任務A繼承任務C的優先級,現在3個任務的優先級順序為任務C=任務A>任務B。當任務C由于得不到互斥量而阻塞后,任務A會取得CPU權限,等到任務A釋放互斥量后,任務C就會得到運行。看,任務C等待的時間變短了。
有了上面的基礎理論,我們就很好理解為何釋放互斥量會比較復雜了。還是可以簡化為兩種情況:第1,如果隊列未滿,除隊列結構體成員uxMessageWaiting加1外,還要判斷獲得互斥量的任務是不是有優先級繼承,如果有的話,還要將任務的優先級恢復到原始值。固然,恢復到原來值也是有條件的,就是該任務必須在沒有使用其它互斥量的情況下,才能將繼承的優先級恢復到原始值。然后判斷是不是有阻塞的任務,有的話消除阻塞,最后返回成功信息(pdPASS);第2,如果如果隊列滿,返回毛病代碼(err_QUEUE_FULL),表示隊列滿。
用于釋放1個信號量,帶中斷保護。被釋放的信號量可以是2進制信號量和計數信號量。和普通版本的釋放信號量API函數不同,它不能釋放互斥量,這是由于互斥量不可以在中斷中使用!互斥量的優先級繼承機制只能在任務中起作用,在中斷中毫無意義。帶中斷保護的信號量釋放其實也是1個宏,真正調用的函數是xQueueGiveFromISR (),宏定義以下:
由于不觸及互斥量,不觸及阻塞,函數xQueueGiveFromISR()異常簡單,如果隊列滿,直接返回毛病代碼(err_QUEUE_FULL);如果隊列未滿,則將隊列結構體成員uxMessageWaiting加1,然后視隊列是不是上鎖而決定是不是消除任務阻塞(如果有得話)。如果你覺得難以理解,則需要先看看《FreeRTOS高級篇5---FreeRTOS隊列分析》。
不管2進制信號量、計數信號量還是互斥量,它們都使用相同的獲得和釋放API函數。釋獲得信號量會消耗信號量,如果獲得信號量失敗,任務可能會阻塞,阻塞時間由函數參數xBlockTime指定,如果為0,則直接返回,不阻塞。獲得信號量分為不帶中斷保護和帶中斷保護兩個版本。
用于獲得信號量,不帶中斷保護。獲得的信號量可以是2進制信號量、計數信號量和互斥量。注意遞歸互斥量其實不能使用這個API函數獲得。其實獲得信號量是1個宏,真正調用的函數是xQueueGenericReceive (),宏定義以下:
通過上面的宏定義可以看出,獲得信號量實際上是履行出隊操作。
對2進制信號量和計數信號量,可以簡化為3種情況:第1,如果隊列不為空,隊列結構體成員uxMessageWaiting減1,判斷是不是有因入隊而阻塞的任務,有的話消除阻塞,然后返回成功信息(pdPASS);第2,如果隊列為空并且阻塞時間為0,則直接返回毛病碼(errQUEUE_EMPTY),表示隊列為空;第3,如果隊列為空并且阻塞時間不為0,則任務會由于等待信號量而進入阻塞狀態,任務會被掛接到延時列表中。
對互斥量,也能夠簡化為3種情況,但是進程要復雜1些:第1,如果隊列不為空,隊列結構體成員uxMessageWaiting減1、將當前任務TCB結構體成員uxMutexesHeld加1,表示任務獲得互斥量的個數、將隊列結構體成員指針pxMutexHolder指向任務TCB、判斷是不是有因入隊而阻塞的任務,有的話消除阻塞,然后返回成功信息(pdPASS);第2,如果隊列為空并且阻塞時間為0,則直接返回毛病碼(errQUEUE_EMPTY),表示隊列為空;第3,如果隊列為空并且阻塞時間不為0,則任務會由于等待信號量而進入阻塞狀態,在將任務掛接到延時列表之前,會判斷當前任務和具有互斥量的任務優先級哪一個高,如果當前任務優先級高,則具有互斥量的任務繼承擔前任務優先級。
用于獲得信號量,帶中斷保護。獲得的信號量可以是2進制信號量和計數信號量。和普通版本的獲得信號量API函數不同,它不能獲得互斥量,這是由于互斥量不可以在中斷中使用!互斥量的優先級繼承機制只能在任務中起作用,在中斷中毫無意義。帶中斷保護的獲得信號量其實也是1個宏,真正調用的函數是xQueueReceiveFromISR (),宏定義以下:
一樣由于不觸及互斥量,不觸及阻塞,函數xQueueReceiveFromISR ()一樣異常簡單:如果隊列為空,直接返回毛病代碼(pdFAIL);如果隊列非空,則將隊列結構體成員uxMessageWaiting減1,然后視隊列是不是上鎖而決定是不是消除任務阻塞(如果有得話)。
函數xSemaphoreGiveRecursive()用于釋放1個遞歸互斥量。已獲得遞歸互斥量的任務可以重復獲得該遞歸互斥量。使用xSemaphoreTakeRecursive() 函數成功獲得幾次遞歸互斥量,就要使用xSemaphoreGiveRecursive()函數返還幾次,在此之前遞歸互斥量都處于無效狀態。比如,某個任務成功獲得5次遞歸互斥量,那末在它沒有返還5次該遞歸互斥量之前,這個互斥量對別的任務無效。
像其它信號量1樣,xSemaphoreGiveRecursive()也是1個宏定義,它終究使用現有的隊列機制,實際履行的函數是xQueueGiveMutexRecursive(),這個宏定義以下所示:
我們重點來看函數xQueueGiveMutexRecursive()的實現進程。經過整理后(去除跟蹤調試語句)的源碼以下所示:
這個函數是帶條件編譯的,只有將宏configUSE_RECURSIVE_MUTEXES定義為1才會編譯這個函數。
互斥量和遞歸互斥量的最大區分在于1個遞歸互斥量可以被已獲得這個遞歸互斥量的任務重復獲得,這個遞歸調用功能是通過隊列結構體成員u.uxRecursiveCallCount實現的。這個變量用于存儲遞歸調用的次數,每次獲得遞歸互斥量后,這個變量加1,在釋放遞歸互斥量后,這個變量減1。只有這個變量減到0,即釋放和獲得的次數相等時,互斥量才能再次有效,使用入隊函數釋放1個遞歸互斥量。
函數xSemaphoreTakeRecursive()用于獲得1個遞歸互斥量。像其它信號量1樣,xSemaphoreTakeRecursive()也是1個宏定義,它終究使用現有的隊列機制,實際履行的函數是xQueueTakeMutexRecursive(),這個宏定義以下所示:
獲得遞歸互斥量具有阻塞超時參數,如果互斥量正被別的任務使用,可以阻塞設定的時間。我們重點來看函數xQueueTakeMutexRecursive()的實現進程。經過整理后(去除跟蹤調試語句)的源碼以下所示:
這個函數是帶條件編譯的,只有將宏configUSE_RECURSIVE_MUTEXES定義為1才會編譯這個函數。
程序邏輯比較簡單,如果是第1次獲得這個遞歸互斥量,直接使用出隊函數,成功后將遞歸次數計數器加1;如果是第2次或更屢次獲得這個遞歸互斥量,則只需要將遞歸次數計數器加1便可。