本系列文章中的前兩部分,我們探討管道及信號兩種通信機制,本文將深入第三部分,介紹系統 V 消息隊列及其相應 API。
消息隊列(也叫做報文隊列)能夠克服早期unix通信機制的一些缺點。作為早期unix通信機制之一的信號能夠傳送的信息量有限,后來雖然 POSIX 1003.1b在信號的實時性方面作了拓廣,使得信號在傳遞信息量方面有了相當程度的改進,但是信號這種通信方式更像"即時"的通信方式,它要求接受信號 的進程在某個時間范圍內對信號做出反應,因此該信號最多在接受信號進程的生命周期內才有意義,信號所傳遞的信息是接近于隨進程持續的概念 (process-persistent),見附錄 1;管道及有名管道及有名管道則是典型的隨進程持續IPC,并且,只能傳送無格式的字節流無疑會給應用程序開發帶來不便,另外,它的緩沖區大小也受到限制。
消息隊列就是一個消息的鏈表。可以把消息看作一個記錄,具有特定的格式以及特定的優先級。對消息隊列有寫權限的進程可以向中按照一定的規則添加新消息;對消息隊列有讀權限的進程則可以從消息隊列中讀走消息。消息隊列是隨內核持續的(參見附錄 1)。
目前主要有兩種類型的消息隊列:POSIX消息隊列以及系統V消息隊列,系統V消息隊列目前被大量使用。考慮到程序的可移植性,新開發的應用程序應盡量使用POSIX消息隊列。
在本系列專題的序(深刻理解Linux進程間通信(IPC))中,提到對于消息隊列、信號燈、以及共享內存區來說,有兩個實現版本:POSIX的以 及系統V的。Linux內核(內核2.4.18)支持POSIX信號燈、POSIX共享內存區以及POSIX消息隊列,但對于主流Linux發行版本之一 redhad8.0(內核2.4.18),還沒有提供對POSIX進程間通信API的支持,不過應該只是時間上的事。
因此,本文將主要介紹系統V消息隊列及其相應API。在沒有聲明的情況下,以下討論中指的都是系統V消息隊列。
一、消息隊列基本概念
從上圖可以看出,全局數據結構 struct ipc_ids msg_ids 可以訪問到每個消息隊列頭的第一個成員:struct kern_ipc_perm;而每個struct kern_ipc_perm能夠與具體的消息隊列對應起來是因為在該結構中,有一個key_t類型成員key,而key則唯一確定一個消息隊列。 kern_ipc_perm結構如下:
|
二、操作消息隊列
對消息隊列的操作無非有下面三種類型:
1、 打開或創建消息隊列
消息隊列的內核持續性要求每個消息隊列都在系統范圍內對應唯一的鍵值,所以,要獲得一個消息隊列的描述字,只需提供該消息隊列的鍵值即可;
注:消息隊列描述字是由在系統范圍內唯一的鍵值生成的,而鍵值可以看作對應系統內的一條路經。
2、 讀寫操作
消息讀寫操作非常簡單,對開發人員來說,每個消息都類似如下的數據結構:
|
mtype成員代表消息類型,從消息隊列中讀取消息的一個重要依據就是消息的類型;mtext是消息內容,當然長度不一定為1。因此,對于發送消息 來說,首先預置一個msgbuf緩沖區并寫入消息類型和內容,調用相應的發送函數即可;對讀取消息來說,首先分配這樣一個msgbuf緩沖區,然后把消息 讀入該緩沖區即可。
3、 獲得或設置消息隊列屬性:
消息隊列的信息基本上都保存在消息隊列頭中,因此,可以分配一個類似于消息隊列頭的結構(struct msqid_ds,見附錄 2),來返回消息隊列的屬性;同樣可以設置該數據結構。
消息隊列API
1、文件名到鍵值
|
它返回與路徑pathname相對應的一個鍵值。該函數不直接對消息隊列操作,但在調用ipc(MSGGET,…)或msgget()來獲得消息隊列描述字前,往往要調用該函數。典型的調用代碼是:
|
2、linux為操作系統V進程間通信的三種方式(消息隊列、信號燈、共享內存區)提供了一個統一的用戶界面:int ipc(unsigned int call, int first, int second, int third, void *ptr, long fifth);
第一個參數指明對IPC對象的操作方式,對消息隊列而言共有四種操作:MSGSND、MSGRCV、MSGGET以及MSGCTL,分別代表向消息 隊列發送消息、從消息隊列讀取消息、打開或創建消息隊列、控制消息隊列;first參數代表唯一的IPC對象;下面將介紹四種操作。
注:本人不主張采用系統調用ipc(),而更傾向于采用系統V或者POSIX進程間通信API。原因如下:
3.系統V消息隊列API
系統V消息隊列API共有四個,使用時需要包括幾個頭文件:
|
1)int msgget(key_t key, int msgflg)
參數key是一個鍵值,由ftok獲得;msgflg參數是一些標志位。該調用返回與健值key相對應的消息隊列描述字。
在以下兩種情況下,該調用將創建一個新的消息隊列:
參數msgflg可以為以下:IPC_CREAT、IPC_EXCL、IPC_NOWAIT或三者的或結果。
調用返回:成功返回消息隊列描述字,否則返回-1。
注:參數key設置成常數IPC_PRIVATE并不意味著其他進程不能訪問該消息隊列,只意味著即將創建新的消息隊列。
2)int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg);
該系統調用從msgid代表的消息隊列中讀取一個消息,并把消息存儲在msgp指向的msgbuf結構中。
msqid為消息隊列描述字;消息返回后存儲在msgp指向的地址,msgsz指定msgbuf的mtext成員的長度(即消息內容的長度),msgtyp為請求讀取的消息類型;讀消息標志msgflg可以為以下幾個常值的或:
msgrcv手冊中詳細給出了消息類型取不同值時(>0; <0; =0),調用將返回消息隊列中的哪個消息。
msgrcv()解除阻塞的條件有三個:
調用返回:成功返回讀出消息的實際字節數,否則返回-1。
3)int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg);
向msgid代表的消息隊列發送一個消息,即將發送的消息存儲在msgp指向的msgbuf結構中,消息的大小由msgze指定。
對發送消息來說,有意義的msgflg標志為IPC_NOWAIT,指明在消息隊列沒有足夠空間容納要發送的消息時,msgsnd是否等待。造成msgsnd()等待的條件有兩種:
調用返回:成功返回0,否則返回-1。
4)int msgctl(int msqid, int cmd, struct msqid_ds *buf);
該系統調用對由msqid標識的消息隊列執行cmd操作,共有三種cmd操作:IPC_STAT、IPC_SET 、IPC_RMID。
調用返回:成功返回0,否則返回-1。
三、消息隊列的限制
每個消息隊列的容量(所能容納的字節數)都有限制,該值因系統不同而不同。在后面的應用實例中,輸出了redhat 8.0的限制,結果參見附錄 3。
另一個限制是每個消息隊列所能容納的最大消息數:在redhad 8.0中,該限制是受消息隊列容量制約的:消息個數要小于消息隊列的容量(字節數)。
注:上述兩個限制是針對每個消息隊列而言的,系統對消息隊列的限制還有系統范圍內的最大消息隊列個數,以及整個系統范圍內的最大消息數。一般來說,實際開發過程中不會超過這個限制。
四、消息隊列應用實例
消息隊列應用相對較簡單,下面實例基本上覆蓋了對消息隊列的所有操作,同時,程序輸出結果有助于加深對前面所講的某些規則及消息隊列限制的理解。
|
小結:
消息隊列
與管道以及有名管道相比,具有更大的靈活性,首先,它提供有格式字節流,有利于減少開發人員的工作量;其次,消息具有類型,在實際應用中,可作為優先級使
用。這兩點是管道以及有名管道所不能比的。同樣,消息隊列可以在幾個進程間復用,而不管這幾個進程是否具有親緣關系,這一點與有名管道很相似;但消息隊列
是隨內核持續的,與有名管道(隨進程持續)相比,生命力更強,應用空間更大。
附錄 1:在參考文獻[1]中,給出了IPC隨進程持續、隨內核持續以及隨文件系統持續的定義:
附錄 2:
結構msg_queue用來描述消息隊列頭,存在于系統空間:
|
結構msqid_ds用來設置或返回消息隊列的信息,存在于用戶空間;
|
附錄 3:消息隊列實例輸出結果:
|
參考文獻:
在
time_t time(time_t *timer);
double difftime(time_t time1,time_t time2);
struct tm *gmtime(const time_t *timer);
struct tm *localtime(const time_t *timer);
char *asctime(const struct tm *timeptr);
char *ctime(const time_t *timer);
size_t strftime(char *s,size_t maxsize,const char *format,const struct tm *timeptr);
time_t mktime(struct tm *timeptr);
clock_t clock(void);
下面是我從網上收集到的時間函數集
TinyXML是一個簡單小巧,可以很容易集成到其它程序中的C++ XML解析器。
它能做些什么
簡單地說,TinyXML解析一個XML文檔并由此生成一個可讀可修改可保存的文檔對象模型(DOM)。
XML的意思是“可擴展標記語言“(eXtensible Markup Language)。它允許你創建你自己的文檔標記。在為瀏覽器標記文檔方面HTML做得很好,然而XML允許你定義任何文檔標記,比如可以為一個組織者 應用程序定義一個描述“to do”列表的文檔。 XML擁有一個結構化并且方便的格式,所有為存儲應用程序數據而創建的隨機文件格式都可以用XML代替,而這一切只需要一個解析器。
最全面正確的說明可以在http://www.w3.org/TR/2004/REC-xml-20040204/找到,但坦白地說,它很晦澀難懂。事實上我喜歡http://skew.org/xml/tutorial上關于XML的介紹。
有不同的方法可以訪問和與XML數據進行交互。TinyXML使用文檔對象模型(DOM),這意味著XML數據被解析成一個可被瀏覽和操作的C++ 對象,然后它可以被寫到磁盤或者另一個輸出流中。你也可以把C++對象構造成一個XML文檔然后把它寫到磁盤或者另一個輸出流中。
TinyXML被設計得容易快速上手。它只有兩個頭文件和四個cpp文件。只需要把它們簡單地加到你的項目中就行了。有一個例子文件——xmltest.cpp來引導你該怎么做。
TinyXML以Zlib許可來發布,所以你可以在開源或者商業軟件中使用它。許可證更具體的描述在每個源代碼文件的頂部可以找到。
TinyXML在保證正確和恰當的XML輸出的基礎上嘗試成為一個靈活的解析器。TinyXML可以在任何合理的C++適用系統上編譯。它不依賴于 異常或者運行時類型信息,有沒有STL支持都可以編譯。TinyXML完全支持UTF-8編碼和前64k個字符實體(<i>譯注:如果你不明 白這句譯文,可能你需要了解一下Unicode編碼</i>)。
它無法做些什么
TinyXML不解析不使用DTDs(文檔類型定義)或者XSLs(可擴展樣式表語言)。有其它解析器(到www.sourceforge.org 搜索一下XML)具有更加全面的特性,但它們也就更大,需要花更長的時間來建立你的項目,有更陡的學習曲線,而且經常有一個更嚴格的許可協議。如果你是用 于瀏覽器或者有更復雜的XML需要,那么TinyXML不適合你。
下面的DTD語法在TinyXML里是不做解析的:
<!DOCTYPE Archiv [
<!ELEMENT Comment (#PCDATA)>
]>
因為TinyXML把它看成是一個帶著非法嵌入!ELEMENT結點的!DOCTYPE結點。或許這在將來會得到支持。
指南
有耐性些,這是一份能很好地指導你怎么開始的指南,它(非常短小精悍)值得你花時間完整地讀上一遍。
代碼狀況
TinyXML是成熟且經過測試的代碼,非常健壯。如果你發現了漏洞,請提交漏洞報告到sourcefore網站上 (www.sourceforge.net/projects/tinyxml)。 我們會盡快修正。
有些地方可以讓你得到提高,如果你對TinyXML的工作感興趣的話可以上sourceforge查找一下。
相關項目
你也許會覺得TinyXML很有用!(簡介由項目提供)
特性
使用STL
TinyXML可以被編譯成使用或不使用STL。如果使用STL,TinyXML會使用std::string類,而且完全支持 std::istream,std::ostream,operator<<和operator>>。許多API方法都有 ‘const char*’和’const std::string&’兩個版本。
如果被編譯成不使用STL,則任何STL都不會被包含。所有string類都由TinyXML它自己實現。所有API方法都只提供’const char*’傳入參數。
使用運行時定義:
TIXML_USE_STL
來編譯成不同的版本。這可以作為參數傳給編譯器或者在“tinyxml.h”文件的第一行進行設置。
注意:如果在Linux上編譯測試代碼,設置環境變量TINYXML_USE_STL=YES/NO可以控制STL的編譯。而在Windows上, 項目文件提供了STL和非STL兩種目標文件。在你的項目中,在tinyxml.h的第一行添加"#define TIXML_USE_STL"應該是最簡單的。
UTF-8
TinyXML支持UTF-8,所以可以處理任何語言的XML文件,而且TinyXML也支持“legacy模式”——一種在支持UTF-8之前使用的編碼方式,可能最好的解釋是“擴展的ascii”。
正常情況下,TinyXML會檢測出正確的編碼并使用它,然而,通過設置頭文件中的TIXML_DEFAULT_ENCODING值,TinyXML可以被強制成總是使用某一種編碼。
除非以下情況發生,否則TinyXML會默認使用Legacy模式:
如果編碼設置錯誤或者檢測到錯誤會發生什么事呢?TinyXML會嘗試跳過這些看似不正確的編碼,你可能會得到一些奇怪的結果或者亂碼,你可以強制TinyXML使用正確的編碼模式。
通過使用LoadFile( TIXML_ENCODING_LEGACY )或者LoadFile( filename, TIXML_ENCODING_LEGACY ), 你可以強制TinyXML使用Legacy模式。你也可以通過設置TIXML_DEFAULT_ENCODING = TIXML_ENCODING_LEGACY來強制一直使用Legacy模式。同樣的,你也可以通過相同的方法來強制設置成 TIXML_ENCODING_UTF8。
對于使用英文XML的英語用戶來說,UTF-8跟low-ASCII是一樣的。你不需要知道UTF-8或者一點也不需要修改你的代碼。你可以把UTF-8當作是ASCII的超集。
UTF-8并不是一種雙字節格式,但它是一種標準的Unicode編碼!TinyXML當前不使用或者直接支持wchar,TCHAR,或者微軟的 _UNICODE。"Unicode"這個術語被普遍地認為指的是UTF-16(一種unicode的寬字節編碼)是不適當的,這是混淆的來源。
對于“high-ascii”語言來說——幾乎所有非英語語言,只要XML被編碼成UTF-8, TinyXML就能夠處理。說起來可能有點微妙,比較舊的程序和操作系統趨向于使用“默認”或者“傳統”的編碼方式。許多應用程序(和幾乎所有現在的應用 程序)都能夠輸出UTF-8,但是那些比較舊或者難處理的(或者干脆不能使用的)系統還是只能以默認編碼來輸出文本。
比如說,日本的系統傳統上使用SHIFT-JIS編碼,這種情況下TinyXML就無法讀取了。但是一個好的文本編輯器可以導入SHIFT-JIS的文本然后保存成UTF-8編碼格式的。
Skew.org link上關于轉換編碼的話題做得很好。
測試文件“utf8test.xml”包含了英文、西班牙文、俄文和簡體中文(希望它們都能夠被正確地轉化)。“utf8test.gif”文件是 從IE上截取的XML文件快照。請注意如果你的系統上沒有正確的字體(簡體中文或者俄文),那么即使你正確地解析了也看不到與GIF文件上一樣的輸出。同 時要注意在一個西方編碼的控制臺上(至少我的Windows機器是這樣),Print()或者printf()也無法正確地顯示這個文件,這不關 TinyXML的事——這只是操作系統的問題。TinyXML沒有丟掉或者損壞數據,只是控制臺無法顯示UTF-8而已。
實體
TinyXML認得預定義的特殊“字符實體”,即:
& &
< <
> >
" "
' ‘
這些在XML文檔讀取時都會被辨認出來,并會被轉化成等價的UTF-8字符。比如下面的XML文本:
Far & Away
從TiXmlText 對象查詢出來時會變成"Far & Away"這樣的值,而寫回XML流/文件時會以“&”的方式寫回。老版本的TinyXML“保留”了字符實體,而在新版本中它們會被轉化成字符串。
另外,所有字符都可以用它的Unicode編碼數字來指定, " "和" "都表示不可分的空格字符。
打印
TinyXML有幾種不同的方式來打印輸出,當然它們各有各的優缺點。
流
設置了TIXML_USE_STL,TinyXML就能支持C++流(operator <<,>>)和C(FILE*)流。但它們之間有些差異你需要知道:
C風格輸出:
生成具有很多空格的格式化過的輸出,這是為了盡可能讓人看得明白。它們非常快,而且能夠容忍XML文檔中的格式錯誤。例如一個XML文檔包含兩個根元素和兩個聲明仍然能被打印出來。
C風格輸入:
速度快,容錯性好。當你不需要C++流時就可以使用它。
C++風格輸出:
生成壓縮過的輸出,目的是為了便于網絡傳輸而不是為了可讀性。它可能有些慢(可能不會),這主要跟你系統上ostream類的實現有關。無法容忍格式錯誤的XML:此文檔只能包含一個根元素。另外根級別的元素無法以流形式輸出。
C++風格輸入:
從流中讀取XML使其可用于網絡傳輸。通過些小技巧,它知道當XML文檔讀取完畢時,流后面的就一定是其它數據了。TinyXML總假定當它讀取到 根結點后XML數據就結束了。換句話說,那些具有不止一個根元素的文檔是無法被正確讀取的。另外還要注意由于STL的實現和TinyXML的限 制,operator>>會比Parse慢一些。
空格
對是保留還是壓縮空格這一問題人們還沒達成共識。舉個例子,假設‘_’代表一個空格,對于"Hello____world",HTML和某些XML 解析器會解釋成"Hello_world",它們壓縮掉了一些空格。而有些XML解析器卻不會這樣,它們會保留空格,于是就是 “Hello____world”(記住_表示一個空格)。其它的還建議__Hello___world__應該變成Hello___world 。
這是一個解決得不能讓我滿意的問題。TinyXML一開始就兩種方式都支持。調用TiXmlBase::SetCondenseWhiteSpace( bool )來設置你想要的結果,默認是壓縮掉多余的空格。
如果想要改變默認行為,你應該在解析任何XML數據之前調用TiXmlBase::SetCondenseWhiteSpace( bool ) ,而且我不建議設置之后再去改動它。
句柄
想要健壯地讀取一個XML文檔,檢查方法調用后的返回值是否為null是很重要的。一種安全的檢錯實現可能會產生像這樣的代碼:
TiXmlElement* root = document.FirstChildElement( "Document" );
if ( root )
{
TiXmlElement* element = root->FirstChildElement( "Element" );
if ( element )
{
TiXmlElement* child = element->FirstChildElement( "Child" );
if ( child )
{
TiXmlElement* child2 = child->NextSiblingElement( "Child" );
if ( child2 )
{
// Finally do something useful.
用句柄的話就不會這么冗長了,使用TiXmlHandle類,前面的代碼就會變成這樣:
TiXmlHandle docHandle( &document );
TiXmlElement* child2 = docHandle.FirstChild( "Document" ).FirstChild( "Element" ).Child( "Child", 1 ).ToElement();
if ( child2 )
{
// do something useful
這處理起來容易多了。 查閱TiXmlHandle可以得到更多的信息。
行列追蹤
對于某些應用程序來說,能夠追蹤節點和屬性在它們源文件中的原始位置是很重要的。另外,知道解析錯誤在源文件中的發生位置可以節省大量時間。
TinyXML能夠追蹤所有結點和屬性在文本文件中的行列原始位置。TiXmlBase::Row() 和 TiXmlBase::Column() 方法返回結點在源文件中的原始位置。正確的制表符號可以經由TiXmlDocument::SetTabSize() 來配置。
使用與安裝
編譯與運行xmltest:
提供了一個Linux Makefile和一個Windows Visual C++ .dsw 文件。只需要簡單地編譯和運行,它就會在你的磁盤上生成demotest.xml文件并在屏幕上輸出。它還嘗試用不同的方法遍歷DOM并打印出結點數。
那個Linux makefile很通用,可以運行在很多系統上——它目前已經在mingw和MacOSX上測試過。你不需要運行 ‘make depend’,因為那些依賴關系已經硬編碼在文件里了。
用于VC6的Windows項目文件
Makefile
在makefile的頂部你可以設置:
PROFILE,DEBUG,和TINYXML_USE_STL。makefile里有具體描述。
在tinyxml目錄輸入“make clean”然后“make”,就可以生成可執行的“xmltest”文件。
在某一應用程序中使用:
把tinyxml.cpp,tinyxml.h, tinyxmlerror.cpp, tinyxmlparser.cpp, tinystr.cpp, 和 tinystr.h 添加到你的項目和makefile中。就這么簡單,它可以在任何合理的C++適用系統上編譯。不需要為TinyXML打開異常或者運行時類型信息支持。
TinyXML怎么工作
舉個例子可能是最好的辦法,理解一下:
<?xml version="1.0" standalone=no>
<!– Our to do list data –>
<ToDo>
<Item priority="1"> Go to the <bold>Toy store!</bold></Item>
<Item priority="2"> Do bills</Item>
</ToDo>
它稱不上是一個To Do列表,但它已經足夠了。像下面這樣讀取并解析這個文件(叫“demo.xml”)你就能創建一個文檔:
TiXmlDocument doc( "demo.xml" );
doc.LoadFile();
現在它準備好了,讓我們看看其中的某些行和它們怎么與DOM聯系起來。
<?xml version="1.0" standalone=no>
第一行是一個聲明,它會轉化成TiXmlDeclaration 類,同時也是文檔結點的第一個子結點。
這是TinyXML唯一能夠解析的指令/特殊標簽。一般來說指令標簽會保存在TiXmlUnknown 以保證在它保存回磁盤時不會丟失這些命令。
<!– Our to do list data –>
這是一個注釋,會成為一個TiXmlComment對象。
<ToDo>
"ToDo"標簽定義了一個TiXmlElement 對象。它沒有任何屬性,但包含另外的兩個元素。
<Item priority="1">
生成另一個TiXmlElement對象,它是“ToDo”元素的子結點。此元素有一個名為“priority”和值為“1”的屬性。
Go to the
TiXmlText ,這是一個葉子結點,它不能再包含其它結點,是"Item" TiXmlElement的子結點。
<bold>
另一個TiXmlElement, 這也是“Item”元素的子結點。
等等
最后,看看整個對象樹:
TiXmlDocument "demo.xml"
TiXmlDeclaration "version=’1.0′" "standalone=no"
TiXmlComment " Our to do list data"
TiXmlElement "ToDo"
TiXmlElement "Item" Attribtutes: priority = 1
TiXmlText "Go to the "
TiXmlElement "bold"
TiXmlText "Toy store!"
TiXmlElement "Item" Attributes: priority=2
TiXmlText "Do bills"
文檔
本文檔由Doxygen使用‘dox’配置文件生成。
許可證
TinyXML基于zlib許可證來發布:
本軟件按“現狀”提供(即現在你看到的樣子),不做任何明確或隱晦的保證。由使用此軟件所引起的任何損失都決不可能由作者承擔。
只要遵循下面的限制,就允許任何人把這軟件用于任何目的,包括商業軟件,也允許修改它并自由地重新發布:
1. 決不能虛報軟件的來源;你決不能聲稱是你是軟件的第一作者。如果你在某個產品中使用了這個軟件,那么在產品文檔中加入一個致謝辭我們會很感激,但這并非必要。
2. 修改了源版本就應該清楚地標記出來,決不能虛報說這是原始軟件。
3. 本通告不能從源發布版本中移除或做修改。
參考書目
萬維網聯盟是定制XML的權威標準機構,它的網頁上有大量的信息。
權威指南:http://www.w3.org/TR/2004/REC-xml-20040204/
我還要推薦由OReilly出版由Robert Eckstein撰寫的"XML Pocket Reference"……這本書囊括了入門所需要的一切。
捐助者,聯系人,還有簡史
非常感謝給我們建議,漏洞報告,意見和鼓勵的所有人。它們很有用,并且使得這個項目變得有趣。特別感謝那些捐助者,是他們讓這個網站頁面生機勃勃。
有很多人發來漏洞報告和意見,與其在這里一一列出來不如我們試著把它們寫到“changes.txt”文件中加以贊揚。
TinyXML的原作者是Lee Thomason(文檔中還經常出現“我”這個詞) 。在Yves Berquin,Andrew Ellerton,和tinyXml社區的幫助下,Lee查閱修改和發布新版本。
我們會很感激你的建議,還有我們想知道你是否在使用TinyXML。希望你喜歡它并覺得它很有用。請郵寄問題,評論,漏洞報告給我們,或者你也可登錄網站與我們取得聯系:
www.sourceforge.net/projects/tinyxml
Lee Thomason, Yves Berquin, Andrew Ellerton
2) 結構體每個成員相對于結構體首地址的偏移量都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充字節;例如上面第二個結構體變量的地址空間。
3) 結構體的總大小為結構體最寬基本類型成員大小的整數倍,如有需要編譯器會在最末一個成員之后加上填充字節。例如上面第一個結構體變量。
現代計算機中內存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定類型變量的時候經常在特
定的內存地址訪問,這就需要各種類型數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。
對齊的作用和原因:各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。比如有些架構的CPU在訪問
一個沒有進行對齊的變量的時候會發生錯誤,那么在這種架構下編程必須保證字節對齊.其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺要求對
數據存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設為32位系統)如果存放在偶地址開始的地方,那
么一個讀周期就可以讀出這32bit,而如果存放在奇地址開始的地方,就需要2個讀周期,并對兩次讀出的結果的高低字節進行拼湊才能得到該32bit數
據。顯然在讀取效率上下降很多。
先讓我們看幾個例子吧(32bit,x86環境,gcc編譯器):
設結構體如下定義:
struct A
{
int a;
char b;
short c;
};
struct B
{
char b;
int a;
short c;
};
現在已知32位機器上各種數據類型的長度如下:
char:1(有符號無符號同)
short:2(有符號無符號同)
int:4(有符號無符號同)
long:4(有符號無符號同)
float:4 double:8
那么上面兩個結構大小如何呢?
結果是:
sizeof(strcut A)值為8
sizeof(struct B)的值卻是12
結構體A中包含了4字節長度的int一個,1字節長度的char一個和2字節長度的short型數據一個,B也一樣;按理說A,B大小應該都是7字節。
之所以出現上面的結果是因為編譯器要對數據成員在空間上進行對齊。上面是按照編譯器的默認設置進行對齊的結果,那么我們是不是可以改變編譯器的這種默認對齊設置呢,當然可以.例如:
#pragma pack (2) /*指定按2字節對齊*/
struct C
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定對齊,恢復缺省對齊*/
sizeof(struct C)值是8。
修改對齊值為1:
#pragma pack (1) /*指定按1字節對齊*/
struct D
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定對齊,恢復缺省對齊*/
sizeof(struct D)值為7。
后面我們再講解#pragma pack()的作用.
先讓我們看四個重要的基本概念:
1.數據類型自身的對齊值:
對于char型數據,其自身對齊值為1,對于short型為2,對于int,float,double類型,其自身對齊值為4,單位字節。
2.結構體或者類的自身對齊值:其成員中自身對齊值最大的那個值。
3.指定對齊值:#pragma pack (value)時的指定對齊值value。
4.數據成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中小的那個值。
有
了這些值,我們就可以很方便的來討論具體數據結構的成員和其自身的對齊方式。有效對齊值N是最終用來決定數據存放地址方式的值,最重要。有效對齊N,就是
表示“對齊在N上”,也就是說該數據的"存放起始地址%N=0".而數據結構中的數據變量都是按定義的先后順序來排放的。第一個數據變量的起始地址就是數
據結構的起始地址。結構體的成員變量要對齊排放,結構體本身也要根據自身的有效對齊值圓整(就是結構體成員變量占用總長度需要是對結構體有效對齊值的整數
倍,結合下面例子理解)。這樣就不能理解上面的幾個例子的值了。
例子分析:
分析例子B;
struct B
{
char b;
int a;
short c;
};
假
設B從地址空間0x0000開始排放。該例子中沒有定義指定對齊值,在筆者環境下,該值默認為4。第一個成員變量b的自身對齊值是1,比指定或者默認指定
對齊值4小,所以其有效對齊值為1,所以其存放地址0x0000符合0x0000%1=0.第二個成員變量a,其自身對齊值為4,所以有效對齊值也為4,
所以只能存放在起始地址為0x0004到0x0007這四個連續的字節空間中,復核0x0004%4=0,且緊靠第一個變量。第三個變量c,自身對齊值為
2,所以有效對齊值也是2,可以存放在0x0008到0x0009這兩個字節空間中,符合0x0008%2=0。所以從0x0000到0x0009存放的
都是B內容。再看數據結構B的自身對齊值為其變量中最大對齊值(這里是b)所以就是4,所以結構體的有效對齊值也是4。根據結構體圓整的要求,
0x0009到0x0000=10字節,(10+2)%4=0。所以0x0000A到0x000B也為結構體B所占用。故B從0x0000到0x000B
共有12個字節,sizeof(struct B)=12;其實如果就這一個就來說它已將滿足字節對齊了,
因為它的起始地址是0,因此肯定是對齊的,之所以在后面補充2個字節,是因為編譯器為了實現結構數組的存取效率,試想如果我們定義了一個結構B的數組,那
么第一個結構起始地址是0沒有問題,但是第二個結構呢?按照數組的定義,數組中所有元素都是緊挨著的,如果我們不把結構的大小補充為4的整數倍,那么下一
個結構的起始地址將是0x0000A,這顯然不能滿足結構的地址對齊了,因此我們要把結構補充成有效對齊大小的整數倍.其實諸如:對于char型數據,其
自身對齊值為1,對于short型為2,對于int,float,double類型,其自身對齊值為4,這些已有類型的自身對齊值也是基于數組考慮的,只
是因為這些類型的長度已知了,所以他們的自身對齊值也就已知了.
同理,分析上面例子C:
#pragma pack (2) /*指定按2字節對齊*/
struct C
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定對齊,恢復缺省對齊*/
第
一個變量b的自身對齊值為1,指定對齊值為2,所以,其有效對齊值為1,假設C從0x0000開始,那么b存放在0x0000,符合0x0000%1=
0;第二個變量,自身對齊值為4,指定對齊值為2,所以有效對齊值為2,所以順序存放在0x0002、0x0003、0x0004、0x0005四個連續
字節中,符合0x0002%2=0。第三個變量c的自身對齊值為2,所以有效對齊值為2,順序存放
在0x0006、0x0007中,符合
0x0006%2=0。所以從0x0000到0x00007共八字節存放的是C的變量。又C的自身對齊值為4,所以C的有效對齊值為2。又8%2=0,C
只占用0x0000到0x0007的八個字節。所以sizeof(struct C)=8.
1.在VC IDE中,可以這樣修改:[Project]|[Settings],c/c++選項卡Category的Code Generation選項的Struct Member Alignment中修改,默認是8字節。
2.在編碼時,可以這樣動態修改:#pragma pack .注意:是pragma而不是progma.
如果在編程的時候要考慮節約空間的話,那么我們只需要假定結構的首地址是0,然后各個變量按照上面的原則進行排列即可,基本的原則就是把結構中的變量按照
類型大小從小到大聲明,盡量減少中間的填補空間.還有一種就是為了以空間換取時間的效率,我們顯示的進行填補空間進行對齊,比如:有一種使用空間換時間做
法是顯式的插入reserved成員:
struct A{
char a;
char reserved[3];//使用空間換時間
int b;
}
reserved成員對我們的程序沒有什么意義,它只是起到填補空間以達到字節對齊的目的,當然即使不加這個成員通常編譯器也會給我們自動填補對齊,我們自己加上它只是起到顯式的提醒作用.
代碼中關于對齊的隱患,很多是隱式的。比如在強制類型轉換的時候。例如:
unsigned int i = 0x12345678;
unsigned char *p=NULL;
unsigned short *p1=NULL;
p=&i;
*p=0x00;
p1=(unsigned short *)(p+1);
*p1=0x0000;
最后兩句代碼,從奇數邊界去訪問unsignedshort型變量,顯然不符合對齊的規定。
在x86上,類似的操作只會影響效率,但是在MIPS或者sparc上,可能就是一個error,因為它們要求必須字節對齊.
如果出現對齊或者賦值問題首先查看
1. 編譯器的big little端設置
2. 看這種體系本身是否支持非對齊訪問
3. 如果支持看設置了對齊與否,如果沒有則看訪問時需要加某些特殊的修飾來標志其特殊訪問操作。
八.相關文章:轉自http://blog.csdn.net/goodluckyxl/archive/2005/10/17/506827.aspx
ARM下的對齊處理
from DUI0067D_ADS1_2_CompLib
3.13 type qulifiers
有部分摘自ARM編譯器文檔對齊部分
對齊的使用:
1.__align(num)
這個用于修改最高級別對象的字節邊界。在匯編中使用LDRD或者STRD時
就要用到此命令__align(8)進行修飾限制。來保證數據對象是相應對齊。
這個修飾對象的命令最大是8個字節限制,可以讓2字節的對象進行4字節
對齊,但是不能讓4字節的對象2字節對齊。
__align是存儲類修改,他只修飾最高級類型對象不能用于結構或者函數對象。
2.__packed
__packed是進行一字節對齊
1.不能對packed的對象進行對齊
2.所有對象的讀寫訪問都進行非對齊訪問
3.float及包含float的結構聯合及未用__packed的對象將不能字節對齊
4.__packed對局部整形變量無影響
5.強制由unpacked對象向packed對象轉化是未定義,整形指針可以合法定
義為packed。
__packed int* p; //__packed int 則沒有意義
6.對齊或非對齊讀寫訪問帶來問題
__packed struct STRUCT_TEST
{
char a;
int b;
char c;
} ; //定義如下結構此時b的起始地址一定是不對齊的
//在棧中訪問b可能有問題,因為棧上數據肯定是對齊訪問[from CL]
//將下面變量定義成全局靜態不在棧上
static char* p;
static struct STRUCT_TEST a;
void Main()
{
__packed int* q; //此時定義成__packed來修飾當前q指向為非對齊的數據地址下面的訪問則可以
p = (char*)&a;
q = (int*)(p+1);
*q = 0x87654321;
/*
得到賦值的匯編指令很清楚
ldr r5,0x20001590 ; = #0x12345678
[0xe1a00005] mov r0,r5
[0xeb0000b0] bl __rt_uwrite4 //在此處調用一個寫4byte的操作函數
[0xe5c10000] strb r0,[r1,#0] //函數進行4次strb操作然后返回保證了數據正確的訪問
[0xe1a02420] mov r2,r0,lsr #8
[0xe5c12001] strb r2,[r1,#1]
[0xe1a02820] mov r2,r0,lsr #16
[0xe5c12002] strb r2,[r1,#2]
[0xe1a02c20] mov r2,r0,lsr #24
[0xe5c12003] strb r2,[r1,#3]
[0xe1a0f00e] mov pc,r14
*/
/*
如果q沒有加__packed修飾則匯編出來指令是這樣直接會導致奇地址處訪問失敗
[0xe59f2018] ldr r2,0x20001594 ; = #0x87654321
[0xe5812000] str r2,[r1,#0]
*/
在APUE 14.7節對消息隊列的講解中,最后一段說“我們得出的結論是:在新的應用程式中不應當再使用他們。”
雖然在新的應用程式中不應該再使用消息隊列,我也沒有怎么使用過System V IPC總覺得在UNIX/Linux編程中少了什么,也許學習一下System V IPC對我的自信心會有相當大的幫助,從此我也敢講我知道如何使用IPC了。
先把各個函數原形列出。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflag);
int msgsnd(int msgid, struct msgbuf *msgp, size_t msgsz, int msgflag);
ssize_t msgrcv(int msgid, struct msgbuf *msgp, size_t msgsz, long msgtype, int msgflag);
int msgctl(int msgid, int cmd, struct msqid_ds *buf);
msgget()用來創建Message Queue(服務端)或和一個已建立的Message
Queue連接(客戶端)。key,指定用來生成message
id的關鍵字,msgflag和open()的flags很相似,可用IPC_CREAT, IPC_EXECL, S_IRUSR等。
在服務端,可用IPC_PRIVATE(或0)來指定key值,來生成一個新的Message Queue,或使用指定的key值(32位的無符號數),或使用ftok()來生成一個key。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
在客戶端,能夠直接使用服務端生成的message
id(通過某些途徑傳送,如文檔,父子進程),也能夠用msgget通過和服務端使用相同的key值來生成相同的message
id,但不能使用IPC_PRIVATE(或0),msgflag也不能使用IPC_CREAT。
Return Value: Sucess return value is the message id(non-negative integer), otherwise -1 return.
msgsnd()用來發送消息。
struct msgbuf {
long mtype;
char mtext[1];
};
msgsz的計算方法: msgsz = sizeof(msgbuf) - sizeof(long);
msgflag有一個標志:IPC_NOWAIT。當消息隊列已滿(可能是消息總數達到了限制值,也可能是隊列中字節總數達到了限制值),立即出錯返回,假如沒有指定,則阻塞。
msgrcv()用來接收消息。msgtype用來指定讀取的消息類型。msgtype == 0, 返回第一個消息; msgtype >
0, 返回消息類型為msgtype的消息;msgtype < 0, 返回隊列中類型值小于msgtype的絕對值的消息集中最小的消息。
msgflag有兩個值:MSG_NOERROR, IPC_NOWAIT。當MSG_NOERROR被指定的時候,若消息太長就被截斷,否則返回錯誤;IPC_NOWAIT用于需要讀取的消息不存在時則阻塞。
msgctl用于控制消息隊列。cmd有三種值:IPC_STAT,IPC_SET,IPC_RMID。
IPC_STAT用于取出消息隊列的 msqid_ds結構并保存到buf中。
IPC_SET用來把buf指向的msqid_ds,配置成消息隊列的msqid_ds。只有四個值能夠更改:msg_perm.uid, msg_perm.gid,msg_perm.mode, msg_qbytes。
IPC_RMID用來刪除消息隊列。
struct msqid_ds
{
struct ipc_perm msg_perm;
ulong msg_qbytes; //max of bytes of queue
...
};
struct ipc_perm
{
uid_t uid; //owner's effective user id
gid_t gid; //owner's effective group id
uid_t cuid; //creator's effective user id
gid_t cgid; //creator's effective group id
mode_t mode; //access modes
ulong seq; //slot usage sequence number
key_t key;
};