韋東山freeRTOS系列教程之【第六章】訊號量(semaphore)
需要獲取更好閱讀體驗的同學,請訪問我專門設立的站點檢視,地址:http://rtos.100ask.net/
系列教程總目錄
本教程連載中,篇章會比較多,為方便同學們閱讀,點選這裡可以檢視文章的 目錄列表,目錄列表頁面地址:https://blog.csdn.net/thisway_diy/article/details/121399484
概述
前面介紹的佇列(queue)可以用於傳輸資料:在任務之間、任務和中斷之間。
有時候我們只需要傳遞狀態,並不需要傳遞具體的資訊,比如:
- 我的事做完了,通知一下你
- 賣包子了、賣包子了,做好了1個包子!做好了2個包子!做好了3個包子!
- 這個停車位我佔了,你們只能等著
在這種情況下我們可以使用訊號量(semaphore),它更節省記憶體。
本章涉及如下內容:
- 怎麼建立、刪除訊號量
- 怎麼傳送、獲得訊號量
- 什麼是計數型訊號量?什麼是二進位制訊號量?
6.1 訊號量的特性
6.1.1 訊號量的常規操作
訊號量這個名字很恰當:
- 訊號:起通知作用
- 量:還可以用來表示資源的數量
- 當"量"沒有限制時,它就是"計數型訊號量"(Counting Semaphores)
- 當"量"只有0、1兩個取值時,它就是"二進位制訊號量"(Binary Semaphores)
- 支援的動作:"give"給出資源,計數值加1;"take"獲得資源,計數值減1
計數型訊號量的典型場景是:
- 計數:事件產生時"give"訊號量,讓計數值加1;處理事件時要先"take"訊號量,就是獲得訊號量,讓計數值減1。
- 資源管理:要想訪問資源需要先"take"訊號量,讓計數值減1;用完資源後"give"訊號量,讓計數值加1。
訊號量的"give"、"take"雙方並不需要相同,可以用於生產者-消費者場合:
- 生產者為任務A、B,消費者為任務C、D
- 一開始訊號量的計數值為0,如果任務C、D想獲得訊號量,會有兩種結果:
- 阻塞:買不到東西咱就等等吧,可以定個鬧鐘(超時時間)
- 即刻返回失敗:不等
- 任務A、B可以生產資源,就是讓訊號量的計數值增加1,並且把等待這個資源的顧客喚醒
- 喚醒誰?誰優先順序高就喚醒誰,如果大家優先順序一樣就喚醒等待時間最長的人
二進位制訊號量跟計數型的唯一差別,就是計數值的最大值被限定為1。
6.1.2 訊號量跟佇列的對比
差異列表如下:
佇列 | 訊號量 |
---|---|
可以容納多個數據, 建立佇列時有2部分記憶體: 佇列結構體、儲存資料的空間 |
只有計數值,無法容納其他資料。 建立訊號量時,只需要分配訊號量結構體 |
生產者:沒有空間存入資料時可以阻塞 | 生產者:用於不阻塞,計數值已經達到最大時返回失敗 |
消費者:沒有資料時可以阻塞 | 消費者:沒有資源時可以阻塞 |
6.1.3 兩種訊號量的對比
訊號量的計數值都有限制:限定了最大值。如果最大值被限定為1,那麼它就是二進位制訊號量;如果最大值不是1,它就是計數型訊號量。
差別列表如下:
二進位制訊號量 | 技術型訊號量 |
---|---|
被建立時初始值為0 | 被建立時初始值可以設定 |
其他操作是一樣的 | 其他操作是一樣的 |
6.2 訊號量函式
使用訊號量時,先建立、然後去新增資源、獲得資源。使用控制代碼來表示一個訊號量。
6.2.1 建立
使用訊號量之前,要先建立,得到一個控制代碼;使用訊號量時,要使用控制代碼來表明使用哪個訊號量。
對於二進位制訊號量、計數型訊號量,它們的建立函式不一樣:
二進位制訊號量 | 計數型訊號量 | |
---|---|---|
動態建立 | xSemaphoreCreateBinary 計數值初始值為0 |
xSemaphoreCreateCounting |
vSemaphoreCreateBinary(過時了) 計數值初始值為1 |
||
靜態建立 | xSemaphoreCreateBinaryStatic | xSemaphoreCreateCountingStatic |
建立二進位制訊號量的函式原型如下:
/* 建立一個二進位制訊號量,返回它的控制代碼。
* 此函式內部會分配訊號量結構體
* 返回值: 返回控制代碼,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinary( void );
/* 建立一個二進位制訊號量,返回它的控制代碼。
* 此函式無需動態分配記憶體,所以需要先有一個StaticSemaphore_t結構體,並傳入它的指標
* 返回值: 返回控制代碼,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );
建立計數型訊號量的函式原型如下:
/* 建立一個計數型訊號量,返回它的控制代碼。
* 此函式內部會分配訊號量結構體
* uxMaxCount: 最大計數值
* uxInitialCount: 初始計數值
* 返回值: 返回控制代碼,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
/* 建立一個計數型訊號量,返回它的控制代碼。
* 此函式無需動態分配記憶體,所以需要先有一個StaticSemaphore_t結構體,並傳入它的指標
* uxMaxCount: 最大計數值
* uxInitialCount: 初始計數值
* pxSemaphoreBuffer: StaticSemaphore_t結構體指標
* 返回值: 返回控制代碼,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t *pxSemaphoreBuffer );
6.2.2 刪除
對於動態建立的訊號量,不再需要它們時,可以刪除它們以回收記憶體。
vSemaphoreDelete可以用來刪除二進位制訊號量、計數型訊號量,函式原型如下:
/*
* xSemaphore: 訊號量控制代碼,你要刪除哪個訊號量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
6.2.3 give/take
二進位制訊號量、計數型訊號量的give、take操作函式是一樣的。這些函式也分為2個版本:給任務使用,給ISR使用。列表如下:
在任務中使用 | 在ISR中使用 | |
---|---|---|
give | xSemaphoreGive | xSemaphoreGiveFromISR |
take | xSemaphoreTake | xSemaphoreTakeFromISR |
xSemaphoreGive的函式原型如下:
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
xSemaphoreGive函式的引數與返回值列表如下:
引數 | 說明 |
---|---|
xSemaphore | 訊號量控制代碼,釋放哪個訊號量 |
返回值 | pdTRUE表示成功, 如果二進位制訊號量的計數值已經是1,再次呼叫此函式則返回失敗; 如果計數型訊號量的計數值已經是最大值,再次呼叫此函式則返回失敗 |
pxHigherPriorityTaskWoken的函式原型如下:
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
xSemaphoreGiveFromISR函式的引數與返回值列表如下:
引數 | 說明 |
---|---|
xSemaphore | 訊號量控制代碼,釋放哪個訊號量 |
pxHigherPriorityTaskWoken | 如果釋放訊號量導致更高優先順序的任務變為了就緒態, 則*pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE表示成功, 如果二進位制訊號量的計數值已經是1,再次呼叫此函式則返回失敗; 如果計數型訊號量的計數值已經是最大值,再次呼叫此函式則返回失敗 |
xSemaphoreTake的函式原型如下:
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
xSemaphoreTake函式的引數與返回值列表如下:
引數 | 說明 |
---|---|
xSemaphore | 訊號量控制代碼,獲取哪個訊號量 |
xTicksToWait | 如果無法馬上獲得訊號量,阻塞一會: 0:不阻塞,馬上返回 portMAX_DELAY: 一直阻塞直到成功 其他值: 阻塞的Tick個數,可以使用 pdMS_TO_TICKS() 來指定阻塞時間為若干ms |
返回值 | pdTRUE表示成功 |
xSemaphoreTakeFromISR的函式原型如下:
BaseType_t xSemaphoreTakeFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
xSemaphoreTakeFromISR函式的引數與返回值列表如下:
引數 | 說明 |
---|---|
xSemaphore | 訊號量控制代碼,獲取哪個訊號量 |
pxHigherPriorityTaskWoken | 如果獲取訊號量導致更高優先順序的任務變為了就緒態, 則*pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE表示成功 |
6.3 示例12: 使用二進位制訊號量來同步
本節程式碼為: FreeRTOS_12_semaphore_binary
。
main函式中建立了一個二進位制訊號量,然後建立2個任務:一個用於釋放訊號量,另一個用於獲取訊號量,程式碼如下:
/* 二進位制訊號量控制代碼 */
SemaphoreHandle_t xBinarySemaphore;
int main( void )
{
prvSetupHardware();
/* 建立二進位制訊號量 */
xBinarySemaphore = xSemaphoreCreateBinary( );
if( xBinarySemaphore != NULL )
{
/* 建立1個任務用於釋放訊號量
* 優先順序為2
*/
xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );
/* 建立1個任務用於獲取訊號量
* 優先順序為1
*/
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
/* 啟動排程器 */
vTaskStartScheduler();
}
else
{
/* 無法建立二進位制訊號量 */
}
/* 如果程式執行到了這裡就表示出錯了, 一般是記憶體不足 */
return 0;
}
傳送任務、接收任務的程式碼和執行流程如下:
- A:傳送任務優先順序高,先執行。連續3次釋放二進位制訊號量,只有第1次成功
- B:傳送任務進入阻塞態
- C:接收任務得以執行,得到訊號量,列印OK;再次去獲得訊號量時,進入阻塞狀態
- 在傳送任務的vTaskDelay退出之前,執行的是空閒任務:現在傳送任務、接收任務都阻塞了
- D:傳送任務再次執行,連續3次釋放二進位制訊號量,只有第1次成功
- E:傳送任務進入阻塞態
- F:接收任務被喚醒,得到訊號量,列印OK;再次去獲得訊號量時,進入阻塞狀態
執行結果如下圖所示,即使傳送任務連續釋放多個訊號量,也只能成功1次。釋放、獲得訊號量是一一對應的。
6.4 示例13: 防止資料丟失
本節程式碼為: FreeRTOS_13_semaphore_circle_buffer
。
在示例12中,傳送任務發出3次"提醒",但是接收任務只接收到1次"提醒",其中2次"提醒"丟失了。
這種情況很常見,比如每接收到一個串列埠字元,串列埠中斷程式就給任務發一次"提醒",假設收到多個字元、發出了多次"提醒"。當任務來處理時,它只能得到1次"提醒"。
你需要使用其他方法來防止資料丟失,比如:
-
在串列埠中斷中,把資料放入緩衝區
-
在任務中,一次性把緩衝區中的資料都讀出
-
簡單地說,就是:你提醒了我多次,我太忙只響應你一次,但是我一次性拿走所有資料
main函式中建立了一個二進位制訊號量,然後建立2個任務:一個用於釋放訊號量,另一個用於獲取訊號量,程式碼如下:
/* 二進位制訊號量控制代碼 */
SemaphoreHandle_t xBinarySemaphore;
int main( void )
{
prvSetupHardware();
/* 建立二進位制訊號量 */
xBinarySemaphore = xSemaphoreCreateBinary( );
if( xBinarySemaphore != NULL )
{
/* 建立1個任務用於釋放訊號量
* 優先順序為2
*/
xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );
/* 建立1個任務用於獲取訊號量
* 優先順序為1
*/
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
/* 啟動排程器 */
vTaskStartScheduler();
}
else
{
/* 無法建立二進位制訊號量 */
}
/* 如果程式執行到了這裡就表示出錯了, 一般是記憶體不足 */
return 0;
}
傳送任務、接收任務的程式碼和執行流程如下:
- A:傳送任務優先順序高,先執行。連續寫入3個數據、釋放3個訊號量:只有1個訊號量起作用
- B:傳送任務進入阻塞態
- C:接收任務得以執行,得到訊號量
- D:接收任務一次性把所有資料取出
- E:接收任務再次嘗試獲取訊號量,進入阻塞狀態
- 在傳送任務的vTaskDelay退出之前,執行的是空閒任務:現在傳送任務、接收任務都阻塞了
- F:傳送任務再次執行,連續寫入3個數據、釋放3個訊號量:只有1個訊號量起作用
- G:傳送任務進入阻塞態
- H:接收任務被喚醒,得到訊號量,一次性把所有資料取出
程式執行結果如下,資料未丟失:
6.5 示例14: 使用計數型訊號量
本節程式碼為: FreeRTOS_14_semaphore_counting
。
使用計數型訊號量時,可以多次釋放訊號量;當訊號量的技術值達到最大時,再次釋放訊號量就會出錯。
如果訊號量計數值為n,就可以連續n次獲取訊號量,第(n+1)次獲取訊號量就會阻塞或失敗。
main函式中建立了一個計數型訊號量,最大計數值為3,初始值計數值為0;然後建立2個任務:一個用於釋放訊號量,另一個用於獲取訊號量,程式碼如下:
/* 計數型訊號量控制代碼 */
SemaphoreHandle_t xCountingSemaphore;
int main( void )
{
prvSetupHardware();
/* 建立計數型訊號量 */
xCountingSemaphore = xSemaphoreCreateCounting(3, 0);
if( xCountingSemaphore != NULL )
{
/* 建立1個任務用於釋放訊號量
* 優先順序為2
*/
xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );
/* 建立1個任務用於獲取訊號量
* 優先順序為1
*/
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
/* 啟動排程器 */
vTaskStartScheduler();
}
else
{
/* 無法建立訊號量 */
}
/* 如果程式執行到了這裡就表示出錯了, 一般是記憶體不足 */
return 0;
}
傳送任務、接收任務的程式碼和執行流程如下:
- A:傳送任務優先順序高,先執行。連續釋放4個訊號量:只有前面3次成功,第4次失敗
- B:傳送任務進入阻塞態
- CDE:接收任務得以執行,得到3個訊號量
- F:接收任務試圖獲得第4個訊號量時進入阻塞狀態
- 在傳送任務的vTaskDelay退出之前,執行的是空閒任務:現在傳送任務、接收任務都阻塞了
- G:傳送任務再次執行,連續釋放4個訊號量:只有前面3次成功,第4次失敗
- H:傳送任務進入阻塞態
- IJK:接收任務得以執行,得到3個訊號量
- L:接收任務再次獲取訊號量時進入阻塞狀態
執行結果如下圖所示: