章節概述:
介紹 FreeRTOS中的任務間通訊機制:信號量的使用(盡管FreeRTOS中沒有進程的概念,但為了統一,我們還是以進程間通訊(IPC)的說法)
介紹
todo : 信號量實際上的使用(因為找不到對應的函數)
FreeRTOS的信號量包括:二值信號量、計數信號量、互斥量(互斥信號量) 與 遞歸互斥量。
我們可以把互斥量和遞歸互斥量看成特殊的信號量。
互斥量和信號量在用法上不同:
- 信號量用於同步,任務間或者任務和中斷間同步;互斥量用於互鎖,用於保護同時只能有一個任務訪問的資源,為資源上一把鎖。
- 信號量用於同步時,一般是一個任務(或中斷)給出信號,另一個任務獲取信號;互斥量必須在同一個任務中獲取信號、同一個任務給出信號。
- 互斥量具有優先級繼承,信號量沒有。
- 互斥量不能用在中斷服務程序中,信號量可以。
信號量這里大多數應用都是應用在任務同步中,特別對於中斷同步來說。對於有芯片編程的經驗的朋友應該都知道,中斷函數中我們盡可能是少做事情,這樣CPU的工作效率以及整體的性能都會得到提升。信號量在系統應用中就多用在中斷程序與任務之間的同步中。
學過Linux驅動的學者,肯定對於中斷上下文(Interrupt context)非常熟悉:為了在中斷執行時間盡可能短和中斷處理需完成大量工作之間找到一個平衡點,Linux 將中斷處理程序分解為兩個半部:頂半部(top half)和底半部(bottom half)。
中斷頂半部: 觸發中斷開始到進入中斷處理程序的那部分中斷響應程序。
中斷底半部: 執行中斷處理程序的那部分。
為了盡快地處理中斷響應,頂半部完成盡可能少的比較緊急的功能,它往往只是簡單地讀取寄存器中的中斷狀態並清除中斷標志后就進行“登記中斷”的工作,“登記中斷”意味着將底半部處理程序掛到該設備的下半部執行隊列中去。這樣,頂半部執行的速度就會很快,可以服務更多的中斷請求。,以免影響系統的性能。
當然了,如果中斷要處理的工作本身很少,則完全可以直接在頂半部全部完成。
注意:
- 信號量API函數實際上都是宏,它使用現有的隊列機制。這些宏定義在semphr.h文件中。如果使用信號量或者互斥量,需要包含semphr.h頭文件。
- 二值信號量、計數信號量和互斥量信號量的創建API函數是獨立的,但是獲取和釋放API函數都是相同的;遞歸互斥信號量的創建、獲取和釋放API函數都是獨立的。
- 互斥量和二值信號量都是
SemaphoreHandle_t類型,並且可以用於任何具有這類參數的API函數中。
二值信號量
既可以用於互斥功能也可以用於同步功能。
當任務企圖獲取一個無效信號量時,任務進入阻塞狀態,阻塞時間用來確定任務進入阻塞的最大時間。如果有多個任務阻塞在同一個信號量上,那么當信號量有效時,具有最高優先級別的任務最先解除阻塞。
注:在大部分應用場合,任務通知都可以代替二值信號量,並且速度更快、生成的代碼更少。
假定現在需要有一個任務用來維護外設,如果使用輪詢的方法會浪費CPU資源並且妨礙其它任務執行。
可以使用二值信號量實現這種應用:當任務調用xSemaphoreTake()取信號量時,因為此時尚未發生特定事件,信號量為空,任務會進入阻塞狀態;當外設需要維護時,觸發一個中斷服務程序,該中斷服務僅僅使用API函數xSemaphoreGiveFromISR()給出信號量(生產者)。任務被喚醒,取走信號量並執行任務(消費者)。
因此,在二值信號量的支持下,任務的大部分時間處於阻塞狀態(允許其它任務執行),直到某些事件發生該任務才執行。這樣也使得中斷執行盡可能的短,其它處理過程可以在任務中實現。
計數信號量
通常計數信號量用於下面兩種事件:
- 計數事件:每當事件發生,程序將給出一個信號(信號量計數值增1),當處理事件時,處理程序會取走信號量(信號量計數值減1)。因此,計數值是事件發生的數量和事件處理的數量差值。在這種情況下,計數信號量在創建時其值為0。
- 資源管理:計數值表示有效的資源數目。任務必須先獲取信號量才能獲取資源控制權。當計數值減為零時表示沒有的資源。當任務完成后,它會返還信號量(信號量計數值增加)。在這種情況下,信號量創建時,計數值等於最大資源數目。
注:在大部分應用場合,任務通知都可以代替計數信號量,並且速度更快、生成的代碼更少。
互斥量
互斥量和信號量使用相同的Take、GiveAPI函數,因此互斥量也允許指定一個阻塞時間。
互斥量是一個包含優先級繼承機制的二值信號量;用於實現同步(任務之間或者任務與中斷之間)的話,二值信號量是更好的選擇,互斥量用於簡單的互鎖。
用於互鎖的互斥量可以充當保護資源的令牌。當一個任務希望訪問某個資源時,它必須先獲取令牌。當任務使用完資源后,必須還回令牌,以便其它任務可以訪問同一資源。
互斥量與二值信號量最大的不同是:互斥量具有優先級繼承機制。也就是說,如果一個互斥量(令牌)正在被一個低優先級任務使用,此時一個高優先級企圖獲取這個互斥量,高優先級任務會因為得不到互斥量而進入阻塞狀態,正在使用互斥量的低優先級任務會臨時將自己的優先級提升,提升后的優先級與與進入阻塞狀態的高優先級任務相同。這個優先級提升的過程叫做優先級繼承。
這個機制用於確保高優先級任務進入阻塞狀態的時間盡可能短,以及將已經出現的“優先級翻轉”影響降低到最小。
“優先級翻轉”:在很多場合中,某個硬件資源只有一個,當低優先級任務占用該資源的時候,即便高優先級任務也只能乖乖等待低優先級任務釋放資源。此時,高優先級任務無法運行而低優先級任務可以運行的現象稱為“優先級翻轉”。
優先級繼承不能解決優先級反轉,只能將這種情況的影響降低到最小。硬實時系統在一開始設計時就要避免優先級反轉發生。
為什么優先級繼承能夠降低優先級翻轉的影響呢?
舉個例子,現在有任務A、任務B和任務C;三個任務的優先級順序為任務C > 任務B > 任務A。
任務A和任務C都要使用某一個硬件資源,並且當前低優先級的任務A占有該資源。
先看沒有優先級繼承的情況:任務C也要使用該資源,但是此時任務A正在使用這個資源,因此任務C進入阻塞,此時三個任務的優先級順序沒有發生變化。在任務C進入阻塞之后,某硬件產生了一次中斷,喚醒了一個事件,該事件可以解除任務B的阻塞狀態。在中斷結束后,因為任務B的優先級是大於任務A的,所以任務B搶占任務A的CPU權限。那么任務C的阻塞時間就至少為:中斷處理時間+任務B的運行時間+任務A的運行時間。
再看有優先級繼承的情況:任務C也要使用該資源,但是此時任務A正在使用這個資源,因此任務C進入阻塞,此時由於優先級A會繼承任務C的優先級,三個任務的優先級順序發生了變化,新的優先級順序為:任務C=任務A>任務B。在任務C進入阻塞之后,某硬件產生了一次中斷,喚醒了一個事件,該事件可以解除任務B的阻塞狀態。在中斷結束后,因為任務A的優先級臨時被提高,大於任務B的優先級,所以任務A繼續獲得CPU權限。任務A完成后,處於高優先級的任務C會接管CPU。所以任務C的阻塞時間為:中斷處理時間+任務A的運行時間。看,任務C的阻塞時間變小了,這就是優先級繼承的優勢。
簡單來講,優先級繼承減少了介於中間優先級的任務運行時間(例如這里的任務B)
遞歸互斥量
已經獲取遞歸互斥量的任務可以重復獲取該遞歸互斥量。使用xSemaphoreTakeRecursive() 函數成功獲取幾次遞歸互斥量,就要使用xSemaphoreGiveRecursive()函數返還幾次,在此之前遞歸互斥量都處於無效狀態。
比如,某個任務成功獲取5次遞歸互斥量,那么在它沒有返還5次該遞歸互斥量之前,這個互斥量對別的任務無效。
遞歸互斥量可以看成帶有優先級繼承機制的信號量,獲取遞歸互斥量的任務在用完后必須返還。
互斥量不能用在中斷服務程序中,這是因為:
- 互斥量具有優先級繼承機制,只有在任務中獲取或給出互斥才有意義。
- 中斷不能因為等待互斥量而阻塞。
API
FreeRTOS的信號量包括二值信號量、計數信號量、互斥信號量(以后簡稱互斥量)和遞歸互斥信號量(以后簡稱遞歸互斥量)。我們可以把互斥量和遞歸互斥量看成特殊的信號量。
信號量API函數實際上都是宏,它使用現有的隊列機制。這些宏定義在semphr.h文件中。如果使用信號量或者互斥量,需要包含semphr.h頭文件。
二值信號量、計數信號量和互斥量信號量的創建API函數是獨立的,但是獲取和釋放API函數都是相同的;遞歸互斥信號量的創建、獲取和釋放API函數都是獨立的。
創建二值信號量
SemaphoreHandle_t xSemaphoreCreateBinary( void );
描述:用於創建一個二值信號量。
返回值:
- NULL:創建信號量失敗,因為FreeRTOS堆棧不足。
- 其它值:信號量創建成功。這個返回值存儲着信號量句柄。
例子:
SemaphoreHandle_t xSemaphore;
void vATask( void * pvParameters )
{
/* 創建信號量 */
xSemaphore = xSemaphoreCreateBinary();
if( xSemaphore == NULL )
{
/* 因堆棧不足,信號量創建失敗,這里進行失敗處理*/
}
else
{
/* 信號量可以使用。信號量句柄存儲在變量xSemahore中。
如果在這里調用API函數xSemahoreTake()來獲取信號量,
則必然是失敗的,因為創建的信號量初始是無效(空)的。*/
}
}
創建計數信號量
SemaphoreHandle_t xSemaphoreCreateCounting (UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount );
描述:創建計數信號量
參數解析:
- uxMaxCount:最大計數值,當信號到達這個值后,就不再增長了。
- uxInitialCount:創建信號量時的初始值。
返回值:NULL表示信號量創建失敗,否則返回信號量句柄。
例子:
void vATask( void * pvParameters )
{
xSemaphoreHandle xSemaphore;
// 必須先創建信號量,才能使用它
// 信號量可以計數的最大值為10,計數初始值為0.
xSemaphore = xSemaphoreCreateCounting( 10, 0 );
if( xSemaphore != NULL )
{
// 信號量創建成功
// 現在可以使用信號量了。
}
}
創建互斥量
SemaphoreHandle_t xSemaphoreCreateMutex( void );
描述:創建互斥量。
返回值:NULL表示信號量創建失敗,否則返回信號量句柄。
例子:
xSemaphoreHandle xSemaphore;
voidvATask( void * pvParameters )
{
// 互斥量在未創建之前是不可用的
xSemaphore = xSemaphoreCreateMutex();
if( xSemaphore != NULL )
{
// 創建成功
// 在這里可以使用這個互斥量了
}
}
創建遞歸互斥量
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );
描述:創建遞歸互斥量。
返回值: NULL表示互斥量創建失敗,否則返回互斥量句柄。
例子:
xSemaphoreHandle xMutex;
void vATask( void * pvParameters )
{
// 互斥量未創建前是不能被使用的
xMutex = xSemaphoreCreateRecursiveMutex();
if( xMutex != NULL )
{
// 創建成功
// 在這里創建互斥量
}
}
刪除信號量
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
描述:刪除信號量。如果有任務阻塞在這個信號量上,則這個信號量不要刪除。
參數解析:信號量句柄
獲取信號量
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
// 中斷保護
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
signedBaseType_t *pxHigherPriorityTaskWoken);
描述:獲取信號量。
參數解析:
- xSemaphore:信號量句柄。信號量必須是通過API函數
xSemaphoreCreateBinary()、xSemaphoreCreateCounting()和xSemaphoreCreateMutex()預先創建過的。 - xTickToWait:信號量無效時,任務最多等待的時間,單位是系統節拍周期個數。使用宏portTICK_PERIOD_MS可以輔助將系統節拍個數轉化為實際時間(以毫秒為單位)。如果設置為0,表示不是設置等待時間。如果INCLUDE_vTaskSuspend設置為1,並且參數xTickToWait為portMAX_DELAY則可以無限等待。
- pxHigherPriorityTaskWoken:如果*pxHigherPriorityTaskWoken為pdTRUE,則需要在中斷退出前手動進行一次上下文切換。從FreeRTOS V7.3.0開始,該參數為可選參數,並可以設置為NULL。
返回值:成功獲取到信號量返回pdTRUE,否則返回pdFALSE。
例子:
SemaphoreHandle_t xSemaphore = NULL;
/*這個任務創建信號量 */
void vATask( void * pvParameters )
{
/*創建互斥型信號量,用於保護共享資源。*/
xSemaphore = xSemaphoreCreateMutex();
}
/* 這個任務使用信號量 */
void vAnotherTask( void * pvParameters )
{
/* ... 做其它事情. */
if( xSemaphore != NULL )
{
/*如果信號量無效,則最多等待10個系統節拍周期。*/
if( xSemaphoreTake( xSemaphore, ( TickType_t ) 10 ) == pdTRUE )
{
/*到這里我們獲取到信號量,現在可以訪問共享資源了*/
/* ... */
/* 完成訪問共享資源后,必須釋放信號量*/
xSemaphoreGive( xSemaphore );
}
else
{
/* 沒有獲取到信號量,這里處理異常情況。*/
}
}
}
獲取遞歸互斥量
BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex,
TickType_t xTicksToWait );
描述:獲取遞歸互斥信號量。
文件FreeRTOSConfig.h中的宏configUSE_RECURSIVE_MUTEXES必須設置成1,此函數才有效。
參數解析:
- xMutex:互斥量句柄,必須是使用API函數xSemaphoreCreateRecursiveMutex()返回的。
- xTickToWait:互斥量無效時,任務最多等待的時間,單位是系統節拍周期個數。使用宏portTICK_PERIOD_MS可以輔助將系統節拍個數轉化為實際時間(以毫秒為單位)。如果設置為0,表示不是設置等待時間。如果任務已經擁有信號量則xSemaphoreTakeRecursive()立即返回,不管xTickToWait是什么值。
返回值:成功獲取遞歸互斥量返回pdTURE,否則返回pdFALSE。
例子:
SemaphoreHandle_t xMutex = NULL;
// 這個任務創建互斥量
void vATask( void *pvParameters )
{
// 這個互斥量用於保護共享資源
xMutex =xSemaphoreCreateRecursiveMutex();
}
//這個任務使用互斥量
void vAnotherTask( void *pvParameters )
{
// ... 做其它事情.
if( xMutex != NULL )
{
// 如果互斥量無效,則最多等待10系統時鍾節拍周期.
if(xSemaphoreTakeRecursive( xMutex, ( TickType_t ) 10 ) == pdTRUE )
{
// 到這里我們成功獲取互斥量並可以訪問共享資源了
// ...
// 由於某種原因,某些代碼需要在一個任務中多次調用API函數
// xSemaphoreTakeRecursive()。當然不會像本例中這樣連續式
//調用,實際代碼會有更加復雜的結構
xSemaphoreTakeRecursive( xMutex, ( TickType_t ) 10 );
xSemaphoreTakeRecursive( xMutex, ( TickType_t ) 10 );
// 我們獲取一個互斥量三次,所以我們要將這個互斥量釋放三次
//它才會變得有效。再一次說明,實際代碼可能會更加復雜。
xSemaphoreGiveRecursive( xMutex );
xSemaphoreGiveRecursive( xMutex );
xSemaphoreGiveRecursive( xMutex );
// 到這里,這個共享資源可以被其它任務使用了.
}
else
{
// 處理異常情況
}
}
}
釋放信號量
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore );
// 中斷保護
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore,
signed BaseType_t *pxHigherPriorityTaskWoken );
描述:釋放一個信號量。
必須使用API函數xSemaphoreTake()獲取這個信號量。
參數解析:
- xSemaphore:信號量句柄。信號量必須是API函數xSemaphoreCreateBinary()、xSemaphoreCreateCounting()或xSemaphoreCreateMutex() 創建的。
- pxHigherPriorityTaskWoken:如果
pxHigherPriorityTaskWoken為pdTRUE,則需要在中斷退出前人為的進行一次上下文切換。從FreeRTOS V7.3.0開始,該參數為可選參數,並可以設置為NULL。
返回值:信號量釋放成功返回pdTRUE,否則返回pdFALSE。
例子:
SemaphoreHandle_t xSemaphore = NULL;
voidvATask( void * pvParameters )
{
// 創建一個互斥量,用來保護共享資源
xSemaphore = xSemaphoreCreateMutex();
if( xSemaphore != NULL )
{
if( xSemaphoreGive( xSemaphore ) != pdTRUE )
{
//我們希望這個函數調用失敗,因為首先要獲取互斥量
}
// 獲取信號量,不等待
if( xSemaphoreTake( xSemaphore, ( TickType_t ) 0 ) )
{
// 現在我們擁有互斥量,可以安全的訪問共享資源
if( xSemaphoreGive( xSemaphore ) != pdTRUE )
{
//我們不希望這個函數調用失敗,因為我們必須
//要釋放已獲取的互斥量
}
}
}
}
// xSemaphoreGiveFromISR 例子
#define LONG_TIME 0xffff
#define TICKS_TO_WAIT 10
SemaphoreHandle_t xSemaphore = NULL;
/* Repetitive task. */
void vATask( void * pvParameters )
{
/* 我們使用信號量同步,所以先創建一個二進制信號量.必須確保
在創建這個二進制信號量之前,中斷不會訪問它。*/
xSemaphore = xSemaphoreCreateBinary();
for( ;; )
{
/* 我們希望每產生10次定時器中斷,任務運行一次。*/
if( xSemaphoreTake( xSemaphore, LONG_TIME ) == pdTRUE )
{
/* 到這里成功獲取到信號量*/
...
/* 我們成功執行完一次,由於這是個死循環,所以任務仍會
阻塞在等待信號量上。信號量由ISR釋放。*/
}
}
}
/* 定時器 ISR */
void vTimerISR( void * pvParameters )
{
static unsigned char ucLocalTickCount = 0;
static signed BaseType_txHigherPriorityTaskWoken;
/*定時器中斷發生 */
...執行其它代碼
/*需要vATask() 運行嗎? */
xHigherPriorityTaskWoken = pdFALSE;
ucLocalTickCount++;
if( ucLocalTickCount >= TICKS_TO_WAIT )
{
/* 釋放信號量,解除vATask任務阻塞狀態 */
xSemaphoreGiveFromISR( xSemaphore, &xHigherPriorityTaskWoken );
/* 復位計數器 */
ucLocalTickCount = 0;
}
/* 如果 xHigherPriorityTaskWoken 表達式為真,需要執行一次上下文切換*/
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
釋放遞歸互斥量
BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t xMutex );
描述:釋放一個遞歸互斥量。
文件FreeRTOSConfig.h中宏configUSE_RECURSIVE_MUTEXES必須設置成1本函數才有效。
參數解析:
xMutex:互斥量句柄。必須是函數xSemaphoreCreateRecursiveMutex()返回的值。
返回值:如果遞歸互斥量釋放成功,返回pdTRUE。
獲取互斥量持有任務的句柄
TaskHandle_t xSemaphoreGetMutexHolder( SemaphoreHandle_t xMutex );
描述:返回互斥量持有任務的句柄(如果有的話)
文件FreeRTOSConfig.h中宏configUSE_MUTEXES必須設置成1本函數才有效。
參數解析:
xMutex:互斥量句柄
返回值:返回互斥量持有任務的句柄(如果有的話)。
以下情況返回NULL:
- 參數xMutex不是互斥類型信號量
- 雖然互斥量有效但這個互斥量不被任何任務持有
注意:如果調用此函數的任務持有互斥量,那么能夠可靠地返回任務句柄,但是如果是別的任務持有互斥量,則不總是可靠。
