嵌入式系統/鎖和臨界區
RTOS 的重要部分是鎖機制和臨界區 (CS) 的實現。本節將討論建立這些機制時涉及的一些問題。
大多數嵌入式系統至少有一個數據結構,它由一個任務寫入,由另一個任務讀取。使用搶佔式排程器,很容易編寫在大多數情況下 *看起來* 執行良好的軟體,但偶爾編寫者會在更新資料結構的中間被中斷,RTOS 切換到閱讀者任務,然後閱讀者會因不一致的資料而阻塞。
我們需要一種方法來安排事物,以便編寫者的修改看起來是 “原子” 的——閱讀者總是隻看到(一致的)舊版本或(一致的)新版本,而不是一些部分修改的不一致狀態。
有很多方法可以避免這個問題,包括
- 設計資料結構,以便編寫者可以以始終保持一致狀態的方式更新它。這需要支援原子原語的硬體,這些原語足夠強大,可以將資料結構從一個一致的狀態原子地更新到下一個一致的狀態。 維基百科:無鎖和無等待演算法。例如,我們在其他地方討論的 讀兩次並比較 演算法。
- 讓編寫者在更新資料結構時關閉任務排程器。然後,閱讀者可能看到資料結構的唯一時間是資料結構處於一致狀態。
- 讓編寫者在更新資料結構時關閉所有中斷(包括啟動任務排程器的計時器中斷)。然後,閱讀者可能看到資料結構的唯一時間是資料結構處於一致狀態。但這會使中斷延遲變得更糟。
- 使用與每個資料結構關聯的 “鎖”。當閱讀者看到編寫者正在更新資料結構時,讓閱讀者告訴任務排程器執行其他程序,直到編寫者完成。(有許多種鎖)。
- 使用與每個使用資料結構的例程關聯的 “監視器”。
無論何時呼叫鎖或 CS 機制,重要的是 RTOS 要停用排程器,以防止原子操作被搶佔並錯誤地執行。請記住,嵌入式系統需要穩定和健壯,因此我們不能冒險讓作業系統本身在嘗試建立鎖或臨界區時被搶佔。如果我們有一個名為 DisableScheduler( ) 的函式,我們可以在嘗試任何原子操作之前呼叫該函式來停用排程器,然後我們可以使用一個名為 EnableScheduler( ) 的函式來恢復排程器,並繼續正常操作。
現在讓我們建立一個進入臨界區的通用函式
EnterCS()
{
DisableScheduler();
return;
}
以及一個退出臨界區的函式
ExitCS()
{
EnableScheduler();
return;
}
透過在臨界區期間停用排程器,我們保證了在臨界區期間不會發生搶佔式任務切換。
這種方法的缺點是它會減慢系統速度,並阻止其他時間敏感的任務執行。接下來,我們將展示一種可以實現臨界區以允許搶佔的方法。
臨界區,就像計算中的任何其他術語一樣,可能具有與僅僅防止搶佔的操作不同的定義。例如,許多系統將 CS 定義為一個防止多個任務進入給定程式碼段的物件。假設我們在系統上實現 malloc( ) 的版本。我們要確保一旦記憶體分配嘗試開始,就沒有任何其他記憶體分配嘗試可以開始。一次只能進行 1 次記憶體分配嘗試。但是,我們希望允許 malloc 函式像其他任何函式一樣被搶佔。為了實現這一點,我們需要一個名為 CRITICAL_SECTION 或 CRIT_X 或類似名稱的新資料物件。我們的 malloc 函式現在看起來像這樣
CRIT_SECT mallocCS; //a global CS variable, for use in all tasks.
int RTOS_main(void) //we register our CS in the beginning of the RTOS main routine
{
AllocCS(mallocCS); //register our critical section with the OS, to prevent duplicates
...
void *malloc(size_t size)
{
void *ptr;
EnterCS(mallocCS); //we enter the CS, and no other instance of malloc can enter it.
ptr = FindFreeMemory(size);
ExitCS(mallocCS); //other malloc attempts can now proceed
return ptr;
}
如果兩個任務幾乎同時呼叫 malloc,第一個任務將進入臨界區,而第二個任務將等待或在 EnterCS 例程中 “阻塞”。當第一個 malloc 完成時,第二個 malloc 的 EnterCS 函式將返回,並且函式將繼續。
為了允許其他檢視其他資料結構的程序繼續執行,即使此資料結構已被鎖定,EnterCS() 通常被重新定義為類似於
// non-blocking attempt to enter critical section
int TryEnterCS( CRIT_SECT this )
{
int success = 0;
DisableScheduler();
if( this->lock == 0 ){
this->lock = 1; // mark structure as locked
success = 1;
};
EnableScheduler();
return success;
}
// blocking attempt to enter critical section
EnterCS( CRIT_SECT this ){
int success = 0;
do{
success = TryEnterCS( this->lock );
if( !success ){ Yield(); }// tell scheduler to run some other task for a while.
}while( !success );
return;
}
// release lock
ExitCS( CRIT_SECT this )
{
ASSERT( 1 == this->lock );
this->lock = 0;
return;
}
建立臨界區物件並使用它來防止敏感區域被搶佔的價值在於,這種方案不會像第一種方案那樣減慢系統速度(透過停用排程器,並阻止其他任務執行)。
某些作業系統,如 Dragonfly BSD,使用 “序列化令牌” 實現 EnterCS() 和 ExitCS(),這樣,當一個程序嘗試在另一個數據結構上獲取鎖時,作業系統會在給該程序在所有請求的鎖上獲取鎖之前,短暫地釋放該程序持有的所有鎖。