1. 程式人生 > >μCOS 系列專題—實時系統及相關概念(1)

μCOS 系列專題—實時系統及相關概念(1)

第一章節  實時系統及相關概念

1.實時系統(Real-Time System)

實時系統主要是指系統能否在規定的時間內完成相應的處理邏輯,並得到正確的結果。主要包括:硬實時系統和軟實時系統。在軟實時系統中,任務可以儘可能快地得到處理,但並不是在精確的時間內完成。在硬實時系統中,任務不僅能夠正確完成,而且能夠按時完成。大多數實時系統都是軟實時和硬實時的綜合折中方案。實時應用涵蓋比較寬泛,一般地大多數嵌入式系統都是實時的,通過構建的實時系統,一臺計算機在使用者看來並不是一臺。實時應用的設計比起非實時應用要複雜和困難一些。

2.前後臺系統(Foreground/Background Systems)

前後臺系統,即計算機前後臺系統,早期的嵌入式系統中沒有作業系統的概念,程式設計師編寫嵌入式程式通常直接面對裸機及裸裝置,在這種情況下,通常把嵌入式程式分成兩部分,即前臺程式和後臺程式。

如下圖所示,應用程式是一個無限的迴圈,迴圈中呼叫相應的函式完成相應的操作,這部分可以看成後臺行為。前臺程式通過中斷來處理事件;後臺程式則掌管整個嵌入式系統軟、硬體資源的分配、管理以及任務的排程,是一個系統管理排程程式。這就是通常所說的前後臺系統。一般情況下,後臺程式也叫事件處理任務或中斷級任務,前臺程式也叫事件處理級程式。在程式執行時,後臺程式檢查每個任務是否具備執行條件,通過一定的排程演算法來完成相應的操作。對於實時性要求特別嚴格的操作通常由中斷來完成,僅在中斷服務程式中標記事件的發生,不再做任何工作就退出中斷,經過後臺程式的排程,轉由前臺程式完成事件的處理,這樣就不會造成在中斷服務程式中處理費時的事件而影響後續和其他中斷。


實際上,前後臺系統的實時性比預計的要差。這是因為前後臺系統認為所有的任務具有相同的優先級別,即是平等的,而且任務的執行又是通過FIFO佇列排隊,因而對那些實時性要求高的任務不可能立刻得到處理。另外,由於後臺程式是一個無限迴圈的結構,一旦在這個迴圈體中正在處理的任務崩潰,使得整個任務佇列中的其他任務得不到機會被處理,從而造成整個系統的崩潰。由於這類系統結構簡單,幾乎不需要RAM/ROM的額外開銷,因而在簡單的嵌入式應用被廣泛使用。

3.臨界區程式碼(Critical Section of Code)

要謹慎對待臨界區程式碼,一旦這部分程式碼執行之後,系統將不能被中斷,否則就會崩潰。為確保系統安全,在執行臨界區程式碼之前,一般採取的手段是關閉中斷,執行完畢後再開啟中斷。

4.資源(Resources)

資源通常是任務使用的一個實體,資源可以是一個I/O裝置如印表機、鍵盤或者顯示器,資源也可以是一個變數、結構體或者陣列。

5.共享資源(Shared Resource)

能夠被一個以上任務共同使用的資源稱為共享資源。為了防止資料訪問衝突,每個任務必須獲取獨立的訪問許可權後才能對共享資源進行操作,而這種機制就稱為互斥。

6.多工(Multitasking)

多工是CPU排程和切換處理機制。通過這種機制,單個CPU可以在多個任務之間切換執行,多工好比擁有多個上下文環境的前後臺系統。多工機制最大化的提高了CPU的利用率,並且為多模組應用提供了可能。多工最顯著的一個特點就是允許程式設計師去管理複雜的實時應用。因此,採用多工機制能夠使程式開發的設計和維護變得更加容易。

7.任務(Task)

任務有時候也稱為執行緒,任務可以看做是擁有CPU全部控制權的一個簡單程式。實施應用的設計包括將一項工作分割成多個子任務來分別處理問題的某個部分。如下圖所示,每個任務都被分配一個優先順序、CPU寄存值集以及它的棧。 


