蘇州永節電子科技有限公司
設為首頁 | 收藏本站
新聞詳情

單片機開發實時系統概念

瀏覽數:1
文章附圖

TOC \o "1-3" \h \z 第2章實時系統概念1

2.0前后臺系統 Foreground/Background System1

2.1代碼的臨界段2

2.2資源2

2.3共享資源2

2.4多任務2

2.5任務2

2.6任務切換(Context Switch or Task Switch)3

2.7內核(Kernel3

2.8調度(Scheduler4

2.9不可剝奪型內核 Non-Preemptive Kernel4

2.10剝奪型內核5

2.11可重入性(Reentrancy5

2.12時間片輪番調度法7

2.13任務優先級7

2.142.14靜態優先級7

2.15動態優先級7

2.16優先級反轉7

2.17任務優先級分配8

2.18互斥條件10

2.18.1關中斷和開中斷10

2.18.2測試并置位11

2.18.3禁止,然后允許任務切換11

2.18.4信號量(Semaphores)12

2.19死鎖(或抱死)Deadlock (or Deadly Embrace)16

2.20同步16

2.21事件標志(Event Flags)18

2.22任務間的通訊(Intertask Communication)18

2.23消息郵箱(Message Mail boxes)18

2.24消息隊列(Message Queue)19

2.25中斷20

2.26中斷延遲20

2.27中斷響應21

2.28中斷恢復時間(Interrupt Recovery)21

2.29中斷延遲、響應和恢復22

2.30中斷處理時間22

2.31非屏蔽中斷(NMI)23

2.32時鐘節拍(Clock Tick)24

2.33對存儲器的需求25

2.34使用實時內核的優缺點26

2.35實時系統小結26


第2
實時系統概念

實時系統的特點是,如果邏輯和時序出現偏差將會引起嚴重后果的系統。有兩種類型的實時系統:軟實時系統和硬實時系統。在軟實時系統中系統的宗旨是使各個任務運行得越快越好,并不要求限定某一任務必須在多長時間內完成。

在硬實時系統中,各任務不僅要執行無誤而且要做到準時。大多數實時系統是二者的結合。實時系統的應用涵蓋廣泛的領域,而多數實時系統又是嵌入式的。這意味著計算機建在系統內部,用戶看不到有個計算機在系統里面。以下是一些嵌入式系統的例子:

















實時應用軟件的設計一般比非實時應用軟件設計難一些。本章講述實時系統概念。

3.
前后臺系統Foreground/Background System

不復雜的小系統一般設計成如圖2.1所示的樣子。這種系統可稱為前后臺系統或超循環系統(Super-Loops)。應用程序是一個無限的循環,循環中調用相應的函數完成相應的操作,這部分可以看成后臺行為(background)。中斷服務程序處理異步事件,這部分可以看成前臺行為(foreground)。后臺也可以叫做任務級。前臺也叫中斷級。時間相關性很強的關鍵操作(Critical operation)一定是靠中斷服務來保證的。因為中斷服務提供的信息一直要等到后臺程序走到該處理這個信息這一步時才能得到處理,這種系統在處理信息的及時性上,比實際可以做到的要差。這個指標稱作任務級響應時間。最壞情況下的任務級響應時間取決于整個循環的執行時間。因為循環的執行時間不是常數,程序經過某一特定部分的準確時間也是不能確定的。進而,如果程序修改了,循環的時序也會受到影響。


圖2-1前后臺系統


很多基于微處理器的產品采用前后臺系統設計,例如微波爐、電話機、玩具等。在另外一些基于微處理器的應用中,從省電的角度出發,平時微處理器處在停機狀態(halt),所有的事都靠中斷服務來完成。

3.
代碼的臨界段

代碼的臨界段也稱為臨界區,指處理時不可分割的代碼。一旦這部分代碼開始執行,則不允許任何中斷打入。為確保臨界段代碼的執行,在進入臨界段之前要關中斷,而臨界段代碼執行完以后要立即開中斷。(參閱2.03共享資源)

3.
資源

任何為任務所占用的實體都可稱為資源。資源可以是輸入輸出設備,例如打印機、鍵盤、顯示器,資源也可以是一個變量,一個結構或一個數組等。

3.
共享資源

可以被一個以上任務使用的資源叫做共享資源。為了防止數據被破壞,每個任務在與共享資源打交道時,必須獨占該資源。這叫做互斥(mutual exclusion)。在2.18節“互斥”中,將對技術上如何保證互斥條件做進一步討論。

3.
多任務

多任務運行的實現實際上是靠CPU(中央處理單元)在許多任務之間轉換、調度。CPU只有一個,輪番服務于一系列任務中的某一個。多任務運行很像前后臺系統,但后臺任務有多個。多任務運行使CPU的利用率得到最大的發揮,并使應用程序模塊化。在實時應用中,多任務化的最大特點是,開發人員可以將很復雜的應用程序層次化。使用多任務,應用程序將更容易設計與維護。

3.
任務

一個任務,也稱作一個線程,是一個簡單的程序,該程序可以認為CPU完全只屬該程序自己。實時應用程序的設計過程,包括如何把問題分割成多個任務,每個任務都是整個應用的某一部分,每個任務被賦予一定的優先級,有它自己的一套CPU寄存器和自己的棧空間(如圖2.2所示)。


圖2.2多任務。

典型地、每個任務都是一個無限的循環。每個任務都處在以下5種狀態之一的狀態下,這5種狀態是休眠態,就緒態、運行態、掛起態(等待某一事件發生)和被中斷態(參見圖2.3)                 休眠態相當于該任務駐留在內存中,但并不被多任務內核所調度。就緒意味著該任務已經準備好,可以運行了,但由于該任務的優先級比正在運行的任務的優先級低,還暫時不能運行。運行態的任務是指該任務掌握了CPU的控制權,正在運行中。掛起狀態也可以叫做等待事件態WAITING,指該任務在等待,等待某一事件的發生,(例如等待某外設的I/O操作,等待某共享資源由暫不能使用變成能使用狀態,等待定時脈沖的到來或等待超時信號的到來以結束目前的等待,等等)。最后,發生中斷時,CPU提供相應的中斷服務,原來正在運行的任務暫不能運行,就進入了被中斷狀態。圖2.3表示μC/OS-Ⅱ中一些函數提供的服務,這些函數使任務從一種狀態變到另一種狀態。


圖2.3任務的狀態

3.
任務切換(Context Switch or Task Switch)

Context Switch 在有的書中翻譯成上下文切換,實際含義是任務切換,或CPU寄存器內容切換。當多任務內核決定運行另外的任務時,它保存正在運行任務的當前狀態(Context),即CPU寄存器中的全部內容。這些內容保存在任務的當前狀況保存區(Task’s Context Storage area),也就是任務自己的棧區之中。(見圖2.2)。入棧工作完成以后,就是把下一個將要運行的任務的當前狀況從該任務的棧中重新裝入CPU的寄存器,并開始下一個任務的運行。這個過程叫做任務切換。任務切換過程增加了應用程序的額外負荷。CPU的內部寄存器越多,額外負荷就越重。做任務切換所需要的時間取決于CPU有多少寄存器要入棧。實時內核的性能不應該以每秒鐘能做多少次任務切換來評價。

3.
內核(Kernel)

多任務系統中,內核負責管理各個任務,或者說為每個任務分配CPU時間,并且負責任務之間的通訊。內核提供的基本服務是任務切換。之所以使用實時內核可以大大簡化應用系統的設計,是因為實時內核允許將應用分成若干個任務,由實時內核來管理它們。內核本身也增加了應用程序的額外負荷,代碼空間增加ROM的用量,內核本身的數據結構增加了RAM的用量。但更主要的是,每個任務要有自己的棧空間,這一塊吃起內存來是相當厲害的。內核本身對CPU的占用時間一般在2到5個百分點之間。

單片機一般不能運行實時內核,因為單片機的RAM很有限。通過提供必不可缺少 的系統服務,諸如信號量管理,郵箱、消息隊列、延時等,實時內核使得CPU的利用更為有效。一旦讀者用實時內核做過系統設計,將決不再想返回到前后臺系統。

3.
調度Scheduler)

調度(Scheduler),英文還有一詞叫dispatcher,也是調度的意思。這是內核的主要職責之一,就是要決定該輪到哪個任務運行了。多數實時內核是基于優先級調度法的。每個任務根據其重要程度的不同被賦予一定的優先級。基于優先級的調度法指,CPU總是讓處在就緒態的優先級最高的任務先運行。然而,究竟何時讓高優先級任務掌握CPU的使用權,有兩種不同的情況,這要看用的是什么類型的內核,是不可剝奪型的還是可剝奪型內核。

3.
不可剝奪型內核 (Non-Preemptive Kernel)

不可剝奪型內核要求每個任務自我放棄CPU的所有權。不可剝奪型調度法也稱作合作型多任務,各個任務彼此合作共享一個CPU。異步事件還是由中斷服務來處理。中斷服務可以使一個高優先級的任務由掛起狀態變為就緒狀態。但中斷服務以后控制權還是回到原來被中斷了的那個任務,直到該任務主動放棄CPU的使用權時,那個高優先級的任務才能獲得CPU的使用權。

不可剝奪型內核的一個優點是響應中斷快。在討論中斷響應時會進一步涉及這個問題。在任務級,不可剝奪型內核允許使用不可重入函數。函數的可重入性以后會討論。每個任務都可以調用非可重入性函數,而不必擔心其它任務可能正在使用該函數,從而造成數據的破壞。因為每個任務要運行到完成時才釋放CPU的控制權。當然該不可重入型函數本身不得有放棄CPU控制權的企圖。

使用不可剝奪型內核時,任務級響應時間比前后臺系統快得多。此時的任務級響應時間取決于最長的任務執行時間。

不可剝奪型內核的另一個優點是,幾乎不需要使用信號量保護共享數據。運行著的任務占有CPU,而不必擔心被別的任務搶占。但這也不是絕對的,在某種情況下,信號量還是用得著的。處理共享I/O設備時仍需要使用互斥型信號量。例如,在打印機的使用上,仍需要滿足互斥條件。圖2.4示意不可剝奪型內核的運行情況,任務在運行過程之中,[L2.4(1)]中斷來了,如果此時中斷是開著的,CPU由中斷向量[F2.4(2)]進入中斷服務子程序,中斷服務子程序做事件處理[F2.4(3)],使一個有更高級的任務進入就緒態。中斷服務完成以后,中斷返回指令[F2.4(4)], 使CPU回到原來被中斷的任務,接著執行該任務的代碼[F2.4(5)]直到該任務完成,調用一個內核服務函數以釋放CPU控制權,由內核將控制權交給那個優先級更高的、并已進入就緒態的任務[F2.4(6)],這個優先級更高的任務才開始處理中斷服務程序標識的事件[F2.4(7)]。   


圖2.4不可剝奪型內核

不可剝奪型內核的最大缺陷在于其響應時間。高優先級的任務已經進入就緒態,但還不能運行,要等,也許要等很長時間,直到當前運行著的任務釋放CPU。與前后系統一樣,

不可剝奪型內核的任務級響應時間是不確定的,不知道什么時候最高優先級的任務才能拿到CPU的控制權,完全取決于應用程序什么時候釋放CPU。

總之,不可剝奪型內核允許每個任務運行,直到該任務自愿放棄CPU的控制權。中斷可以打入運行著的任務。中斷服務完成以后將CPU控制權還給被中斷了的任務。任務級響應時間要大大好于前后系統,但仍是不可知的,商業軟件幾乎沒有不可剝奪型內核。

3.
剝奪型內核

當系統響應時間很重要時,要使用可剝奪型內核。因此,μC/OS-Ⅱ以及絕大多數商業上銷售的實時內核都是可剝奪型內核。最高優先級的任務一旦就緒,總能得到CPU的控制權。當一個運行著的任務使一個比它優先級高的任務進入了就緒態,當前任務的CPU使用權就被剝奪了,或者說被掛起了,那個高優先級的任務立刻得到了CPU的控制權。如果是中斷服務子程序使一個高優先級的任務進入就緒態,中斷完成時,中斷了的任務被掛起,優先級高的那個任務開始運行。如圖2.5所示。


圖2.5可剝奪型內核

使用可剝奪型內核,最高優先級的任務什么時候可以執行,可以得到CPU的控制權是可知的。使用可剝奪型內核使得任務級響應時間得以最優化。

使用可剝奪型內核時,應用程序不應直接使用不可重入型函數。調用不可重入型函數時,要滿足互斥條件,這一點可以用互斥型信號量來實現。如果調用不可重入型函數時,低優先級的任務CPU的使用權被高優先級任務剝奪,不可重入型函數中的數據有可能被破壞。綜上所述,可剝奪型內核總是讓就緒態的高優先級的任務先運行,中斷服務程序可以搶占CPU,到中斷服務完成時,內核讓此時優先級最高的任務運行(不一定是那個被中斷了的任務)。任務級系統響應時間得到了最優化,且是可知的。μC/OS-Ⅱ屬于可剝奪型內核。

3.
可重入性(Reentrancy)

可重入型函數可以被一個以上的任務調用,而不必擔心數據的破壞。可重入型函數任何時候都可以被中斷,一段時間以后又可以運行,而相應數據不會丟失。可重入型函數或者只使用局部變量,即變量保存在CPU寄存器中或堆棧中。如果使用全局變量,則要對全局變量予以保護。程序2.1是一個可重入型函數的例子。


程序清單2.1可重入型函數

void strcpy(char *dest, char *src)

{

    while (*dest++ = *src++) {

        ;

    }

    *dest = NUL;

}



函數Strcpy()做字符串復制。因為參數是存在堆棧中的,故函數Strcpy()可以被多個任務調用,而不必擔心各任務調用函數期間會互相破壞對方的指針。

不可重入型函數的例子如程序2.2所示。Swap()是一個簡單函數,它使函數的兩個形式變量的值互換。為便于討論,假定使用的是可剝奪型內核,中斷是開著的,Temp定義為整數全程變量。


程序清單 2.2    不可重入型函數

int Temp;


void swap(int *x, int *y)

{

    Temp = *x;

    *x   = *y;

    *y   = Temp;

}


程序員打算讓Swap() 函數可以為任何任務所調用,如果一個低優先級的任務正在執行Swap()函數,而此時中斷發生了,于是可能發生的事情如圖2.6所示。[F2.6(1)]表示中斷發生時Temp已被賦值1,中斷服務子程序使更優先級的任務就緒,當中斷完成時[F2.6(2)],內核(假定使用的是μC/OS-Ⅱ)使高優先級的那個任務得以運行[F2.6(3)],高優先級的任務調用Swap()函數是Temp賦值為3。這對該任務本身來說,實現兩個變量的交換是沒有問題的,交換后Z的值是4,X的值是3。然后高優先級的任務通過調用內核服務函數中的延遲一個時鐘節拍[F2.6(4)],釋放了CPU的使用權,低優先級任務得以繼續運行[F2.6(5)].注意,此時Temp的值仍為3!在低優先級任務接著運行時,Y的值被錯誤地賦為3,而不是正確值1。


圖2.6不可重入性函數

請注意,這只是一個簡單的例子,如何能使代碼具有可重入性一看就明白。然而有些情況下,問題并非那么易解。應用程序中的不可重入函數引起的錯誤很可能在測試時發現不了,直到產品到了現場問題才出現。如果在多任務上您還是把新手,使用不可重入型函數時,千萬要當心。

使用以下技術之一即可使Swap()函數具有可重入性:


?把Temp定義為局部變量

?調用Swap()函數之前關中斷,調動后再開中斷

?用信號量禁止該函數在使用過程中被再次調用


如果中斷發生在Swap()函數調用之前或調用之后,兩個任務中的X,Y值都會是正確的。

3.
時間片輪番調度法(ucos不支持)

當兩個或兩個以上任務有同樣優先級,內核允許一個任務運行事先確定的一段時間,叫做時間額度(quantum),然后切換給另一個任務。也叫做時間片調度。內核在滿足以下條件時,把CPU控制權交給下一個任務就緒態的任務:

?當前任務已無事可做

?當前任務在時間片還沒結束時已經完成了。

目前,μC/OS-Ⅱ不支持時間片輪番調度法。應用程序中各任務的優先級必須互不相同。

3.
任務優先級

每個任務都有其優先級。任務越重要,賦予的優先級應越高。

3.
2.14靜態優先級

應用程序執行過程中諸任務優先級不變,則稱之為靜態優先級。在靜態優先級系統中,諸任務以及它們的時間約束在程序編譯時是已知的。

3.
動態優先級

應用程序執行過程中,任務的優先級是可變的,則稱之為動態優先級。實時內核應當避免出現優先級反轉問題。

3.
優先級反轉

使用實時內核,優先級反轉問題是實時系統中出現得最多的問題。圖2.7解釋優先級反轉是如何出現的。如圖,任務1優先級高于任務2,任務2優先級高于任務3。任務1和任務2處于掛起狀態,等待某一事件的發生,任務3正在運行如[圖2.7(1)]。此時,任務3要使用其共享資源。使用共享資源之前,首先必須得到該資源的信號量(Semaphore)(見2. 18.04信號量)。任務3得到了該信號量,并開始使用該共享資源[圖2.7(2)]。由于任務1優先級高,它等待的事件到來之后剝奪了任務3的CPU使用權[圖2.7(3)],任務1開始運行[圖2.7(4)]。運行過程中任務1也要使用那個任務3正在使用著的資源,由于該資源的信號量還被任務3占用著,任務1只能進入掛起狀態,等待任務3釋放該信號量[圖2.7(5)]。任務3得以繼續運行[圖2.7(6)]。由于任務2的優先級高于任務3,當任務2等待的事件發生后,任務2剝奪了任務3的CPU的使用權[圖2.7(7)]并開始運行。處理它該處理的事件[圖2.7(8)],直到處理完之后將CPU控制權還給任3[圖2.7(9)]。任務3接著運行[圖2.7(10)],直到釋放那個共享資源的信號量[圖27(11)]。直到此時,由于實時內核知道有個高優先級的任務在等待這個信號量,內核做任務切換,使任務1得到該信號量并接著運行[圖2.7(12)]。

在這種情況下,任務1優先級實際上降到了任務3 的優先級水平。因為任務1要等,直等到任務3釋放占有的那個共享資源。由于任務2剝奪任務3的CPU使用權,使任務1的狀況更加惡化,任務2使任務1增加了額外的延遲時間。任務1和任務2的優先級發生了反轉

糾正的方法可以是,在任務3使用共享資源時,提升任務3的優先級。任務完成時予以恢復。任務3的優先級必須升至最高,高于允許使用該資源的任何任務。多任務內核應允許動態改變任務的優先級以避免發生優先級反轉現象。然而改變任務的優先級是很花時間的。如果任務3并沒有先被任務1剝奪CPU使用權,又被任務2搶走了CPU使用權,花很多時間在共享資源使用前提升任務3的優先級,然后又在資源使用后花時間恢復任務3的優先級,則無形中浪費了很多CPU時間。真正需要的是,為防止發生優先級反轉,內核能自動變換任務的優先級,這叫做優先級繼承(Priority inheritance)但μC/OS-Ⅱ不支持優先級繼承,一些商業內核有優先級繼承功能。


圖2.7優先級反轉問題

圖2.8解釋如果內核支持優先級繼承的話,在上述例子中會是怎樣一個過程。任務3在運行[圖2.8(1)],任務3申請信號量以獲得共享資源使用權[圖2.8(2)],任務3得到并開始使用共享資源[圖2.8(3)]。后來CPU使用權被任務1剝奪[圖2.8(4)],任務1開始運行[圖2.8(5)],任務1申請共享資源信號量[圖2.8(6)]。此時,內核知道該信號量被任務3占用了,而任務3的優先級比任務1低,內核于是將任務3的優先級升至與任務1一樣,,然而回到任務3繼續運行,使用該共享資源[圖2.7(7)],直到任務3釋放共享資源信號量[圖2。8(8)]。這時,內核恢復任務3本來的優先級并把信號量交給任務1,任務1得以順利運行。 [圖2.8(9)],任務1完成以后[圖2.8(10)]那些任務優先級在任務1與任務3之間的任務例如任務2才能得到CPU使用權,并開始運行 [圖2.8(11)]。注意,任務2在從[圖2.8(3)]到[圖2.8(10)]的任何一刻都有可能進入就緒態,并不影響任務1、任務3的完成過程。在某種程度上,任務2和任務3之間也還是有不可避免的優先級反轉。


圖2.8

3.
任務優先級分配

給任務定優先級可不是件小事,因為實時系統相當復雜。許多系統中,并非所有的任務都至關重要。不重要的任務自然優先級可以低一些。實時系統大多綜合了軟實時和硬實時這兩種需求。軟實時系統只是要求任務執行得盡量快,并不要求在某一特定時間內完成。硬實時系統中,任務不但要執行無誤,還要準時完成。

一項有意思的技術可稱之為單調執行率調度法RMS(Rate Monotonic Scheduling),用于分配任務優先級。這種方法基于哪個任務執行的次數最頻繁,執行最頻繁的任務優先級最高。見圖2.9。


圖2.9 基于任務執行頻繁度的優先級分配法


任務執行頻繁度(Hz)

RMS做了一系列假設:

?所有任務都是周期性的

?任務間不需要同步,沒有共享資源,沒有任務間數據交換等問題

?CPU必須總是執行那個優先級最高且處于就緒態的任務。換句話說,要使用可剝奪型調度法。

給出一系列n值表示系統中的不同任務數,要使所有的任務滿足硬實時條件,必須使不等式[2.1]成立,這就是RMS定理:


[2.1]


 這里Ei是任務i最長執行時間,Ti是任務i的執行周期。換句話說,Ei/Ti是任務i所需的CPU時間。表2.1給出n(21/n - 1 )的值,n是系統中的任務數。對于無窮多個任務,極限值是   或0.693。這就意味著,基于RMS,要任務都滿足硬實時條件,所有有時間條件要求的任務i總的CPU利用時間應小于70%!請注意,這是指有時間條件要求的任務,系統中當然還可以有對時間沒有什么要求的任務,使得CPU的利用率達到100%。使CPU利用率達到100%并不好,因為那樣的話程序就沒有了修改的余地,也沒法增加新功能了。作為系統設計的一條原則,CPU利用率應小于60%到70%。

    RMS認為最高執行率的任務具有最高的優先級,但最某些情況下,最高執行率的任務并非是最重要的任務。如果實際應用都真的像RMS說的那樣,也就沒有什么優先級分配可討論了。然而討論優先級分配問題,RMS無疑是一個有意思的起點。


表2.1基于任務到CPU最高允許使用率.


任務數

n(21/n - 1)

1

1.000

2

0.828

3

0.779

4

0.756

5

0.743

.

.

.

.

.

.

0.693


3.
互斥條件

實現任務間通訊最簡便到辦法是使用共享數據結構。特別是當所有到任務都在一個單一地址空間下,能使用全程變量、指針、緩沖區、鏈表、循環緩沖區等,使用共享數據結構通訊就更為容易。雖然共享數據區法簡化了任務間的信息交換,但是必須保證每個任務在處理共享數據時的排它性,以避免競爭和數據的破壞。與共享資源打交道時,使之滿足互斥條件最一般的方法有:

?關中斷

?使用測試并置位指令

?禁止做任務切換

?利用信號量

3.0.
關中斷和開中斷

處理共享數據時保證互斥,最簡便快捷的辦法是關中斷和開中斷。如示意性代碼程序2.3所示:


程序清單2.3    關中斷和開中斷

Disable interrupts;                                           /*關中斷*/

Access the resource (read/write from/to variables);     /*讀/寫變量*/

Reenable interrupts;                                         /*重新允許中斷*/



   μC/OS-Ⅱ在處理內部變量和數據結構時就是使用的這種手段,即使不是全部,也是絕大部分。實際上μC/OS-Ⅱ提供兩個宏調用,允許用戶在應用程序的C代碼中關中斷然后再開中斷:OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()[參見8.03.02 OS_ENTER_CRITICAL()和OS_EXIT_CRITICALL()],這兩個宏調用的使用法見程序2.4


程序清單2.4利用μC/OS_Ⅱ 宏調用關中斷和開中斷

void Function (void)

{

    OS_ENTER_CRITICAL();

    .

    .    /*在這里處理共享數據*/

    .

    OS_EXIT_CRITICAL();

}



可是,必須十分小心,關中斷的時間不能太長。因為它影響整個系統的中斷響應時間,即中斷延遲時間。當改變或復制某幾個變量的值時,應想到用這種方法來做。這也是在中斷服務子程序中處理共享變量或共享數據結構的唯一方法。在任何情況下,關中斷的時間都要盡量短。

如果使用某種實時內核,一般地說,關中斷的最長時間不超過內核本身的關中斷時間,就不會影響系統中斷延遲。當然得知道內核里中斷關了多久。凡好的實時內核,廠商都提供這方面的數據。總而言之,要想出售實時內核,時間特性最重要。

3.0.
測試并置位

如果不使用實時內核,當兩個任務共享一個資源時,一定要約定好,先測試某一全程變量,如果該變量是0,允許該任務與共享資源打交道。為防止另一任務也要使用該資源,前者只要簡單地將全程變量置為1,這通常稱作測試并置位(Test-And-Set),或稱作TASTAS操作可能是微處理器的單獨一條不會被中斷的指令,或者是在程序中關中斷做TAS操作再開中斷,如程序清單2.5所示。


程序清單2.5 利用測試并置位處理共享資源

Disable interrupts;                              關中斷

if (‘Access Variable’ is 0) {                  如果資源不可用,標志為0

    Set variable to 1;                           置資源不可用,標志為1

    Reenable interrupts;                         重開中斷

    Access the resource;                         處理該資源

    Disable interrupts;                          關中斷

    Set the ‘Access Variable’ back to 0;      清資源不可使用,標志為0

    Reenable interrupts;                         重新開中斷

} else {                                            否則

    Reenable interrupts;                          開中斷

/* You don’t have access to the resource, try back later; */

/* 資源不可使用,以后再試; */

}


有的微處理器有硬件的TAS指令(如Motorola 68000系列,就有這條指令)

3.0.
禁止,然后允許任務切換

如果任務不與中斷服務子程序共享變量或數據結構,可以使用禁止、然后允許任務切換。(參見3.06給任務切換上鎖和開鎖)。如程序清單2.6所示,以μC/OS-Ⅱ的使用為例,兩個或兩個以上的任務可以共享數據而不發生競爭。注意,此時雖然任務切換是禁止了,但中斷還是開著的。如果這時中斷來了,中斷服務子程序會在這一臨界區內立即執行。中斷服務子程序結束時,盡管有優先級高的任務已經進入就緒態,內核還是返回到原來被中斷了的任務。直到執行完給任務切換開鎖函數OSSchedUnlock (),內核再看有沒有優先級更高的任務被中斷服務子程序激活而進入就緒態,如果有,則做任務切換。雖然這種方法是可行的,但應該盡量避免禁止任務切換之類操作,因為內核最主要的功能就是做任務的調度與協調。禁止任務切換顯然與內核的初衷相違。應該使用下述方法。


程序清單2.6   用給任務切換上鎖,然后開鎖的方法實現數據共享.

void Function (void)

{

    OSSchedLock();

    .

    .    /* You can access shared data in here (interrupts are recognized) */

.    /*在這里處理共享數據(中斷是開著的)*/

    OSSchedUnlock();




3.0.
信號量(Semaphores)

信號量是60年代中期Edgser Dijkstra 發明的。信號量實際上是一種約定機制,在多任務內核中普遍使用.信號量用于:

?控制共享資源的使用權(滿足互斥條件)

?標志某事件的發生

?使兩個任務的行為同步

(譯者注:信號與信號量在英文中都叫做Semaphore,并不加以區分,而說它有兩種類型,二進制型(binary)和計數器型(counting)。本書中的二進制型信號量實際上是只取兩個值0和1的信號量。實際上 這個信號量只有一位,這種信號量翻譯為信號更為貼切。而二進制信號量通常指若干位的組合。而本書中解釋為事件標志的置位與清除(

見2.21))。

信號像是一把鑰匙,任務要運行下去,得先拿到這把鑰匙。如果信號已被別的任務占用,該任務只得被掛起,直到信號被當前使用者釋放。換句話說,申請信號的任務是在說:“把鑰匙給我,如果誰正在用著,我只好等!”信號是只有兩個值的變量,信號量是計數式的。只取兩個值的信號是只有兩個值0和1的量,因此也稱之為信號量。計數式信號量的值可以是0到255或0到65535,或0到4294967295,取決于信號量規約機制使用的是8位、16位還是32位。到底是幾位,實際上是取決于用的哪種內核。根據信號量的值,內核跟蹤那些等待信號量的任務。

一般地說,對信號量只能實施三種操作:初始化(INITIALIZE),也可稱作建立(CREATE);等信號(WAIT)也可稱作掛起(PEND);給信號(SIGNAL)或發信號(POST)。信號量初始化時要給信號量賦初值,等待信號量的任務表(Waiting list)應清為空。

想要得到信號量的任務執行等待(WAIT)操作。如果該信號量有效(即信號量值大于0),則信號量值減1,任務得以繼續運行。如果信號量的值為0,等待信號量的任務就被列入等待信號量任務表。多數內核允許用戶定義等待超時,如果等待時間超過了某一設定值時,該信號量還是無效,則等待信號量的任務進入就緒態準備運行,并返回出錯代碼(指出發生了等待超時錯誤)。

任務以發信號操作(SIGNAL)釋放信號量。如果沒有任務在等待信號量,信號量的值僅僅是簡單地加1。如果有任務在等待該信號量,那么就會有一個任務進入就緒態,信號量的值也就不加1。于是鑰匙給了等待信號量的諸任務中的一個任務。至于給了那個任務,要看內核是如何調度的。收到信號量的任務可能是以下兩者之一。

?等待信號量任務中優先級最高的,或者是

?最早開始等待信號量的那個任務,即按先進先出的原則(First In First Out ,FIFO)

有的內核有選擇項,允許用戶在信號量初始化時選定上述兩種方法中的一種。但μC/OS-Ⅱ只支持優先級法。如果進入就緒態的任務比當前運行的任務優先級高(假設,是當前任務釋放的信號量激活了比自己優先級高的任務)。則內核做任務切換(假設,使用的是可剝奪型內核),高優先級的任務開始運行。當前任務被掛起。直到又變成就緒態中優先級最高任務。

程序清單2.7示意在μC/OS-Ⅱ中如何用信號量處理共享數據。要與同一共享數據打交道的任務調用等待信號量函數OSSemPend()。處理完共享數據以后再調用釋放信號量函數OSSemPost()。這兩個函數將在以后的章節中描述。要注意的是,在使用信號量之前,一定要對該信號量做初始化。作為互斥條件,信號量初始化為1。使用信號量處理共享數據不增加中斷延遲時間,如果中斷服務程序或當前任務激活了一個高優先級的任務,高優先級的任務立即開始執行。


程序清單2.7   通過獲得信號量處理共享數據

OS_EVENT *SharedDataSem;

void Function (void)

{

    INT8U err;

    OSSemPend(SharedDataSem, 0, &err);

    .

    .    /* You can access shared data in here (interrupts are recognized) */

    .    /*共享數據的處理在此進行,(中斷是開著的)*/

    OSSemPost(SharedDataSem);

}


當諸任務共享輸入輸出設備時,信號量特別有用。可以想象,如果允許兩個任務同時給打印機送數據時會出現什么現象。打印機會打出相互交叉的兩個任務的數據。例如任務1要打印“I am Task!”,而任務2要打印“I am Task2!”可能打印出來的結果是:“I Ia amm T Tasask k1!2!”

在這種情況下,使用信號量并給信號量賦初值1(用二進制信號量)。規則很簡單,要想使用打印機的任務,先要得到該資源的信號量。圖2.10兩個任務競爭得到排它性打印機使用權,圖中信號量用一把鑰匙表示,想使用打印機先要得到這把鑰匙。


圖2.10用獲取信號量來得到打印機使用權


上例中,每個任務都知道有個信號表示資源可不可以使用。要想使用該資源,要先得到這個信號。然而有些情況下,最好把信號量藏起來,各個任務在同某一資源打交道時,并不知道實際上是在申請得到一個信號量。例如,多任務共享一個RS-232C外設接口,各任務要送命令給接口另一端的設備并接收該設備的回應。如圖2.11所示。

調用向串行口發送命令的函數CommSendCmd(),該函數有三個形式參數:Cmd指向送出的ASCII碼字符串命令。Response指向外設回應的字符串。timeout指設定的時間間隔。如果超過這段時間外設還不響應,則返回超時錯誤信息。函數的示意代碼如程序清單2.8所示。


程序清單 2.8   隱含的信號量

INT8U CommSendCmd(char *cmd, char *response, INT16U timeout)

{

    Acquire port's semaphore;

    Send command to device;

    Wait for response (with timeout);

    if (timed out) {

        Release semaphore;

        return (error code);

    } else {

        Release semaphore;

        return (no error);

    }

}


要向外設發送命令的任務得調用上述函數。設信號量初值為1,表示允許使用。初始化是在通訊口驅動程序的初始化部分完成的。第一個調用CommSendCmd()函數的任務申請并得到了信號量,開始向外設發送命令并等待響應。而另一個任務也要送命令,此時外設正“忙”,則第二個任務被掛起,直到該信號量重新被釋放。第二個任務看起來同調用了一個普通函數一樣,只不過這個函數在沒有完成其相應功能時不返回。當第一個任務釋放了那個信號量,第二個任務得到了該信號量,第二個任務才能使用RS-232口。


圖2.11在任務級看不到隱含的信號量

計數式信號量用于某資源可以同時為幾個任務所用例如,用信號量管理緩沖區陣列(buffer pool),如圖2.12所示。緩沖區陣列中共有10個緩沖區,任務通過調用申請緩沖區函數BufReq()向緩沖區管理方申請得到緩沖區使用權。當緩沖區使用權還不再需要時,通過調用釋放緩沖區函數BufRel()將緩沖區還給管方。函數示意碼如程序清單2.9所示



程序清單 2.9   用信號量管理緩沖區。

BUF *BufReq(void)

{

   BUF *ptr;


   Acquire a semaphore;

   Disable interrupts;

   ptr         = BufFreeList;

   BufFreeList = ptr->BufNext;

   Enable interrupts;

   return (ptr);

}



void BufRel(BUF *ptr)

{

   Disable interrupts;

   ptr->BufNext = BufFreeList;

   BufFreeList   = ptr;

   Enable interrupts;

   Release semaphore;

}


圖2.12 計數式信號量的用法

緩沖區陣列管理方滿足前十個申請緩沖區的任務,就好像有10把鑰匙可以發給諸任務。當所有的鑰匙都用完了,申請緩沖區的任務被掛起,直到信號量重新變為有效。緩沖區管理程序在處理鏈表指針時,為滿足互斥條件,中斷是關掉的(這一操作非常快)。任務使用完某一緩沖區,通過調用緩沖區釋放函數BufRel()將緩沖區還給系統。系統先將該緩沖區指針插入到空閑緩沖區鏈表中(Linked list)然后再給信號量加1或釋放該信號量。這一過程隱含在緩沖區管理程序BufReq()和BufRel()之中,調用這兩個函數的任務不用管函數內部的詳細過程。

信號量常被用過了頭。處理簡單的共享變量也使用信號量則是多余的。請求和釋放信號量的過程是要花相當的時間的。有時這種額外的負荷是不必要的。用戶可能只需要關中斷、開中斷來處理簡單共享變量,以提高效率。(參見2.18.0.1 關中斷和開中斷)。假如兩個任務共享一個32位的整數變量,一個任務給這個變量加1,另一個任務給這個變量清0。如果注意到不管哪種操作,對微處理器來說,只花極短的時間,就不會使用信號量來滿足互斥條件了。每個任務只需操作這個任務前關中斷,之后再開中斷就可以了。然而,如果這個變量是浮點數,而相應微處理器又沒有硬件的浮點協處理器,浮點運算的時間相當長,關中斷時間長了會影響中斷延遲時間,這種情況下就有必要使用信號量了。

3.
死鎖(或抱死)(Deadlock (or Deadly Embrace))

死鎖也稱作抱死,指兩個任務無限期地互相等待對方控制著的資源。設任務T1正獨享資源R1,任務T2在獨享資源T2,而此時T1又要獨享R2,T2也要獨享R1,于是哪個任務都沒法繼續執行了,發生了死鎖。最簡單的防止發生死鎖的方法是讓每個任務都:


?先得到全部需要的資源再做下一步的工作

?用同樣的順序去申請多個資源

?釋放資源時使用相反的順序


內核大多允許用戶在申請信號量時定義等待超時,以此化解死鎖。當等待時間超過了某一確定值,信號量還是無效狀態,就會返回某種形式的出現超時錯誤的代碼,這個出錯代碼告知該任務,不是得到了資源使用權,而是系統錯誤。死鎖一般發生在大型多任務系統中,在嵌入式系統中不易出現。

3.
同步

可以利用信號量使某任務與中斷服務同步(或者是與另一個任務同步,這兩個任務間沒有數據交換)。如圖2.13所示。注意,圖中用一面旗幟,或稱作一個標志表示信號量。這個標志表示某一事件的發生(不再是一把用來保證互斥條件的鑰匙)。用來實現同步機制的信號量初始化成0,信號量用于這種類型同步的稱作單向同步(unilateral rendezvous)。一個任務做I/O操作,然后等信號回應。當I/O操作完成,中斷服務程序(或另外一個任務)發出信號,該任務得到信號后繼續往下執行。


圖2.13 用信號量使任務與中斷服務同步

如果內核支持計數式信號量,信號量的值表示尚未得到處理的事件數。請注意,可能會有一個以上的任務在等待同一事件的發生,則這種情況下內核會根據以下原則之一發信號給相應的任務:


?發信號給等待事件發生的任務中優先級最高的任務,或者

?發信號給最先開始等待事件發生的那個任務


根據不同的應用,發信號以標識事件發生的中斷服務或任務也可以是多個。

兩個任務可以用兩個信號量同步它們的行為。如圖2.14所示。這叫做雙向同步(bilateral rendezvous)。雙向同步同單向同步類似,只是兩個任務要相互同步。

例如則程序清單2.10中,運行到某一處的第一個任務發信號給第二個任務[L22.10(1)],然后等待信號返回[L2.10(2)]。同樣,當第二個任務運行到某一處時發信號給第一個任務[2.10(3)]等待返回信號[L2.10(4)]。至此,兩個任務實現了互相同步。在任務與中斷服務之間不能使用雙向同步,因為在中斷服務中不可能等一個信號量。


圖2.14 兩個任務用信號量同步彼此的行為



程序清單2.10   雙向同步

Task1()

{

    for (;;) {

        Perform operation;

        Signal task #2;                    (1)

        Wait for signal from task #2;      (2)

        Continue operation;

    }

}


Task2()

{

    for (;;) {

        Perform operation;

        Signal task #1;                    (3)

        Wait for signal from task #1;      (4)

        Continue operation;

    }

}

3.
事件標志(Event Flags)

當某任務要與多個事件同步時,要使用事件標志。若任務需要與任何事件之一發生同步,可稱為獨立型同步(即邏輯或關系)。任務也可以與若干事件都發生了同步,稱之為關聯型(邏輯與關系)。獨立型及關聯型同步如圖2.15所示。


圖2.15獨立型及關聯型同步

可以用多個事件的組合發信號給多個任務。如圖2.16所示,典型地,8個、16個或32個事件可以組合在一起,取決于用的哪種內核。每個事件占一位(bit),以32位的情況為多。任務或中斷服務可以給某一位置位或復位,當任務所需的事件都發生了,該任務繼續執行,至于哪個任務該繼續執行了,是在一組新的事件發生時辨定的。也就是在事件位置位時做辨斷。

內核支持事件標志,提供事件標志置位、事件標志清零和等待事件標志等服務。事件標志可以是獨立型或組合型。μC/OS-Ⅱ目前不支持事件標志.

3.
任務間的通訊(Intertask Communication)

有時很需要任務間的或中斷服務與任務間的通訊。這種信息傳遞稱為任務間的通訊。任務間信息的傳遞有兩個途徑:通過全程變量或發消息給另一個任務。

用全程變量時,必須保證每個任務或中斷服務程序獨享該變量。中斷服務中保證獨享的唯一辦法是關中斷。如果兩個任務共享某變量,各任務實現獨享該變量的辦法可以是關中斷再開中斷,或使用信號量(如前面提到的那樣)。請注意,任務只能通過全程變量與中斷服務程序通訊,而任務并不知道什么時候全程變量被中斷服務程序修改了,除非中斷程序以信號量方式向任務發信號或者是該任務以查詢方式不斷周期性地查詢變量的值。要避免這種情況,用戶可以考慮使用郵箱或消息隊列。


圖2.16事件標志

3.
消息郵箱(Message Mail boxes)

通過內核服務可以給任務發送消息。典型的消息郵箱也稱作交換消息,是用一個指針型變量,通過內核服務,一個任務或一個中斷服務程序可以把一則消息(即一個指針)放到郵箱里去。同樣,一個或多個任務可以通過內核服務接收這則消息。發送消息的任務和接收消息的任務約定,該指針指向的內容就是那則消息。

每個郵箱有相應的正在等待消息的任務列表,要得到消息的任務會因為郵箱是空的而被掛起,且被記錄到等待消息的任務表中,直到收到消息。一般地說,內核允許用戶定義等待超時,等待消息的時間超過了,仍然沒有收到該消息,這任務進入就緒態,并返回出錯信息,報告等待超時錯誤。消息放入郵箱后,或者是把消息傳給等待消息的任務表中優先級最高的那個任務(基于優先級),或者是將消息傳給最先開始等待消息的任務(基于先進先出)。圖2.17示意把消息放入郵箱。用一個I字表示郵箱,旁邊的小砂漏表示超時計時器,計時器旁邊的數字表示定時器設定值,即任務最長可以等多少個時鐘節拍(Clock Ticks),關于時鐘節拍以后會講到。

內核一般提供以下郵箱服務:


?郵箱內消息的內容初始化,郵箱里最初可以有,也可以沒有消息

?將消息放入郵箱(POST)

?等待有消息進入郵箱(PEND)

?如果郵箱內有消息,就接受這則消息。如果郵箱里沒有消息,則任務并不被掛起(ACCEPT),用返回代碼表示調用結果,是收到了消息還是沒有收到消息。


消息郵箱也可以當作只取兩個值的信號量來用。郵箱里有消息,表示資源可以使用,而空郵箱表示資源已被其它任務占用。


圖2.17 消息郵箱

3.
消息隊列(Message Queue)

消息隊列用于給任務發消息。消息隊列實際上是郵箱陣列。通過內核提供的服務,任務或中斷服務子程序可以將一條消息(該消息的指針)放入消息隊列。同樣,一個或多個任務可以通過內核服務從消息隊列中得到消息。發送和接收消息的任務約定,傳遞的消息實際上是傳遞的指針指向的內容。通常,先進入消息隊列的消息先傳給任務,也就是說,任務先得到的是最先進入消息隊列的消息,即先進先出原則(FIFO)。然而μC/OS-Ⅱ也允許使用后進先出方式(LIFO)。

像使用郵箱那樣,當一個以上的任務要從消息隊列接收消息時,每個消息隊列有一張等待消息任務的等待列表(Waiting List)。如果消息隊列中沒有消息,即消息隊列是空,等待消息的任務就被掛起并放入等待消息任務列表中,直到有消息到來。通常,內核允許等待消息的任務定義等待超時的時間。如果限定時間內任務沒有收到消息,該任務就進入就緒態并開始運行,同時返回出錯代碼,指出出現等待超時錯誤。一旦一則消息放入消息隊列,該消息將傳給等待消息的任務中優先級最高的那個任務,或是最先進入等待消息任務列表的任務。圖2.18示意中斷服務子程序如何將消息放入消息隊列。圖中兩個大寫的I表示消息隊列,“10”表示消息隊列最多可以放10條消息,沙漏旁邊的0表示任務沒有定義超時,將永遠等下去,直至消息的到來。


典型地,內核提供的消息隊列服務如下:


?消息隊列初始化。隊列初始化時總是清為空。

?放一則消息到隊列中去(Post)

?等待一則消息的到來(Pend)

?如果隊列中有消息則任務可以得到消息,但如果此時隊列為空,內核并不將該任務掛起(Accept)。如果有消息,則消息從隊列中取走。沒有消息則用特別的返回代碼通知調用者,隊列中沒有消息。


圖2.18 消息隊列

3.
中斷

中斷是一種硬件機制,用于通知CPU有個異步事件發生了。中斷一旦被識別,CPU保存部分(或全部)現場(Context)即部分或全部寄存器的值,跳轉到專門的子程序,稱為中斷服務子程序(ISR)。中斷服務子程序做事件處理,處理完成后,程序回到:


?在前后臺系統中,程序回到后臺程序

?對不可剝奪型內核而言,程序回到被中斷了的任務

?對可剝奪型內核而言,讓進入就緒態的優先級最高的任務開始運行


中斷使得CPU可以在事件發生時才予以處理,而不必讓微處理器連續不斷地查詢(Polling)是否有事件發生。通過兩條特殊指令:關中斷(Disable interrupt)和開中斷(Enable interrupt)可以讓微處理器不響應或響應中斷。在實時環境中,關中斷的時間應盡量的短。關中斷影響中斷延遲時間(見2.26中斷延遲)。關中斷時間太長可能會引起中斷丟失。微處理器一般允許中斷嵌套,也就是說在中斷服務期間,微處理器可以識別另一個更重要的中斷,并服務于那個更重要的中斷,如圖2.19所示。

3.
中斷延遲

可能實時內核最重要的指標就是中斷關了多長時間。所有實時系統在進入臨界區代碼段之前都要關中斷,執行完臨界代碼之后再開中斷。關中斷的時間越長,中斷延遲就越長。中斷延遲由表達式[2.2]給出。


[2.2]   中斷延遲 = 關中斷的最長時間 + 開始執行中斷服務子程序的第一條指令的時間


圖2.19中斷嵌套

3.
中斷響應

中斷響應定義為從中斷發生到開始執行用戶的中斷服務子程序代碼來處理這個中斷的時間。中斷響應時間包括開始處理這個中斷前的全部開銷。典型地,執行用戶代碼之前要保護現場,將CPU的各寄存器推入堆棧。這段時間將被記作中斷響應時間。

對前后臺系統,保存寄存器以后立即執行用戶代碼,中斷響應時間由[2.3]給出。


[2.3]   中斷響應時間 = 中斷延遲 + 保存CPU內部寄存器的時間


對于不可剝奪型內核,微處理器保存內部寄存器以后,用戶的中斷服務子程序代碼全立即得到執行。不可剝奪型內核的中斷響應時間由表達式[2.4]給出。


[2.4]   中斷響應時間 = 中斷延遲 + 保存CPU內部寄存器的時間


對于可剝奪型內核,則要先調用一個特定的函數,該函數通知內核即將進行中斷服務,使得內核可以跟蹤中斷的嵌套。對于 μC/OS-Ⅱ說來,這個函數是OSIntEnter(),可剝奪型內核的中斷響應時間由表達式[2.5]給出:


[2.5]   中斷響應 = 中斷延遲 + 保存CPU內部寄存器的時間 + 內核的進入中斷服務函數的執行時間


中斷響應是系統在最壞情況下的響應中斷的時間,某系統100次中有99次在50μs之內響應中斷,只有一次響應中斷的時間是250μs,只能認為中斷響應時間是250μs。

3.
中斷恢復時間(Interrupt Recovery)

中斷恢復時間定義為微處理器返回到被中斷了的程序代碼所需要的時間。在前后臺系統中,中斷恢復時間很簡單,只包括恢復CPU內部寄存器值的時間和執行中斷返回指令的時間。中斷恢復時間由[2.6]式給出。


[2.6]   中斷恢復時間 = 恢復CPU內部寄存器值的時間 + 執行中斷返回指令的時間


和前后臺系統一樣,不可剝奪型內核的中斷恢復時間也很簡單,只包括恢復CPU內部寄存器值的時間和執行中斷返回指令的時間,如表達式[2.7]所示。


[2.7]   中斷恢復時間 = 恢復CPU內部寄存器值的時間 + 執行中斷返回指令的時間


對于可剝奪型內核,中斷的恢復要復雜一些。典型地,在中斷服務子程序的末尾,要調用一個由實時內核提供的函數。在μC/OS-Ⅱ中,這個函數叫做OSIntExit(),這個函數用于辨定中斷是否脫離了所有的中斷嵌套。如果脫離了嵌套(即已經可以返回到被中斷了的任務級時),內核要辨定,由于中斷服務子程序ISR的執行,是否使得一個優先級更高的任務進入了就緒態。如果是,則要讓這個優先級更高的任務開始運行。在這種情況下,被中斷了的任務只有重新成為優先級最高的任務而進入就緒態時才能繼續運行。對于可剝奪型內核,中斷恢復時間由表達式[2.8]給出。


[2.8]   中斷恢復時間 = 判定是否有優先級更高的任務進入了就緒態的時間 + 恢復那個優先級更高任務的CPU內部寄存器的時間 + 執行中斷返回指令的時間


3.
中斷延遲、響應和恢復

圖2.20到圖2.22示意前后臺系統、不可剝奪性內核、可剝奪性內核相應的中斷延遲、響應和恢復過程。

注意,對于可剝奪型實時內核,中斷返回函數將決定是返回到被中斷的任務[圖2.22A],還是讓那個優先級最高任務運行。是中斷服務子程序使那個優先級更高的任務進入了就緒態[圖2.22B]。在后一種情況下,恢復中斷的時間要稍長一些,因為內核要做任務切換。在本書中,我做了一張執行時間表,此表多少可以衡量執行時間的不同,假定μC/OS-Ⅱ是在33MHZ Intel 80186微處理器上運行的。此表可以使讀者看到做任務切換的時間開銷。(見表9.3,在33MHZ 80186上μC/OS-Ⅱ服務的執行時間).

3.
中斷處理時間

雖然中斷服務的處理時間應該盡可能的短,但是對處理時間并沒有絕對的限制。不能說中斷服務必須全部小于100μS,500μS或1mS。如果中斷服務是在任何給定的時間開始,且中斷服務程序代碼是應用程序中最重要的代碼,則中斷服務需要多長時間就應該給它多長時間。然而在大多數情況下,中斷服務子程序應識別中斷來源,從叫中斷的設備取得數據或狀態,并通知真正做該事件處理的那個任務。當然應該考慮到是否通知一個任務去做事件處理所花的時間比處理這個事件所花的時間還多。在中斷服務中通知一個任務做時間處理(通過信號量、郵箱或消息隊列)是需要一定時間的,如果事件處理需花的時間短于給一個任務發通知的時間,就應該考慮在中斷服務子程序中做事件處理并在中斷服務子程序中開中斷,以允許優先級更高的中斷打入并優先得到服務。


圖2.20中斷延遲、響應和恢復(前后臺模式)


3.
非屏蔽中斷(NMI)

有時,中斷服務必須來得盡可能地快,內核引起的延時變得不可忍受。在這種情況下可以使用非屏蔽中斷,絕大多數微處理器有非屏蔽中斷功能。通常非屏蔽中斷留做緊急處理用,如斷電時保存重要的信息。然而,如果應用程序沒有這方面的要求,非屏蔽中斷可用于時間要求最苛刻的中斷服務。下列表達式給出如何確定中斷延遲、中斷響應時間和中斷恢復時間。


[2.9] 中斷延遲時間 = 指令執行時間中最長的那個時間 + 開始做非屏蔽中斷服務的時間


[2.10] 中斷響應時間 = 中斷延遲時間 + 保存CPU寄存器花的時間


[2.11] 中斷恢復時間 = 恢復CPU寄存器的時間 + 執行中斷返回指令的時間。


在一項應用中,我將非屏蔽中斷用于可能每150μS發生一次的中斷。中斷處理時間在80至125μS之間。所使用的內核的關中斷時間是45μS。可以看出,如果使用可屏蔽中斷的話,中斷響應會推遲20μS。

在非屏蔽中斷的中斷服務子程序中,不能使用內核提供的服務,因為非屏蔽中斷是關不掉的,故不能在非屏蔽中斷處理中處理臨界區代碼。然而向非屏蔽中斷傳送參數或從非屏蔽中斷獲取參數還是可以進行的。參數的傳遞必須使用全程變量,全程變量的位數必須是一次讀或寫能完成的,即不應該是兩個分離的字節,要兩次讀或寫才能完成。


圖2.21中斷延遲、響應和恢復(不可剝奪型內核)


圖2.22中斷延遲、響應和恢復(可剝奪型內核)


非屏蔽中斷可以用增加外部電路的方法禁止掉,如圖2.23所示。假定中斷源和非屏蔽中斷都是正邏輯,用一個簡單的“與”門插在中斷源和微處理器的非屏蔽中斷輸入端之間。向輸出口(Output Port)寫0就將中斷關了。不一定要以這種關中斷方式來使用內核服務,但可以用這種方式在中斷服務子程序和任務之間傳遞參數(大的、多字節的,一次讀寫不能完成的變量)。


圖2.23非屏蔽中斷的禁止

假定非屏蔽中斷服務子程序每40次執行中有一次要給任務發信號,如果非屏蔽中斷150μS執行一次,則每6mS(40*150μS)給任務發一次信號。在非屏蔽中斷服務子程序中,不能使用內核服務給任務發信號,但可以使用如圖2.24所示的中斷機制。即用非屏蔽中斷產生普通可屏蔽中斷的機制。在這種情況下,非屏蔽中斷通過某一輸出口產生硬件中斷(置輸出口為有效電平)。由于非屏蔽中斷服務通常具有最高的優先級,在非屏蔽中斷服務過程中不允許中斷嵌套,普通中斷一直要等到非屏蔽中斷服務子程序運行結束后才能被識別。在非屏蔽中斷服務子程序完成以后,微處理器開始響應這個硬件中斷。在這個中斷服務子程序中,要清除中斷源(置輸出口為無效電平),然后用信號量去喚醒那個需要喚醒的任務。任務本身的運行時間和信號量的有效時間都接近6mS,實時性得到了滿足。


                  圖2.24非屏蔽中斷產生普通可屏蔽中斷

3.
時鐘節拍(Clock Tick)

時鐘節拍是特定的周期性中斷。這個中斷可以看作是系統心臟的脈動。中斷之間的時間間隔取決于不同的應用,一般在10mS到200mS之間。時鐘的節拍式中斷使得內核可以將任務延時若干個整數時鐘節拍,以及當任務等待事件發生時,提供等待超時的依據。時鐘節拍率越快,系統的額外開銷就越大。

各種實時內核都有將任務延時若干個時鐘節拍的功能。然而這并不意味著延時的精度是1個時鐘節拍,只是在每個時鐘節拍中斷到來時對任務延時做一次裁決而已。

圖2.25到 圖2.27示意任務將自身延遲一個時鐘節拍的時序。陰影部分是各部分程序的執行時間。請注意,相應的程序運行時間是長短不一的,這反映了程序中含有循環和條件轉移語句(即if/else, switch, ? : 等語句)的典型情況。時間節拍中斷服務子程序的運行時間也是不一樣的。盡管在圖中畫得有所夸大。

第一種情況如圖2.25所示,優先級高的任務和中斷服務超前于要求延時一個時鐘節拍的任務運行。可以看出,雖然該任務想要延時20mS,但由于其優先級的緣故,實際上每次延時多少是變化的,這就引起了任務執行時間的抖動。

第二種情況,如圖2.26所示,所有高優先級的任務和中斷服務的執行時間略微小于一個時鐘節拍。如果任務將自己延時一個時鐘節拍的請求剛好發生在下一個時鐘節拍之前,這個任務的再次執行幾乎是立即開始的。因此,如果要求任務的延遲至少為一個時鐘節拍的話,則要多定義一個延時時鐘節拍。換句話說,如果想要將一個任務至少延遲5個時鐘節拍的話,得在程序中延時6個時鐘節拍。



圖2.25將任務延遲一個時鐘節拍(第一種情況)


圖2.26將任務延遲一個時鐘節拍(第二種情況)


圖2.27將任務延遲一個時鐘節拍(第三種情況)


第三種情況,如圖2.27所示,所有高優先級的任務加上中斷服務的執行時間長于一個時鐘節拍。在這種情況下,擬延遲一個時鐘節拍的任務實際上在兩個時鐘節拍后開始運行,引起了延遲時間超差。這在某些應用中或許是可以的,而在多數情況下是不可接受的。

上述情況在所有的實時內核中都會出現,這與CPU負荷有關,也可能與系統設計不正確有關。以下是這類問題可能的解決方案:


?增加微處理器的時鐘頻率

?增加時鐘節拍的頻率

?重新安排任務的優先級

?避免使用浮點運算(如果非使用不可,盡量用單精度數)

?使用能較好地優化程序代碼的編譯器

?時間要求苛刻的代碼用匯編語言寫

?如果可能,用同一家族的更快的微處理器做系統升級。如從8086向80186升級,從68000向68020升級等


不管怎么樣,抖動總是存在的。

3.
對存儲器的需求

如果設計是前后臺系統,對存儲器容量的需求僅僅取決于應用程序代碼。而使用多任務內核時的情況則很不一樣。內核本身需要額外的代碼空間(ROM)。內核的大小取決于多種因素,取決于內核的特性,從1K到100K字節都是可能的。8位CPU用的最小內核只提供任務調度、任務切換、信號量處理、延時及超時服務約需要1K到3K代碼空間。代碼空間總需求量由表達式[2.12]給出。


[2.12]   總代碼量 = 應用程序代碼 + 內核代碼


因為每個任務都是獨立運行的,必須給每個任務提供單獨的棧空間(RAM)。應用程序設計人員決定分配給每個任務多少棧空間時,應該盡可能使之接近實際需求量 (有時,這是相當困難的一件事)。棧空間的大小不僅僅要計算任務本身的需求 (局部變量、函數調用等等),還需要計算最多中斷嵌套層數(保存寄存器、中斷服務程序中的局部變量等)。根據不同的目標微處理器和內核的類型,任務棧和系統棧可以是分開的。系統棧專門用于處理中斷級代碼。這樣做有許多好處,每個任務需要的棧空間可以大大減少。內核的另一個應該具有的性能是,每個任務所需的棧空間大小可以分別定義(μC/OS-II可以做到)。相反,有些內核要求每個任務所需的棧空間都相同。所有內核都需要額外的棧空間以保證內部變量、數據結構、隊列等。如果內核不支持單獨的中斷用棧,總的RAM需求由表達式[2.13]給出。


[2.13] RAM總需求 = 應用程序的RAM需求 + (任務棧需求 + 最大中斷嵌套棧需求) * 任務數


如果內核支持中斷用棧分離,總RAM需求量由表達式[2.14]給出


[2.14]=RAM總需求 = 應用程序的RAM需求 + 內核數據區的RAM需求 + 各任務棧需求之總和 + 最多中斷嵌套之棧需求


除非有特別大的RAM空間可以所用,對棧空間的分配與使用要非常小心。為減少應用程序需要的RAM空間,對每個任務棧空間的使用都要非常小心,特別要注意以下幾點:


?定義函數和中斷服務子程序中的局部變量,特別是定義大型數組和數據結構

?函數(即子程序)的嵌套

?中斷嵌套

?庫函數需要的棧空間

?多變元的函數調用


綜上所述,多任務系統比前后臺系統需要更多的代碼空間(ROM)和數據空間(RAM)。額外的代碼空間取決于內核的大小,而RAM的用量取決于系統中的任務數。

3.
使用實時內核的優缺點

實時內核也稱為實時操作系統或RTOS。它的使用使得實時應用程序的設計和擴展變得容易,不需要大的改動就可以增加新的功能。通過將應用程序分割成若干獨立的任務,RTOS使得應用程序的設計過程大為減化。使用可剝奪性內核時,所有時間要求苛刻的事件都得到了盡可能快捷、有效的處理。通過有效的服務,如信號量、郵箱、隊列、延時、超時等,RTOS使得資源得到更好的利用。

如果應用項目對額外的需求可以承受,應該考慮使用實時內核。這些額外的需求是:內核的價格,額外的ROM/RAM開銷,2到4百分點的CPU額外負荷。

還沒有提到的一個因素是使用實時內核增加的價格成本。在一些應用中,價格就是一切,以至于對使用RTOS連想都不敢想。

當今有80個以上的RTOS商家,生產面向8位、16位、32位、甚至是64位的微處理器的RTOS產品。一些軟件包是完整的操作系統,不僅包括實時內核,還包括輸入輸出管理、視窗系統(用于顯示)、文件系統、網絡、語言接口庫、調試軟件、交叉平臺編譯(Cross-Platform compilers)。RTOS的價格從70美元到30,000美元。RTOS制造商還可能索取每個目標系統的版權使用費。就像從RTOS商家那買一個芯片安裝到每一個產品上,然后一同出售。RTOS商家稱之為硅片軟件(Silicon Software)。每個產品的版權費從5美元到250美元不等。同如今的其它軟件包一樣,還得考慮軟件維護費,這部分開銷為每年還得花100到5,000美元!

3.
實時系統小結

三種類型的實時系統歸納于表2.2中,這三種實時系統是:前后臺系統,不可剝奪型內核和可剝奪型內核。



. 表2.2   實時系統小結



Foreground/
Background

Non-Preemptive Kernel

Preemptive Kernel

Interrupt latency (Time)

MAX(Longest instruction,
    User int. disable)
+ Vector to ISR

MAX(Longest instruction,
    User int. disable,
    Kernel int. disable)
+ Vector to ISR

MAX(Longest instruction,
    User int. disable,
    Kernel int. disable)
+ Vector to ISR

Interrupt response (Time)

Int. latency
+ Save CPU’s context

Int. latency
+ Save CPU’s context

Interrupt latency
+ Save CPU’s context
+ Kernel ISR entry function

Interrupt recovery (Time)

Restore background’s
    context
+ Return from int.

Restore task’s context
+ Return from int.

Find highest priority task
+ Restore highest priority
    task’s context
+ Return from interrupt

Task response (Time)

Background

Longest task
+ Find highest priority task
+ Context switch

Find highest priority task
+ Context switch

ROM size

Application code

Application code
+ Kernel code

Application code
+ Kernel code

RAM size

Application code

Application code
+ Kernel RAM
+ SUM(Task stacks
  + MAX(ISR stack))

Application code
+ Kernel RAM
+ SUM(Task stacks
  + MAX(ISR stack))

Services available?

Application code must
    provide

Yes

Yes