隔離編譯器
1. 什麼是PSOS
pSOS系統結構
pSOS是一個由標准軟組件組成的,可剪裁的實時操作系統。其系統結構如圖2.1所示
,它分為內核層、系統服務層、用戶層。
1. 內核層
pSOS內核負責任務的管理與調度、任務間通信、內存管理、實時時鍾管理、中斷服
務;可以動態生成或刪除任務、內存區、消息隊列、信號燈等系統對象;實現了基於優
先級的、選擇可搶占的任務調度演算法,並提供了可選的時間片輪轉調度。pSOS Kernel還
提供了任務建間通信機制及同步、互斥手段,如消息、信號燈、事件、非同步信號等。
pSOS操作系統在Kernel層中將與具體硬體有關的操作放在一個模塊中,對系統服務層
以上屏蔽了具體的硬體特性,從而使得pSOS很方便地從支持Intel 80x86系列轉到支持MC
68XXX系列,並且在系統服務層上對不同應用系統不同用戶提供標準的軟組件如PNA+、
PHILE+等。
2. 系統服務層
pSOS系統服務層包括PNA+、PRPC+、PHILE+等組件。PNA+實現了完整的基於流的TCP
/IP協議集,並具有良好的實時性能,網路組件內中斷屏蔽時間不大於內核模塊中斷屏蔽時
間。PRPC+提供了遠程調用庫,支持用戶建立一個分布式應用系統。PHILE+提供了文件系
統管理和對塊存儲設備的管理。PREPC+提供了標準的C、C++庫,支持用戶使用C、C++語言
編寫應用程序。
由於pSOS內核屏蔽了具體的硬體特性,因此,pSOS系統服務層的軟組件是標準的、與
硬體無關的。這意味著pSOS各種版本,無論是對80X86系列還是MC68XXX系列,其系統服務
層各組件是標準的、同一的,這減少了軟體維護工作,增強了軟體可移植性。
每個軟組件都包含一系列的系統調用。對用戶而言,這些系統調用就象一個個可重入
的C函數,然而它們卻是用戶進入pSOS內核的唯一手段。
3. 用戶層
用戶指的是用戶編寫的應用程序,它們是以任務的形式出現的。任務通過發系統調
用而進入pSOS內核,並為pSOS內核所管理和調度。
pSOS為用戶還提供了一個集成式的開發環境(IDE)。pSOS_IDE可駐留於UNIX或DOS
環境下,它包括C和C++優化編譯器、CPU和pSOS模擬模擬和DEBUG功能。
pSOS內核機制
§3.1 幾個基本概念
3.1.1 任務
在實時操作系統中,任務是參與資源競爭(如CPU、Memory、I/O devices等)
的基本單位。pSOS為每個任務構造了一個虛擬的、隔離的環境,從而在概念上,一個任務
與另一個任務之間可以相互並行、獨立地執行。任務與任務之間的切換、任務之間的通
信都是通過發系統調用(在有些情況下是通過ISR)進入pSOS Kernel,由pSOS Kernel完
成的。
pSOS系統中任務包括系統任務和用戶任務兩類。關於用戶任務的劃分並沒有一個固
定的法則,但很明顯,劃分太多將導致任務間的切換過於頻繁,系統開銷太大,劃分太少又
會導致實時性和並行性下降,從而影響系統的效率。一般說來,功能模塊A與功能模塊B是
分開為兩個任務還是合為一個任務可以從是否具有時間相關性、優先性、邏輯特性和功
能耦合等幾個方面考慮。
3.1.2 優先順序
每個任務都有一個優先順序。pSOS系統支持0~255級優先順序,0級最低,255級最高。0級
專為IDLE任務所有,240~255級為系統所用。在運行時,任務(包括系統任務)的優先順序
可以通過t_setpri系統調用改變。
3.1.3 任務狀態
pSOS下任務具有三種可能狀態並處於這三個狀態之一。只有通過任務本身或其他任
務、ISR對pSOS內核所作的系統調用才能改變任務狀態。從宏觀角度看,一個多任務應用
通過一系列到pSOS的系統調用迫使pSOS內核改變受影響任務而從運行一個任務到運行另
一任務向前發展的。
對於pSOS kernel,任務在創建前或被刪除後是不存在的。被創建的任務在能夠運行
前必須被啟動。一旦啟動後,一個任務通常處於下面三個狀態之一:
①Executing (Ready)就緒
②Running運行
③Blocked阻塞
就緒任務是未被阻塞可運行的,只等待高優先順序任務釋放CPU的任務。由於一個任務
只能由正運行的任務通過調用來被啟動,而且任何時刻只能有一個正在運行的任務,所
以新任務總是從就緒態開始。
運行態任務是正在使用CPU的就緒任務, 系統只能有一個running任務。一般runni
ng任務是所有就緒任務中優先順序最高的,但也有例外。
任務是由自身特定活動而變為阻塞的,通常是系統調用引起調用任務進入等待狀態
的。所以任務不可能從ready態到blocked態,因為只有運行任務才能執行系統調用。
3.1.4 任務控制塊
任務控制塊TCB是pSOS內核建立並維護的一個系統數據結構,它包含了pSOS Kernel調
度與管理任務所需的一切信息,如任務名、優先順序、剩餘時間片數、當前寄存器狀態等。
在有的RTOS中,任務的狀態與任務TCB所處的隊列是等同的。pSOS操作系統將二者分
為兩個概念,例如任務處於阻塞狀態,但它的TCB卻處於消息等待隊列、信號燈等待隊列、
內存等待隊列、超時隊列之一。
pSOS啟動時,將根據Configuration Table中的參數kc_ntask建立一個包含kc_ntask
個TCB塊的TCB池,它表示最大並行任務數。在創建一個任務時,分配一個TCB給該任務,在
撤銷一個任務時,該TCB將被收回。
3.1.5 對象、對象名及ID號
pSOS Kernel是一個面向對象的操作系統內核,pSOS系統中對象包括任務、memory
regions、memory partitions、消息隊列和信號燈。
對象名由用戶定義(4位ASCII字元),並且在該對象創建時作為系統調用obj_CREAT
E
的一個人口參數傳給pSOS Kernel。pSOS Kernel反過來賦予該對象一個唯一的32位ID號
。除obj_CREATE和obj_IDENT外,所有涉及對象的系統調用都要用到對象ID號。
創建對象的任務通過obj_CREATE就已經知道了該對象的ID號,其餘任務可通過obj_
IDENT或通過全局變數(如果已經為該任務的ID號建立了一個全局變數的話)獲取該對象
的ID號。對象ID號隱含了該對象控制塊(如TCB、QCB)的位置信息,這一位置信息被pSO
S
Kernel用於對該對象的管理和操作,如掛起/解掛一個任務、刪除一個消息隊列等。
3.1.6 任務模式字Mode word.
每個任務帶有一個mode word,用來改變調度決策或執行環境。主要有以下四個參
數
Preemption Enabled/Disabled.
Roundrobin Enabled/Disabled
Interupts Enabled/Disabled.
ASR Enabled/Disabled: 每個任務有一個通過as-catoh建立起來的非同步信號服務例
程ASR。非同步信號類似於軟體中斷。當ASR位為1時as-catch所指向的任務將會被改變執行
路徑,先執行ASR,再返回原執行點。
§3.2 任務調度
3.2.1 影響動態調度效果的兩個因素
pSOS採用優先順序+時間片的調度方式。有兩個因素將影響動態調度的效果:一是優先
級可變(通過t_setpri系統調用改變任務的優先順序);二是任務模式字中的preemption
bit位和roundrobin bit位。preemption bit位決定不同優先順序的任務是否可搶占,並和
roundrobin bit位一起決定任務的時間片輪轉是否有效。
3.2.2 引起任務調度的原因及結果
pSOS系統中引起調度的原因有兩條:
1. 在輪轉方式下時間片到
2. pSOS系統調用引發任務調度。該系統調用可能是ISR發出的,也可能是某個任務發出的
。
pSOS任務調度的結果有兩種:
1. 引起運行任務切換,這指的是
2. 不引起運行任務切換,這指的是
不論任務調度是否引發運行任務切換,都有可能引起一個或多個任務狀態變遷。
3.2.3 運行任務的切換
一、何時切換
下面三種情況將引發運行任務切換:
1. 在時間片輪轉方式下(此時任務模式字的roundrobin bit與preemption bit均為
enable),運行任務Task A的時間片用完,且Ready隊列中有相同優先順序的其它任務,則
Task A退出運行。
2. 在運行任務Task A的Mode word的preemption bit位為enable的前提下,若Task A發出
的某條相同調用引發一個優先順序高於Task A的任務Task B從Block狀態進入Reary狀態,則
將Task B投入運行。
3. ISR使用I_RETURN系統調用,則ISR退出運行,pSOS Kernel選擇Ready隊列中優先順序最高
的任務投入運行(這一任務並不一定是被ISR打斷的前運行任務)。
二、如何切換
上述三類運行任務的切換,其具體的pSOS Kernel運作過程並非完全一樣,但彼此之間
差別不大。為了簡單起見,我們以
為例對切換過程作一簡單敘述。這一過程可細分為4個步驟:
1. 任務A運行信息保存(_t_save proc far)
這一過程主要完成修改系統工作標志,保存切換點地址及運行信息、任務A棧調
整
棧
指針保存、棧切換、參數及返址入棧等一系列工作。
2.任務A入就緒隊列(void t_in_chain)
這一過程將任務A的TCB塊按優先順序順序插入就緒隊列。
3.選擇一個高優先順序任務B(void t_choice( ))
按一定演算法從就緒隊列中選出最高優先順序任務B的TCB塊,並使運行指針指向它。
4.將任務B投入運行(_t_run proc far)
從系統棧切換到任務B棧,用任務B的TCB塊中保存的信息恢復上次運行被打斷的
地
,恢
復任務運行環境,於是任務B開始繼續運行。
圖3.1反映了典型任務切換過程中CPU控制權的轉移、各堆棧活動生命期、任務活動
生命期等信息。圖中
t1,t4為切換點 t2,t3為開/關中斷
Tsch=t4-t1 // Tsch為任務切換時間
Tforbid=t3-t2 // Tforbid為中斷禁止時間
它們是實時操作系統最重要的兩個性能指標。
2. 在C++ 程序中調用被 C 編譯器編譯後的函數,為什麼要加 extern 「C」聲明
因為c++編譯時會進行名變化,而C不會,導致無法找到函數等。
要禁止名變換,使用C++的extern 'C'指示。
詳見
http://dev.csdn.net/article/13/13133.shtm
Item M34:如何在同一程序中混合使用C++和C
許多年來,你一直擔心編製程序時一部分使用C++一部分使用C,就如同在全部用C編程的年代同時使用多個編譯器來生成程序一樣。沒辦法多編譯器編程的,除非不同的編譯器在與實現相關的特性(如int和double的位元組大小,傳參方式)上相同。但這個問題在語言的標准化中被忽略了,所以唯一的辦法就是兩個編譯器的生產商承諾它們間兼容。C++和C混合編程時同樣是這個問題,所以在實體混合編程前,確保你的C++編譯器和C編譯器兼容。
確認兼容後,還有四個要考慮的問題:名變換,靜態初始化,內存動態分配,數據結構兼容。
* 名變換
名變換,就是C++編譯器給程序的每個函數換一個獨一無二的名字。在C中,這個過程是不需要的,因為沒有函數重載,但幾乎所有C++程序都有函數重名(例如,流運行庫就申明了幾個版本的operator<<和operator>>)。重載不兼容於絕大部分鏈接程序,因為鏈接程序通常無法分辨同名的函數。名變換是對鏈接程序的妥協;鏈接程序通常堅持函數名必須獨一無二。
如果只在C++范圍內,名變換不會影響你。如果你你有一個函數叫drawline而編譯器將它變換為xyzzy,你總使用名字drawLine,不會注意到背後的obj文件引用的是xyzzy的。
如果drawLine位於C運行庫中,那就是一個不同的故事了。你的C++源文件包含的頭文件中申明為:
void drawLine(int x1, int y1, int x2, int y2);
代碼體中通常也是調用drawLine。每個這樣的調用都被編譯器轉換為調用名變換後的函數,所以寫下的是
drawLine(a, b, c, d); // call to unmangled function name
obj文件中調用的是:
xyzzy(a, b, c, d); // call to mangled function mame
但如果drawLine是一個C函數,obj文件(或者是動態鏈接庫之類的文件)中包含的編譯後的drawLine函數仍然叫drawLine;沒有名變換動作。當你試圖將obj文件鏈接為程序時,將得到一個錯誤,因為鏈接程序在尋找一個叫xyzzy的函數,而沒有這樣的函數存在。
要解決這個問題,你需要一種方法來告訴C++編譯器不要在這個函數上進行名變換。你不期望對用其它語言寫的函數進行名變換,如C、匯編、Fortran、LISP、Forth或其它。(是的,這「其它」中應該包括COBOL,但那時你將得到什麼?(Yes, what-have-you would include COBOL, but then what would you have? ))總之,如果你調用一個名字為drawLine的C函數,它實際上就叫drawLine,你的obj文件應該包含這樣的一個引用,而不是引用進行了名變換的版本。
要禁止名變換,使用C++的extern 'C'指示:
// declare a function called drawLine; don't mangle
// its name
extern "C"
void drawLine(int x1, int y1, int x2, int y2);
不要以為有一個extern 'C',那麼就應該同樣有一個extern 'Pascal'和extern 'FORTRAN'。沒有,至少在C++標准中沒有。不要將extern 'C'看作是申明這個函數是用C語言寫的,應該看作是申明在個函數應該被當作好象C寫的一樣而進行調用。(使用術語就是,extern 'C'意思是這個函數有C鏈接,但這個意思表達實在不怎麼清晰。不管如何,它總意味著一件事:名變換被禁止了。)
例如,如果不幸到必須要用匯編寫一個函數,你也可以申明它為extern 'C':
// this function is in assembler - don't mangle its name
extern "C" void twiddleBits(unsigned char bits);
你甚至可以在C++函數上申明extern 'C'。這在你用C++寫一個庫給使用其它語言的客戶使用時有用。通過禁止這些C++函數的名變換,你的客戶可以使用你選擇的自然而直觀的名字,而不用使用你的編譯生成的變換後的名字:
// the following C++ function is designed for use outside
// C++ and should not have its name mangled
extern "C" void simulate(int iterations);
經常,你有一堆函數不想進行名變換,為每一個函數添加extern 'C'是痛苦的。幸好,這沒必要。extern 'C'可以對一組函數生效,只要將它們放入一對大括弧中:
extern "C" { // disable name mangling for
// all the following functions
void drawLine(int x1, int y1, int x2, int y2);
void twiddleBits(unsigned char bits);
void simulate(int iterations);
...
}
這樣使用extern 'C'簡化了維護那些必須同時供C++和C使用的頭文件的工作。當用C++編譯時,你應該加extern 'C',但用C編譯時,不應該這樣。通過只在C++編譯器下定義的宏__cplusplus,你可以將頭文件組織得這樣:
#ifdef __cplusplus
extern "C" {
#endif
void drawLine(int x1, int y1, int x2, int y2);
void twiddleBits(unsigned char bits);
void simulate(int iterations);
...
#ifdef __cplusplus
}
#endif
順便提一下,沒有標準的名變換規則。不同的編譯器可以隨意使用不同的變換方式,而事實上不同的編譯器也是這么做的。這是一件好事。如果所有的編譯器使用同樣的變換規則,你會誤認為它們生成的代碼是兼容的。現在,如果混合鏈接來自於不同編譯器的obj文件,極可能得到應該鏈接錯誤,因為變換後的名字不匹配。這個錯誤暗示了,你可能還有其它兼容性問題,早些找到它比以後找到要好。
* 靜態初始化
在掌握了名變換後,你需要面對一個C++中事實:在main執行前和執行後都有大量代碼被執行。尤其是,靜態的類對象和定義在全局的、命名空間中的或文件體中的類對象的構造函數通常在main被執行前就被調用。這個過程稱為靜態初始化(參見Item E47)。這和我們對C++和C程序的通常認識相反,我們一直把main當作程序的入口。同樣,通過靜態初始化產生的對象也要在靜態析構過程中調用其析構函數;這個過程通常發生在main結束運行之後。
為了解決main()應該首先被調用,而對象又需要在main()執行前被構造的兩難問題,許多編譯器在main()的最開始處插入了一個特別的函數,由它來負責靜態初始化。同樣地,編譯器在main()結束處插入了一個函數來析構靜態對象。產生的代碼通常看起來象這樣:
int main(int argc, char *argv[])
{
performStaticInitialization(); // generated by the
// implementation
the statements you put in main go here;
performStaticDestruction(); // generated by the
// implementation
}
不要注重於這些名字。函數performStaticInitialization()和performStaticDestruction()通常是更含糊的名字,甚至是內聯函數(這時在你的obj文件中將找不到這些函數)。要點是:如果一個C++編譯器採用這種方法來初始化和析構靜態對象,除非main()是用C++寫的,這些對象將從沒被初始化和析構。因為這種初始化和析構靜態對象的方法是如此通用,只要程序的任意部分是C++寫的,你就應該用C++寫main()函數。
有時看起來用C寫main()更有意義--比如程序的大部分是C的,C++部分只是一個支持庫。然而,這個C++庫很可能含有靜態對象(即使現在沒有,以後可能會有--參見Item M32),所以用C++寫main()仍然是個好主意。這並不意味著你需要重寫你的C代碼。只要將C寫的main()改名為realMain(),然後用C++版本的main()調用realMain():
extern "C" // implement this
int realMain(int argc, char *argv[]); // function in C
int main(int argc, char *argv[]) // write this in C++
{
return realMain(argc, argv);
}
這么做時,最好加上注釋來解釋原因。
如果不能用C++寫main(),你就有麻煩了,因為沒有其它辦法確保靜態對象的構造和析構函數被調用了。不是說沒救了,只是處理起來比較麻煩一些。編譯器生產商們知道這個問題,幾乎全都提供了一個額外的體系來啟動靜態初始化和靜態析構的過程。要知道你的編譯器是怎麼實現的,挖掘它的隨機文檔或聯系生產商。
* 動態內存分配
現在提到動態內存分配。通行規則很簡單:C++部分使用new和delete(參見Item M8),C部分使用malloc(或其變形)和free。只要new分配的內存使用delete釋放,malloc分配的內存用free釋放,那麼就沒問題。用free釋放new分配的內存或用delete釋放malloc分配的內存,其行為沒有定義。那麼,唯一要記住的就是:將你的new和delete與mallco和free進行嚴格的隔離。
說比做容易。看一下這個粗糙(但很方便)的strp函數,它並不在C和C++標准(運行庫)中,卻很常見:
char * strp(const char *ps); // return a of the
// string pointed to by ps
要想沒有內存泄漏,strp的調用著必須釋放在strp()中分配的內存。但這內存這么釋放?用delete?用free?如果你調用的strp來自於C函數庫中,那麼是後者。如果它是用C++寫的,那麼恐怕是前者。在調用strp後所需要做的操作,在不同的操作系統下不同,在不同的編譯器下也不同。要減少這種可移植性問題,盡可能避免調用那些既不在標准運行庫中(參見Item E49和Item M35)也沒有固定形式(在大多數計算機平台下)的函數。
* 數據結構的兼容性
最後一個問題是在C++和C之間傳遞數據。不可能讓C的函數了解C++的特性的,它們的交互必須限定在C可表示的概念上。因此,很清楚,沒有可移植的方法來傳遞對象或傳遞指向成員函數的指針給C寫的函數。但是,C了解普通指針,所以想讓你的C++和C編譯器生產兼容的輸出,兩種語言間的函數可以安全地交換指向對象的指針和指向非成員的函數或靜態成員函數的指針。自然地,結構和內建類型(如int、char等)的變數也可自由通過。
因為C++中的struct的規則兼容了C中的規則,假設「在兩類編譯器下定義的同一結構將按同樣的方式進行處理」是安全的。這樣的結構可以在C++和C見安全地來回傳遞。如果你在C++版本中增加了非虛函數,其內存結構沒有改變,所以,只有非虛函數的結構(或類)的對象兼容於它們在C中的孿生版本(其定義只是去掉了這些成員函數的申明)。增加虛函數將結束游戲,因為其對象將使用一個不同的內存結構(參見Item M24)。從其它結構(或類)進行繼承的結構,通常也改變其內存結構,所以有基類的結構也不能與C函數交互。
就數據結構而言,結論是:在C++和C之間這樣相互傳遞數據結構是安全的--在C++和C下提供同樣的定義來進行編譯。在C++版本中增加非虛成員函數或許不影響兼容性,但幾乎其它的改變都將影響兼容。
* 總結
如果想在同一程序下混合C++與C編程,記住下面的指導原則:
* 確保C++和C編譯器產生兼容的obj文件。
* 將在兩種語言下都使用的函數申明為extern 'C'。
* 只要可能,用C++寫main()。
* 總用delete釋放new分配的內存;總用free釋放malloc分配的內存。
* 將在兩種語言間傳遞的東西限制在用C編譯的數據結構的范圍內;這些結構的C++版本可以包含非虛成員函數。