一般地,每個任務都是在這5個狀態之間的一個無限迴圈,同時只能處於一個狀態。這5個狀態分別:休眠(DORMANT)、就緒(READY)、 執行(RUNNING)、 等待(WAITING)和 中斷(ISR)。這五個狀態之間的遷移關係如下圖所示:

休眠:處於此狀態的任務仍舊駐留在記憶體中,但是已經不能成為多工核心的一部分。

就緒:處於此狀態的任務可以被執行,但是因為自身優先順序低於當前正在執行任務的優先順序。

執行:處於此狀態的任務擁有著CPU的使用權。

等待:處於此狀態的任務等待某個事件的發生,比如等待I/O操作的完成,等待共享資源的使用權,等待某個定時訊號的產生,或者就是等待一段時間。

中斷:處於此狀態的任務表示,當某個中斷已經發生,CPU正處於這個中斷服務處理過程中。

8.上下文切換(Context Switch)或任務切換(Task Switch)

當多工核心決定要進行任務切換時,需要將當前任務的上下文(CPU暫存器值)儲存到的上下文儲存區域中——也就是任務的棧中。當這個操作執行完畢後,就會從新任務的棧中恢復其上下文環境,然後從上次中斷的地方繼續執行。這個處理過程就稱為上下文切換或者任務切換。上下文切換增加了任務的額外管理的開銷,CPU暫存器越多,開銷就越高。上下文切換的執行時間取決於CPU有多少個暫存器需要儲存和恢復。實時核心的效能評判,主要是取決於核心單位時間內執行上下文切換的能力。

9.核心(Kernel)

核心屬於多工系統的一部分,主要負責任務管理(比如CPU的使用時間)和任務間通訊。核心提供的最基礎服務就是上下文切換。一般採用實時核心可以簡化系統的設計,通過將應用分割成多個任務,並交核心統一進行管理。

但是,由於核心提供的排程服務也需要時間,因此便增加了系統的額外開銷。開銷的多少取決於執行任務切換的頻度。在一個設計好的應用中,一個核心大概會佔用2%~5%的CPU執行時間。核心本身作為軟體也屬於應用中的一部分,他也會佔用額外的ROM(或者程式碼空間)開銷,以及用於儲存額外資料結構和任務棧的RAM,並且有快速耗盡記憶體的趨勢。

通常微控制器因為RAM較少而難以執行一個實時核心,核心會提供必要的的服務如:管理訊號量、郵箱、佇列、延時等,這樣就方便使用者更好的使用處理器。我相信,一旦你使用過實時核心的話,保證你以後再也不想回到前後臺系統的模式了。

10.排程器(Scheduler)

排程器作為核心的一部分,負責決定在接下來的任務切換中應該執行哪個任務。大多數實時核心都是基於優先順序的排程。每個任務在建立時都會根據其重要程度被指定一個優先順序,一般在應用中任務的優先順序都是明確好的。在基於優先順序的核心中,CPU的控制權一般會交給等待執行的高優先順序任務。但是,具體地何時高優先順序任務獲得CPU 是由具體使用的核心來決定的,這裡主要有兩種:佔先式核心和非佔先式核心。(佔先式就是在程式執行過程中,產生其他中斷訊號,這時核心轉到終端服務程式,等終端服務程式執行完成後,再繼續之前的程式。而非佔先式在產生其他終端後,不去執行終端服務程式,而是繼續執行當前程式,結束之後才去執行終端服務程式。)

11.非佔先式核心(Non-Preemptive Kernel)

在非佔先核心中,要求每個執行的任務能夠交出CPU的控制權。為維護併發的假象,核心必須定期地對這些任務進行處理。非佔先排程也稱為合作型多工,任務間通過相互協作來共享CPU。非同步事件仍舊由中斷系統服務來進行處理。在中斷事件處理中,可以將就緒的高優先順序任務切換到執行態,但還是要先返回到之前被中斷的任務中,只有在當前任務交出CPU控制權之後,新的高優先順序才可以獲得CPU的控制權。

