1. 程式人生 > 其它 >韋東山freeRTOS系列教程之【第六章】訊號量(semaphore)

韋東山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:接收任務再次獲取訊號量時進入阻塞狀態

執行結果如下圖所示:

獲取更多嵌入式乾貨,請關注威信baiwenkeji