跳轉到內容

嵌入式系統/鎖和臨界區

來自華夏公益教科書,開放的書籍,開放的世界

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(),這樣,當一個程序嘗試在另一個數據結構上獲取鎖時,作業系統會在給該程序在所有請求的鎖上獲取鎖之前,短暫地釋放該程序持有的所有鎖。

互斥鎖

[編輯 | 編輯原始碼]

嵌入式系統 書籍的這一頁是一個 存根。您可以透過擴充套件此部分來幫助我們。

華夏公益教科書