非佔先核心的特點之一就是中斷延遲較低(可以參考後面章節對中斷的分析)。在任務級(或者從任務這個層面)來看,每個任務可以呼叫不可重入函式,而不必擔心其它任務可能也在使用這個函式,從而造成資料破壞。因為每個任務在執行完畢後才會交出CPU控制權。當然,不可重入函式不能夠被迫交出CPU控制權,也就說非佔先核心中每個任務都是以主動的方式交出CPU的控制權。

使用非佔先核心任務的響應速度比前後臺系統要慢一些,這是因為任務的響應取決於佔用時間最長的任務。

非佔先核心的另一個特點就是,很少需要使用訊號量去保護共享資料。每個任務都有自己的CPU,使用者不需要擔心某個任務被其他任務給搶佔。但這並不總是絕對的,在有些例項中,還是會使用到訊號量。共享I/O裝置仍舊需要使用互斥訊號量,比如一個任務在使用一臺印表機的時候仍舊需要互斥(否則,在一張紙的第一行列印A文章,在第二行打B文章)。

非佔先核心處理流程如下圖所示:

(1)一個正在執行的任務被中斷事件打斷;

(2)如果使能中斷,CPU通過中斷向量表跳轉到中斷處理程式;

(3)中斷處理程式處理事件,讓就緒的高優先順序任務切花到執行態;

(4)完成中斷處理任務,執行中斷返回指令,CPU繼續執行被中斷的任務。

(5)任務從中斷位置處繼續執行。

(6)任務執行完畢後,呼叫核心服務,釋放CPU的控制權並交給另一個任務(也就是之前在中斷服務例程中切換到就緒執行狀態的任務)。

(7)核心看到高優先順序任務已經切換到了準備執行狀態(核心並不關心這個任務是否是在中斷服務例程中被切換到這個狀態,無關緊要),執行上下文切換,從而之前在中斷服務例程中切換到就緒執行狀態的任務便真正執行起來。

高優先順序任務從就緒到執行必須等待較長的時間直到當前任務已經釋放了CPU控制權。就像前後臺系統中後臺執行的程式,在非佔先核心中任務的響應時間是不確定的,你從來都不知道最高優先順序任務何時會獲得CPU的控制權

概括的講,非佔先核心允許每個任務從執行直到自願放棄CPU的控制權。中斷會搶佔任務,直到中斷服務例程執行完畢,而後中斷服務例程返回到之前被中斷的任務,任務響應比前後太系統較好。商業核心中少有非佔先式核心。

12.佔先式核心(Preemptive Kernel)

在系統響應要求較高的場景中佔先式核心應用較多。正因為如此,µC/OS-II和大多數商業實時核心一樣都是佔先式的。等待執行的最高優先順序任務總是可以獲得CPU的控制權。當某個任務將較高優先順序任務切換到等待執行狀態,並且當前任務能夠被搶佔(或者可以被掛起),那麼高優先順序任務將立即獲得CPU的控制權。如果是在中斷服務例程將高優先順序任務切換到就緒態,那麼等中斷服務例程執行完畢後,之前被中斷的任務就會被掛起,新切換為就緒態的高優先順序任務將會被執行。

具體處理過程如下圖所示:

(1)正在執行的任務被中斷;

(2)如果使能中斷,CPU通過中斷向量表跳轉到中斷處理程式;

(3)中斷處理程式處理事件,讓就緒的高優先順序任務切花到執行態,中斷任務執行完畢後,核心提供的一個服務被喚醒(比如某個核心函式被呼叫);

(4)和(5)核心呼叫這個函式,得知一個更加重要的任務已經在等待執行,因此替代中斷任務返回的是,核心將進行上下文切換,並執行更加重要的任務。當這個任務完成之後,核心呼叫另一個函式讓這個任務進入休眠狀態,等待下一個事件(如中斷服務事件);

(6)和(7)核心得知要執行的下一個優先順序較低,因此,進行上下文切換繼續執行之前被中斷的那個任務。

由於佔先式核心執行最高優先順序任務的時間是確定的,因此你可以確定它什麼時候可以獲得CPU的控制權。如此一來,佔先式核心可以最小化任務的響應時間。

應用程式程式碼在使用佔先式核心時不需要呼叫不可重入函式,除非當一個高優先順序和一個低優先順序任務使用一個公共的函式時,為了能確保夠獨立訪問這些函式還是要使用互斥訊號量。如果呼叫不可重入函式時,高優先順序任務搶佔了一個低優先順序任務,不可重入函式的資料衝突可能會遭到破壞。綜上所述,佔先式核心總是先執行就緒的最高優先順序任務,即使中斷搶佔了某個任務的CPU控制權,但在中斷服務例程執行完畢後,核心會繼續執行就緒的最高優先順序任務(注意與非佔先式不一樣,此處不是去執行之前被中斷的任務)。任務的響應時間得到了優化,並且是確定的,µC/OS-II就是一個佔先式核心。

13.可重入(Reentrancy)

可重入函式是指可以被多個任務呼叫而不用擔心資料破壞。可重入函式可以在任意時刻被打斷,並且在隨後的執行過程中不會出現資料丟失。可重入函式要麼是使用區域性變數,要麼是使用全域性受保護的變數。下面是一個可重入函式的例子:

因為函式strcpy()的引數副本儲存在任務的棧中,因此當多個任務呼叫都函式strcpy()時,不用擔心某個任務會破壞其它任務的資料指標。

舉一個不可重入函式的例子,swap()函式是一個交換它的兩個引數值的簡單函式。為了下面討論的需要,我假設你用的是一個搶佔式核心,並切啟用了中斷,Temp是定義的一個全域性變數,如下所示:

程式設計師想讓swap()函式對任意一個函式都是可用的,下面我們來看一下,當一個正在執行swap()的低優先順序任務被打斷時會發生什麼:

(1)當swap()函式被打斷時Temp值為1;

(2)和(3)中斷服務程式(ISR)將高優先順序任務設定為就緒狀態,因此在ISR執行完畢後,核心(假設為µC/OS-II)被喚醒並切換到這個任務。在高優先順序任務中,變數Temp被設定為3且成功地完成了值的交換(在例中,z變成了4,t變成了3);

(4)呼叫核心服務延時服務OSTimeDly(1),高優先順序任務將CPU控制權交給了低優先順序任務,並等待下一個定時中斷再被呼叫(後面會詳細解釋);

(5)隨後,低優先順序任務繼續執行,注意此時Temp的值仍然是3。但是當低優先順序任務執行完畢後,y的值從3被設定成了1。

注意此處只是舉一個簡單的例子,如何能讓程式碼變得可重入也是比較明顯的。你可以通過下列手段來讓swap()函式成為可重入函式:

√將Temp宣告為區域性變數

√在執行這段程式碼之前關閉中斷,執行完畢後開啟中斷

√使用訊號量(後面會講到)

√其他不容易解決的情形。在測試階段不可重入函式產生的錯誤沒有暴露出來,一旦產品釋出時便很有可能發生。如果你對多工的使用還是個新手,那麼在使用到不可重入函式時候你就需要格外地小心。

如果中斷是在swap()函式執行之前和之後發生的,那麼對於例中的兩個任務,x和y都能夠正確地交換。

14.時間片輪番排程法(Round-Robin Scheduling)

當兩個或兩個以上任務擁有相同的優先順序,核心允許一個任務執行預定的時間——時間額度,然後切換到另一個任務,這也叫做時間片排程法。在下列情形中,核心將交出CPU控制權:

√當前任務在分配的時間片內無事可做

√當前任務在分配的時間片內提前完成

√時間片用完

當前,µC/OS-II還不支援時間片輪番排程,在應用中每個任務都有一個唯一的優先順序。

15.任務優先順序(Task Priority)

為每個任務指定一個優先順序,任務重要性程度越高,指派的優先順序就越高。通常你要決定為每個任務指定相應的優先順序。

16.靜態優先順序(Static Priority)

靜態優先順序是指在應用執行期間,任務的優先順序不會發生改變。這樣的話,在編譯的時可為每個任務指定一個固定的優先順序。在一個系統中那些優先順序為靜態地方,所有的任務和他們的定時閾值在編譯期都是已知的。

17.動態優先順序(Dynamic Priorities)

動態優先順序是指在應用執行期間,任務的優先順序可以發生改變。每個任務在執行期間可以改變它的優先順序,擁有這個特性的實時核心應當避免出現優先順序反轉的情況。

18.優先順序反轉(Priority Inversions)

使用實時核心時,優先順序反轉情況是實時系統出現得最多的一個問題。下圖解釋了一個優先順序反轉的情況。Task1具有比Task2更高的優先順序,Task2比Task3優先順序高。

(1)任務Task1和Task2都在等待事件發生,Task3正在執行中;

(2)此時,任務Task3需要請求一個訊號用於訪問共享資源;

(3)為了能夠獲得資源,任務Task3執行了一些必要操作;

(4)任務ask1等待的時間發生,因此核心將任務Task3掛起,然後啟動並執行任務Task1,因為此時Task1擁有較高的優先順序;

(5)任務Task1執行;

(6)任務Task1執行了一會之後,也想訪問某個共享資源(在例子中Task1試圖獲取Task3持有的訊號)。因為Task3持有著這個資源,Task1就被加入到等待這個訊號釋放的任務佇列中。

(7)Task3繼續執行;

(8)在Task3執行了一段時間之後,便被Task2搶佔了CPU的控制權,因為此時Task2等待的事件發生了。

(9)Task2繼續執行;

(10)Task2處理完它所等待的事件,將CPU控制權又交回給了Task3。

(11)Task3繼續執行;

(12)Task3使用資源完成工作之後,釋放了這個資源的訊號。此時,核心得知一個高優先順序任務正等待此此訊號,因此便進行上下文切換,繼續執行Task1;

(13)此時,Task1持有此訊號並且可以訪問這個共享資源。

在上述的整個過程中,任務Task1的優先順序彷彿降低到Task3之下,就是因為它需要等待Task3持有的資源。當Task2搶佔了Task3時,這種讓Task3被延遲的情況就更加惡化了。僅就在訪問這個共享資源是,你可以通過提升Task3的優先順序來及時糾正這種情況,當任務完成後再恢復為原始優先順序。Task3的優先順序應當提升到比其他共同競爭此資源的全部任務優先順序還要高的一個等級,多工核心應當允許任務優先順序的動態修改以避免出現優先順序反轉的問題。然而,這樣需要花費一些時間去修改任務的優先順序。如果Task3在被Task1和Task2搶佔之前已經完成了共享資源的訪問會怎樣?這時你在訪問共享資源之前提升了Task3的優先順序,然後在訪問完畢後恢復到原始優先順序,這樣一來就浪費一定的CPU時間。真正需要防止優先順序反轉,核心需要能夠自動改變任務的優先順序。這就佳作優先順序繼承,µC/OS-II提供了這種特性。


(1)和(2)如前面例子中一樣Task 3正在執行,但是此時因為訪問一個共享變數而需要請求一個互斥訊號;

(3)和(4)Task 3訪問這個資源,然後被Task1搶佔;

(5)和(6)Task 1執行並嘗試去獲取互斥訊號。核心得知Task 3已經持有互斥訊號,並且Task 3優先順序比Task1要低一些,這種情況下,核心將Task3的優先順序提升至與Task1一樣的等級;

(7)核心將Task 1加入到互斥訊號的佇列中,然後繼續執行Task3,這樣一來Task3繼續持有著這個共享資源;

(8)當Task3完成之後,釋放互斥訊號,此時核心將Task3的優先順序恢復至原始值,然後檢視此互斥訊號等待佇列中是否有等待此訊號的任務。核心得知Task1正在等待,將此互斥訊號交給它;

(9)現在,Task1就可以自由的訪問這個共享資源了;

(10)和(11)當任務Task1執行完畢後,優先順序處於中間的任務Task2獲得CPU控制權。注意:在第(3)和第(10)步驟之間,本來任務Task2已經就緒等待執行,但這並不影響最終結果。這裡仍舊有一些無法避免優先順序反轉的問題,但至少遠已經降低了優先順序反轉造成的延遲情況的惡化程度。