ACE的陷阱
坦白說,使用這個标題無非是希望能夠吸引你的眼球,這篇文章的目的僅僅是為了揭示一些ACE缺陷的。文章适合的讀者是對ACE(ADAPTIVE Communication Environment)有一定研究,或者正在使用ACE從事項目開發的人士參考。如果你對C++還是新手,甚至包括ACE知識初學者,(但你想飛的更高),建議你收藏這篇文檔以後閱讀。
秉承陷阱系列文章的傳統,我隻是通過一些辯證的角度去看ACE的一些不足,對于ACE的強大和優美我就不再作贊美。從2000年,到現在,ACE在中國已經從星星之火,開始有燎原之勢。這一方面說明ACE的優美和實力已經逐漸得到大家的認可(我所知道的Adobe reader的使用ACE,估計是為了跨平台,國内的大量電信的網管,計費,智能網軟體也使用ACE),一方面要感謝的是的馬維達這位國内少有的職業作家,國内的ACE的中文資料(包括大量免費資料)都出自這位老兄。
但ACE無疑是複雜的,能夠暢快的遨遊在其中的絕對不是泛泛之輩。沒有對網絡,設計模式,作業系統有一定的底蘊,想痛快的駕馭ACE無疑是較難的。另外,由于ACE仍然處在逐漸發展的過程中。他的很多問題仍然有待進一步完善。重要的是一些文案的不足,閱聽人面狹小,導緻許多ACE的使用者在使用ACE的時候會碰上很多問題。這篇文案就是用于徹底揭示部分這些問題。希望大家能在更加順捷的使用它。
另外,請注意我使用的陷阱這個術語,而不是原罪。(C Trap and Pitfalls 倒有很多應該是Original sin)ACE還在不停的發展中。很多問題可能會在以後的版本中間改進。是以在我認為的的确是問題的章節後面,我會附上知道錯誤的版本号。
1 我将什麼列為陷阱
1.1 低效的子產品
作為一個代碼級的中間件。ACE無疑是高效的,但是坦白說ACE的代碼不是非常完美的。ACE的很多地方提供的是一個架構解決方案,為了保證架構的可移植和通用,代碼中大量使用了virtual 函數,Bridge模式,多線程下的鎖操作,甚至有相當的new操作……,這些東西都限制ACE的性能。是以個人謹慎的将ACE的效率定義為中上。
個人認為,一般情況下,如果你使用ACE的API代替系統API,速度應該降低0.01%以下,主要導緻這些差役在于ACE的再次封裝,而函數棧的調用成本應該可以幾乎不計。ACE的優勢在高性能的系統架構,而不是絕對的函數性能,如果你要再考慮在加入系統架構的其它功能呢,(舉一個例子,當你想把定時器優美的合入你的代碼時),ACE就有足夠的優勢讓你選擇他。【注】
在此啰嗦一句,同樣也有很多人質疑STL的性能。所有好的類庫一樣,他帶來優勢的同時也會有一定的遺憾,比如少量性能降低。但是如果說他們的性能不好,那是無稽之談。(不信,把你認為性能差的代碼給我寫寫看。)建議固步自封的程式員不要再幹買椟還珠的事情,先去讀讀那些優美的代碼。
但是和所有的架構一樣,ACE也有不少的地方的地方是性能的暗礁,你最好繞開。當然一般而言ACE會提供多條道路,重要的是你能選擇正确。
1.2 設計缺陷
ACE的有多個層次,側記缺陷這類錯誤往往出現在ACE的高階封裝中。同時由于ACE是一個跨平台的中間件。是以為了平台的相容性,ACE做了很多折中和彌補,有些是很漂亮的,但有些卻不是非常理想。
1.3 使用不便的地方
所有的代碼都是不完美的,特别是ACE這種要讓無數人在無數環境下使用的軟體。很多使用不便的問題都是來自我個人的一些習慣,這些算是苛責了。
1.4 容易誤解或者誤用的地方
由于ACE的龐大性,很多時候大家會錯誤的了解使用ACE的某些代碼實作某些特性。在此将寫一些曾經讓我們栽跟頭的陰溝寫出來。另一方面,ACE的文檔的某些介紹也存在含混,會誤導大家的了解,錯誤的地方。
2 ACE的連結Link錯誤
很多人在Windows使用ACE的時候往往會出現以下的Link錯誤。
Why do I get errors while using 'TryEnterCriticalSection'?/ace/OS.i(2384) : error C2039:'TryEnterCriticalSection': is not a member of '`global namespace''
複制代碼
其實這個錯誤不是由于ACE導緻的,隻是編譯器把這個贓栽倒了ACE上。出現這個錯誤的原因主要是因為一些關鍵宏定義沖突,一般是_WIN32_WINNT,'TryEnterCriticalSection' 這個函數是NT4.0後才出現的函數,如果這個宏被定義的小于0x0400或者沒有定義,那麼就會出現這個錯誤。
是以最簡單的處理方法是在自己的預定義頭檔案中加入一行。
#if !defined (_WIN32_WINNT)# define _WIN32_WINNT 0x0400#endif
複制代碼
其實ACE自己對于宏的處理是比較嚴謹的,ACE的config-win32-common.h中間就有這行定義,是以在一般而言,可以将ACE的頭檔案包含定義放在在頂部,這樣也可以避免這個編譯錯誤。
預定義頭檔案是一個良好的程式設計習慣,你可以将自己的大部分宏定義,include包含的本工程以外的外部.h檔案。簡言之就是預定義頭檔案中使用#include<>,表示包含工程以外檔案,自己工程内部隻使用#include””,表示包含目前工程目錄下的檔案。大部分C/C++的程式員都有過連結和一些預定義沖突錯誤消耗大量的時間,原來我也是如此,但是在掌握預定義頭檔案方法後,我幾乎沒有為這個問題折磨過。其實Virsual C++ 在生産MFC工程的時候,會自動幫你自動生産一個預定義頭檔案stdafx.h,隻是我們不善利用而已。
其實對于很多編譯器,使用預定義頭檔案還可以加快編譯速度。Virusal C++的預定義會生産一個pch檔案,基本可以提高編譯速度一倍。Virusal C++的工程中間有專門的預定義頭檔案設定。C++ Builder采用可以采用的編譯宏(好像是專用的)加快編譯速度。大緻的原理是編譯器會在對預定義頭檔案中包含的檔案進行與處理,在外部檔案沒有發生改動的時候,編譯器可以使用編譯這些檔案生成的中間檔案加快編譯速度。
3 不要使用ACE_Timer_Hash
ACE有一個非常優美的定時器隊列模型,他提供了4種定時器Queue讓大家使用:ACE_Timer_Heap,ACE_Timer_Wheel,ACE_High_Res_Timer,ACE_Timer_Hash。在《C++ Network Programming Volume 2 - Systematic Reuse with ACE and Frameworks》中間有相應的說明,其中按照說明最誘人的的是:
ACE_Timer_Hash, which uses a hash table to manage the queue. Like the timing wheel implementation, the average-case time required to schedule, cancel, and expire timers is O(1) and its worst-case is O(n).
但是遺憾的是,ACE_Timer_Hash其實是性能最差的。幾乎不值得使用。我曾經也被誘惑過,但是在測試中間發現,文檔中所述根本不屬實,在一個大規模定時器的程式中,我使用ACE_Timer_Hash發現性能非常不理想,檢查後發現ACE的源代碼如下:
template <class TYPE, class FUNCTOR, class ACE_LOCK, class BUCKET> intACE_Timer_Hash_T<TYPE, FUNCTOR, ACE_LOCK, BUCKET>::expire (const ACE_Time_Value &cur_time){ // table_size_為Hash的桶尺寸,如果要避免沖突,桶的數量應該盡量大,//每個桶可以了解為一個Hash開鍊的連結清單 // Go through the table and expire anything that can be expired //周遊所有的桶 for (size_t i = 0; i < this->table_size_; ++i) { //在每個桶中檢查是否有要進行逾時處理的元素 while (!this->table_->is_empty () && this->table_->earliest_time () <= cur_time) { …………
複制代碼
簡單說明一下上面的代碼,ACE_Timer_Hash_T采用開鍊的Hash方式,每個桶就是一個連結清單,在逾時檢查時所有的桶中是由有要進行逾時處理的元素。是以在逾時進行中ACE采用了周遊所有元素的方法。但悖論是如果你希望Hash的沖突不大,你就必須将桶的個數調整的盡量多。我在測試中将上述的程式的Time_Queue替換為标準的的ACE_Timer_Heap,發現性能提高數百倍。
冷靜下來思考一下,這也是正常的。對于一個Hash的實作,保證查詢的速度,也就是通過定時器ID進行操作的速度是足夠快的。但是實際上對于定時器操作,最大的成本應該是尋找要逾時的定時器,對于Hash這種資料結構,隻能采用疊代周遊的方式……, 是以采用Hash的低效是正常的。而原文應該改為schedule, cancel,的最好時間複雜度是O(1),最差是O(n),而expire的時間複雜度始終是O(n)。
這個問題在ACE自己的文檔《Design, Performance, and Optimization of Timer Strategies for Real-time ORBs》中間也有較為正确的描述。
這個問題至少倒5.6.1的版本還是存在的。我個人估計也不會得到解決。Hash的特性擺在那兒呢,除非ACE采用更加複雜的資料結構。
4 Reactor定時器的精度取決于實作
由于Reactor在各個平台的預設實作都取決于平台的實作,比如在Windows下預設的Reactor是WFMO_REACTOR,而在Linux和UNIX平台,預設的Reactor是Select_Reactor,而Reactor的實作往往取決于使用的反應器底層實作,而這些反應器的時間精度就決定了你的定時器的時間精度。下表大緻回報了一些常用的定時器的實作。
表1 常用Raactor的實作
Reactor
反應器的底層實作
時間精度
ACE_Select_Reactor
select函數
使用struct timeval結構進行逾時處理; timeval 結構可以精确倒微秒。
Dev_Poll_Reactor
poll或者而epoll
timeout參數的機關是毫秒。
ACE_WFMO_REACTOR
WaitForMultipleObjects
dwMilliseconds 的參數機關是毫秒
不過作為伺服器的開發,我倒想不出什麼地方需要精确到0.1s定時器的地方,了解一下差異性就足夠了。
5 WFMO_Reactor的與衆不同
WFMO_Reactor是ACE_Reactor在Windows下的預設實作(為什麼不選擇ACE_Select_Reactor作為預設實作,可能是基于效率和強大性的考慮),WFMO_Reactor的低層使用的函數是WaitForMultipleObjects和WSAEventSelect,WSAEnumNetworkEvents。其中WaitForMultipleObjects函數用于處理線程,互斥量,信号燈,事件,定時器等事件,而WSAEventSelect用于處理網絡IO事件。
由于Windows API和作業系統的特性不一樣,WFMO_Reactor在很多地方的表現和其他平台不一緻。 【注】
【注】其實這兩個問題在《C++ Network Programming Volume 2 - Systematic Reuse with ACE and Frameworks》中4.4 The ACE_WFMO_Reactor Class有說明。這兒算是借花獻佛。
5.1 WFMO_Reactor隻能處理62個句柄
由于WaitForMultipleObjects不是一個處理大量事件的函數,其最多處理64個事件句柄,而WFMO_Reactor自身為了處理使用了2個句柄,是以一個WFMO_Rector對象隻能處理。
如果你想做大規模的網絡接入,62個事件句柄顯然是不夠的,特别是要同時處理IO事件時,導緻這個不足的應該是WFMO_Reactor的設計者的一個選擇。在賦予WFMO_Reactor強大的特性的同時,WFMO_Reactor的設計者隻能讓網絡IO事件的數量委屈一下了。
5.2 WRITE_MASK觸發機制
WFMO_Reactor 選擇的是Windows的WSAEventSelect 函數作為網絡的IO的反應器。但是WSAEventSelect函數的FD_WRITE的事件處理和傳統的IO反應器(select)不同。下面是MSDN的描述。
The FD_WRITE network event is handled slightly differently. An FD_WRITE network event is recorded when a socket is first connected with connect/WSAConnect or accepted with accept/WSAAccept, and then after a send fails with WSAEWOULDBLOCK and buffer space becomes available. Therefore, an application can assume that sends are possible starting from the first FD_WRITE network event setting and lasting until a send returns WSAEWOULDBLOCK. After such a failure, the application will find out that sends are again possible when an FD_WRITE network event is recorded and the associated event object is set.
簡單翻譯就是,隻有在三種條件下,WSAEventSelect才會發出FD_WRITE通知,一是使用connect或WSAConnect,一個套接字成功建立連接配接後;二是使用accept或WSAAccept,套接字被接受以後;三是若send、WSASend、sendto或WSASendTo函數傳回失敗,而且錯誤是WSAEWOULDBLOCK錯誤後,緩沖區的空間再次變得可用時。【注】
【注】這種觸發方式在IO反應器或者說IO多路複用模型中應該被稱為邊緣觸發方式。select函數好像沒有這種觸發方式而是水準觸發方式, Epoll是支援這種方式的,但是預設還是水準觸發,這種方式可能有更高的效率,但是代碼更加難寫。
可以這麼了解,WSAEventSelect認為套接字基本都是可寫狀态,它認為你應該大膽send。隻有send出現WSAEWOULDBLOCK失敗後,你才需要使用WSAEventSelect反應器。【注】
是以對于WFMO_Reactor的,你不可能依靠注冊(或者是喚醒)IO句柄進行寫操作,WMFO_Reactor很有可能不會去回調你的handle_output函數。
【注】對于網絡套接字,隻要緩沖區還有空間就可以直接發送,除非緩沖區沒有空間了,才可能出現阻塞錯誤,是以直接send失敗的可能性很小,另外反複調用注冊IO句柄一類的操作其實是比較耗時的。其實先send,如果send失敗再注冊IO句柄到反應器的方式應該是一種更加高效的方式,高壓力的通訊伺服器應該選擇這個編寫方式。
我自己的通信伺服器通過這個改造,提高的性能在15%左右(CPU占用率下降)。
由于WFMO_Reactor的這些特點,其實很大的限制了Reactor的可移植性。其實個人感覺如果你對系統特性沒有那麼多要求,在Windows下選擇Select_Reactor替換WFMO_Reactor是更好的選擇。
6 盡量使用ID取消ACE_Event_Handler定時器
ACE的Reactor 提供了兩種方式取消定時器:virtual int cancel_timer (ACE_Event_Handler *event_handler, int dont_call_handle_close = 1);virtual int cancel_timer (long timer_id, const void **arg = 0, int dont_call_handle_close = 1);
複制代碼
一種是使用定時器ID取消定時器,這個ID是定時器是的傳回值,一種是采用相應的ACE_Event_Handler指針取消定時器。一般情況下使用ACE_Event_Handler的指針取消定時器無疑是最簡單的方法,但是這個方法卻不是一個高效的實作。是以如果您的程式有大規模的定時器設定取消操作,建議盡量使用ID取消定時器。我們用ACE_Timer_Heap和ACE_Timer_Has兩個Timer_Queue剖析一下。
6.1 ACE_Timer_Heap如何根據Event_handler取消
先選擇最常用的Time_Queue ACE_Timer_Heap舉例,其使用ACE_Event_Handler關閉定時器的代碼是:
template <class TYPE, class FUNCTOR, class ACE_LOCK> intACE_Timer_Heap_T<TYPE, FUNCTOR, ACE_LOCK>::cancel (const TYPE &type, int dont_call){ // Try to locate the ACE_Timer_Node that matches the timer_id. //循環比較所有的的ACE_Event_Handler的指針是否相同 for (size_t i = 0; i < this->cur_size_; ) { if (this->heap_->get_type () == type) { ……………… } }
複制代碼
而使用TIMER_ID關閉的代碼如下,它是通過數組下标進行的定位操作。
template <class TYPE, class FUNCTOR, class ACE_LOCK> intACE_Timer_Heap_T<TYPE, FUNCTOR, ACE_LOCK>::cancel (long timer_id, const void **act, int dont_call){ //通過數組下标操作,速度當然奇快無比。 ssize_t timer_node_slot = this->timer_ids_[timer_id]; …… //跟進數組ID進行操作 else { ACE_Timer_Node_T<TYPE> *temp = this->remove (timer_node_slot); }}
複制代碼
對于ACE_Timer_Heap,采用ACE_Event_Handler指針取消定時器的方式的平均時間複雜度應該就是O(N)。由于ACE的的一個Event_handler可能對應多個定時器,是以必須檢查所有的才能確定取消所有的相關定時器。
6.2 ACE_Timer_Hash如何根據Event_handler取消
對于Timer_Hash,其通過ACE_Event_Handler關閉定時器的代碼是:
template <class TYPE, class FUNCTOR, class ACE_LOCK, class BUCKET> intACE_Timer_Hash_T<TYPE, FUNCTOR, ACE_LOCK, BUCKET>::cancel (const TYPE &type, int dont_call){ Hash_Token<TYPE> **timer_ids = 0; //根據Event Handler有一個定時器new一個數組出來 ACE_NEW_RETURN (timer_ids, Hash_Token<TYPE> *[this->size_], -1); size_t pos = 0; //根據定時器的個數再進行取消 for (i = 0; i < this->table_size_; ++i) { ACE_Timer_Queue_Iterator_T<TYPE, ACE_Timer_Hash_Upcall<TYPE, FUNCTOR, ACE_LOCK>, ACE_Null_Mutex> &iter = this->table_->iter ();
複制代碼
可以看到Timer_Hash的cancel比ACE_Timer_Heap的cancel(Event_Handler)要好一點點。但是其中也有new和delete操作,這些操作也不是高效操作。
是以說在大規模的定時器使用中,推薦你還是使用定時器的ID取消定時器更加高效的多。
7 注意ACE_Pipe的實作
ACE_Pipe是一個跨平台的管道實作。标準情況來講,采用的實作,但是在最大的兩個平台Windows和Linux上,ACE的實作是采用的Socket實作。
intACE_Pipe:open (int buffer_size){ ACE_TRACE ("ACE_Pipe:open"); #if defined (ACE_LACKS_SOCKETPAIR) || defined (__Lynx__) //綁定了一個本地端口,0.0.0.0,然後找到相應的端口,用于後面的連結 if (acceptor.open (local_any) == -1 || acceptor.get_local_addr (my_addr) == -1) result = -1; else { // Establish a connection within the same process. if (connector.connect (writer, sv_addr) == -1) result = -1; ……
複制代碼
是以很多管道特性所特有的東西,在這兩個平台上是無法使用ACE_Pipe實作的。比如,管道的特性可以保證在暫時沒有接受者的情況下使用,而Socket是不可能有這個特性的。你必須保證先有接受者,後有發送者的時序。
是以在這些平台上最好不用這個封裝。
8 慎用Reactor Notify機制
在Reactor的模式,有一種輔助的通知機制,Notify機制,簡單說就是通過通知發起者調用notify函數,notify的消息被儲存在一個管道中,handle_event的進行中會檢查這個管道中是否有通知資料,如果有就根據通知的消息,會根據預設的通知消息的類型去調用hanle_input等函數。
從設計的角度将,這個機制無疑是非常優美的,對于Reactor,它在IO驅動以外,提供了一種新的驅動方式。但是從實作角度來講,這個機制要慎用。原因有兩個。
8.1 ACE Reactor的預設Notify方式采用的是ACE_Pipe
ACE Reactor的預設Notify方式采用的是ACE_Pipe,是以ACE_Pipe在Windows和Linux平台上的問題,Notify機制把ACE_Pipe的缺陷一個不少的繼承了,而且問題更加多。
ACE_Pipe notification_pipe_;
複制代碼
原來在調試ACE代碼的時候,我發現隻要一使用Reactor,即使隻使用定時器(除非明确不使用Notify),防火牆都會報警有監聽端口。我曾經對此大惑不解,直到讀了ACE的這部分原代碼。這樣做的壞處有很多。第一個是由于采用的阻塞IO。速度會慢很多,第二個由于是單線程的處理,如果在壓力極大的情況下,可能出現死鎖的問題。比如在有大規模的Notify的情況下,發送緩沖區很可能會被塞滿(由于是單線程,這時不會有接受者),同時由于為了簡化,ACE_Pipe采用的IO是阻塞的,是以會導緻整個程式死鎖。第三就是這樣的情況下ACE_Pipe會打開一個臨時的端口,而且會綁定所有的IP(0.0.0.0),如果對于一個安全要求嚴格的的場景,這個将是一個不可饒恕的錯誤。【注】
【注】在一個安全要求嚴格的環境下,這個臨時端口輕則可以讓你的伺服器輕易陷于崩潰,重則可以讓你整個網絡被黑客攻陷。
不過還好的是ACE的開發者估計自己也意識倒了這個麻煩。是以提供了另外一種消息隊列的方式。你可以通過定義ACE_HAS_REACTOR_NOTIFICATION_QUEUE的宏編譯ACE,這樣ACE将不使用ACE_Pipe作為Notify消息的管道,而使用一個自己的記憶體隊列儲存Notify消息,這個隊列是動态擴充的。而且由于是記憶體操作,性能方面沒有太大問題。
大體位置在重複編譯的衛哨後面,#include "ace/pre.h"前面。保證這個宏起到作用。
#ifndef ACE_CONFIG_LINUX_H#define ACE_CONFIG_LINUX_H //使用記憶體隊列作為Notify Queue#define ACE_HAS_REACTOR_NOTIFICATION_QUEUE #include "ace/pre.h"
複制代碼
這個問題到5.6.1還是存在的,估計由于曆史的原因,在很長一段時間也不會得到解決。
8.2 考慮不周的Reactor Notify機制
同上,這也應該是一個BUG,Reactor Notify的代碼有考慮不周的地方。Notify機制的本質是提供了一條消息隊列讓大家有方法調用Event_handler,但是存在一種可能,在你的通知消息在消息隊列的時候,Event_hanlder由于後面的處理可能已經handle_close了。但是ACE的dispatch_notify卻沒有考慮倒這一點(或者說考慮倒這一點也不好解決)。
ACE_Select_Reactor_Notify::dispatch_notify函數的代碼。intACE_Select_Reactor_Notify::dispatch_notify (ACE_Notification_Buffer &buffer){…………ACE_Event_Handler *event_handler = buffer.eh_; bool const requires_reference_counting = event_handler->reference_counting_policy ().value () == ACE_Event_Handler::Reference_Counting_Policy::ENABLED; //如果此時這個ACE_Event_Handler已經被handle_close了,你如何是好。。。。 switch (buffer.mask_) { case ACE_Event_Handler::READ_MASK: case ACE_Event_Handler::ACCEPT_MASK: result = event_handler->handle_input (ACE_INVALID_HANDLE);
複制代碼
這個bug到5.6.1還沒有解決。我覺得這個問題是可以解決的(暫時還沒有提BUG),但是得到解決的方式卻仍然是低效的方案(還記得取消定時器的那個缺陷嗎)。
如果你仔細看過上面的幾節,你也許會發出驚歎,啊,又是Reactor Notify?對,又是它。看起來我好像一直在和ACE的Notify機制在做對,但它的确讓我吃了無數的苦頭。這部分的設計的确有一點畫蛇添足的感覺,而且由于跨平台性等原因,這個東東的實作一直不如意。其實自己使用ACE的實作(比如Message_Queue)一套這樣的機制應該是易如反掌的事情。不苛求了。
如果你用不到Notify機制,最好在ACE_Reactor初始化的時候徹底關閉Notify機制。很多Reactor的初始化函數都提供了關閉notify pipe的方式。比如ACE_Select_Reactor_T的open函數的disable_notify_pipe參數。當其為1的時候表示關閉notify 管道。
//disable_notify_pipe參數為1時表示關閉NOTIFY PIPE,不使用他
template <class ACE_SELECT_REACTOR_TOKEN> intACE_Select_Reactor_T<ACE_SELECT_REACTOR_TOKEN>:open (size_t size, int restart, ACE_Sig_Handler *sh, ACE_Timer_Queue *tq, int disable_notify_pipe, ACE_Reactor_Notify *notify)
複制代碼
9 ACE_Dev_Poll_Reactor的處理優先級嚴重偏向定時器
不使用POLL和EPOLL【注】的人,估計不太知道這個ACE_Dev_Poll_Reactor,但實際上。特别是Linux下的EPOLL(一個IO多路服用模型),這是Linux大規模接入的重要法寶,從目前的表現來看,其他平台上還沒有可以超越EPOLL的東西,Windows下的異步IO的性能也還遠遠遜于EPOLL。
如果要使用EPOLL而不是POLL,要使用宏ACE_HAS_EVENT_POLL編譯ACE,大體位置在重複編譯的衛哨後面,#include "ace/pre.h"前面。保證起到作用。
#ifndef ACE_CONFIG_LINUX_H#define ACE_CONFIG_LINUX_H// ACE_HAS_EVENT_POLL宏用于定義使用EPOLL子產品,同時注意不同LINUX平台下編譯可能有少量//不同。我曾經使用過的一個核心2.4的Slackware平台,要在編譯ACE的時候加入 –lepoll,可能是由于//其是打更新檔增加的功能#define ACE_HAS_EVENT_POLL #include "ace/pre.h"
複制代碼
但也許是由于這個東西過新還是由于設計者是一個定于時間要求很敏感的人。的設計明顯的是定時器優先。但是了解EPOLL和POLL的人都知道,UNIX和Linux設計這兩個咚咚的目的就是解決大規模IO複用。不是為了保證定時器優先,是以我對這個設計很是不解,郁悶。其大體思路為,
1.) 先檢查定時器逾時的隊列,計算最小的逾時時間,用于IO等待。
2.) 觸發IO事件
3.) 處理逾時的Handler,如果有逾時的事件,傳回(1)。這點我看得最郁悶。
4.) 再分發處理IO事件
可以看到在處理逾時句柄的時候,ACE_Dev_Poll_Reactor發現有逾時的事件會傳回到檢查逾時隊列。是以如果在Reactor同時有定時處理,IO的優先級會很低。
其實這個的設計者也知道這個問題。他在代碼中間做了如下的記錄。
intACE_Dev_Poll_Reactor::dispatch (Token_Guard &guard){…… // Handle timers early since they may have higher latency // constraints than I/O handlers. Ideally, the order of // dispatching should be a strategy... if ((result = this->dispatch_timer_handler (guard)) != 0) return result;
複制代碼
由于EPOLL的特性,使用它大部分都是為了處理大規模的IO請求,定時器其實隻有少量的需求,不是我們需求的重點。
這個問題到最近的5.6.1版本沒有得到解決。
我曾經回報過這個問題。但是得到沒有明确的解答。解決這個問題的方法其實也很簡單,自己重載這個類,然後自己實作相應的函數。觸發IO事件後立即分發IO事件,而且加入了一個IO的優先級别。在多次IO處理的循環後在進入時間事件處理。保證時間處理的粒度在1s以内基本就可以了
10 Event_Handler在程式退出前應該自己關閉
在程式退出的【注】,我們往往不會自己關閉Event_Handler,而寄希望Reactor 的清理。但是實際情況會複雜很多。使用的時候必須當心。
【注】是否要在退出的時候清理所有配置設定的記憶體?在普通的作業系統中,程式的退出會回收所有的配置設定記憶體。是以很多人會逃避在最後階段的清理配置設定的記憶體。但是這實在不是一個良好的喜歡。一方面對于很多OS(比如嵌入系統)不會回收記憶體資源,一些核心資源(UNIX)也不會在程序退出後釋放,程式設計就應該要養成清理的好習慣,更何況不進行釋放在記憶體檢查的軟體一般會報錯,如果不清理會幹擾我們對于記憶體洩露的定位。
10.1 Reactor的close可能不會關閉Event_Handler
理論上講,ACE_Reactor提供了一個close函數,所有的Event_Handler應該統一在這個函數進行關閉。
ACE_Reactor采用的是模式,封裝了不同Reactor的實作。這些實作的close函數未存在一定的差異性。就我的閱讀和嘗試來看,Select_Reactor在close函數關閉了所有的IO句柄相關的Event_Handler,而Dev_Poll_Reactor的close實作就沒有關閉。
Select_Reactor的close代碼。
template <class ACE_SELECT_REACTOR_TOKEN> int
ACE_Select_Reactor_T<ACE_SELECT_REACTOR_TOKEN>::close (void)
{
……
//在handler_rep的close函數會關閉所有的register的句柄的handler,調用他們的
//handle_close函數
this->handler_rep_.close ();
Dev_Poll_Reactor的close的調用了函數ACE_Dev_Poll_Reactor_Handler_Repository::close,而後有逐漸調用了unbind_all,remove_reference。
//close會經過多級調用到ACE_Dev_Poll_Reactor_Handler_Repository:: unbind_all
//unbind被unbind_all函數調用decr_refcnt == true
int
ACE_Dev_Poll_Reactor_Handler_Repository::unbind (ACE_HANDLE handle,
bool decr_refcnt)
{……
// remove_reference函數沒有調用handle_close,而是減去了引用計數
if (decr_refcnt)
this->handlers_[handle].event_handler->remove_reference ();
……
}
ACE_Event_Handler::Reference_Count
ACE_Event_Handler::remove_reference (void)
{
//如果打開了引用計數,則使用應用計數方式管理方式。但是代碼預設不采用應用計數模式
//是以下面的代碼都無法執行
if (reference_counting_required)
{
//減去引用計數
Reference_Count result =
--this->reference_count_;
//如果已經沒用引用個數了,删除自己。
if (result == 0)
delete this;
}
可以看到ACE_Event_Handler的代碼預設不采用應用計數模式,(eference_counting_required預設為DISABLED)而Dev_Poll_Reactor卻非要使用引用計數模式去清理Event_Handler。
我對Dev_Poll_Reactor為什麼要設計成這樣表示不解。也對Dev_Poll_Reactor送出過BUG,但是Dev_Poll_Reactor的開發者不認為這樣有什麼不妥,本人E文羞澀,無法說服具體的開發人員,不過在送出BUG時,居然得到了Douglas回報(他開始時認同我的看法),對于他們的執着和認真還是表示敬仰。
10.2 可能會導緻重複釋放引發Coredump
這個問題是在工作中調試一個BUG出現的。
在測試一個伺服器的時候發現Coredump發生kill程序,讓其退出在之後,會出現Coredump檔案。Coredump顯示出現問題的地方在。
#1 0x0805bc7b in ~ACE_Timer_Heap_T (this=0x82d3ec8) at /usr/local/ACE_wrappers/ace/Timer_Queue_T.cpp:442
#2 0x0805b86d in ~ACE_Singleton (this=0x82cca70) at egg_application.cpp:52
#3 0x08056785 in ACE_Singleton<EggSvrdAppliction, ACE_Null_Mutex>::cleanup (this=0x82dfb90)
由于希望改變ACE_Time_Queue的特性(數量),我替換Reactor的預設Time_Queue,是以必須自己銷毀自己管理的TimeQueue。而在外部最後銷毀的時候出現Coredump。由于和Time_Queue相關,我檢查了所有的Timer相關的Event_handler,發現有一個Event_handler沒有自己主動調用handler_close釋放,這個Event_handler隻有定時器,沒有注冊任何IO事件。修改代碼為主動釋放後,再次測試就發現Coredump的問題得到解決。
我檢查了一下原有代碼堆棧的調用順序,找到了問題原因。
(1)ACE_Reactor::close,實際調用ACE_Select_Reactor::close
(2) Select_Reactor::close 嘗試關閉所有的IO句柄相關的Event_handler,但由于Time_Queue是外部傳入的參數,是以不清理Time_Queue。
(3)Time_Queue清理,Time_Queue的析構函數被調用,Time_Queue的析構函數會釋放所有的定時器相關的Event_handler。而他的釋放還會調用hanlder_close。但是這是Reactor對象已經銷毀了。是以造成了Coredump。
注意由于Reactor的封裝了Event_handler定時器,IO句柄,Notify機制等回調接口。是以Event_handler可能隻關聯到IO句柄,也可能隻關聯定時器,同時Reactor的模型決定了他的内部管理是複雜的。而在釋放的過程中很可能會發生交錯的問題,而,像上面問題的Event_handler就隻關聯的定時器,是以在Reactor的close的時候沒有關閉。進而導緻在後面的清理工作中産生時序問題。
最簡單的方式還是自己在程式退出前清理釋放所有的Event_handler.再調用Reactor的close。
11 調整系統時鐘導緻ACE定時器丢失
由于我們采用的伺服器一般都是靠紐扣電池作為能源驅動和記錄時鐘,一般在運作一段時間後都會出現時間誤差。是以很多大規模的分布系統都有校時操作,特别是一些對時鐘要求精确的分布式系統(比如計費等),往往都會有一個主機提供精确時鐘服務(其可能采用GPS校時),其他伺服器通過這台伺服器校時,校時操作一般都是直接改變系統時鐘。
ACE的定時器都是采用Event_Handler進行處理,而Event_Handler一般而言都是采用絕對時間作為記錄逾時的時間戳,但是絕對時間的方式在系統時鐘被調整的時候,會導緻“丢失”部分定時器的處理,導緻一些問題。
在設定定時器時,schedule_timer函數通過gettimeofday得到定時器時間點的時間。
template <class ACE_SELECT_REACTOR_TOKEN> long
ACE_Select_Reactor_T<ACE_SELECT_REACTOR_TOKEN>::schedule_timer
(ACE_Event_Handler *handler,
const void *arg,
const ACE_Time_Value &delay_time,
const ACE_Time_Value &interval)
{
// schedule_timer記錄的是系統時間,
if (0 != this->timer_queue_)
return this->timer_queue_->schedule
(handler,
arg,
timer_queue_->gettimeofday () + delay_time,
interval);
}
在派發定時器的過程中也是調用gettimeofday函數。
template <class TYPE, class FUNCTOR, class ACE_LOCK> ACE_INLINE int
ACE_Timer_Queue_T<TYPE, FUNCTOR, ACE_LOCK>::expire (void)
{
if (!this->is_empty ())
return this->expire (this->gettimeofday () + timer_skew_);
else
return 0;
}
可以看出,如果在schedule_timer後,将系統時鐘向前調節(調慢)以後,原有的定時器将要經過更多的時間才能觸發。進而導緻這段時間内定時器無法觸發。進而造成定時器丢失。
這個問題的解決方法有2個,簡單方法是将系統時鐘校準的頻度提高,保證每次校準的時候,系統的時鐘出現的偏差都不會影響時鐘的定時器觸發。
另外一種是ACE的Timer_Queue自己提供的方法,通過上面的代碼我們可以發現,其實ACE_Timer_Queue_T::gettimeofday是一個調用的是一個函數指針。預設使用ACE_OS:: gettimeofday函數,這個函數可以替換的。
void gettimeofday (ACE_Time_Value (*gettimeofday)(void));
ACE提供一個依賴于作業系統的高解析定時器,ACE_High_Res_Timer,這個類是通過OS的TICK數量來得到更加精确的時鐘的【注】。
【注】OS在啟動後,都會有一個TICK在不斷的計數,這個TICK就像一個打點計數器,每次增加1.一般計數周期就是一個CPU周期。
由于CPU的TICK不會随着你調整系統時鐘而調整。是以可以看做是一個相對值。ACE_High_Res_Timer可以根據相對值計算得到非常精确的程式運作時鐘,。直接使用ACE_High_Res_Timer:: gettimeofday_hr函數作為ACE_Timer_Queue_T::gettimeofday函數指針。并且在程式的開始部分使用函數,ACE_High_Res_Timer::global_scale_factor (),用于激活高精度定時器。【注】
【注】這個方法得益于原來公司的兩位同僚zhangtianhu和liaobincai的一個終結。在此懷念一下和他們共事的日子。另外,我沒有仔細研究過這個方法,由于擷取CPU的TICK的擷取很有可能是一個核心操作,效率可能不高。
采用上述的兩個方法基本可以避免這個問題。
12 ACE的CDR中的位元組對齊問題
大家應該都知道計算機中間都有位元組對齊問題。CPU通路記憶體的時候,如果從特定的位址開始通路一般可以加快速度,比如在32位機器上,如果一個32位的整數被放在能被32模除等于0的位址上,隻需要通路一次,而如果不在,可能要通路兩次。但是這樣就要求一些資料從特定的位址開始,而不是順序排放(中間會有一些空餘的位址),這就是位元組對齊。
而ACE CDR的估計也是為了加快速度,進而在CDR編碼上預設也使用了位元組對齊。是以在ACE的CDR編解碼過程中,傳入的參數位址最好是能符合位元組對齊規則,否則可能會編解碼錯誤。
ACE_OutputCDR構造函數會調用一個函數mb_align調整傳入的位址參數成為位址對齊位址。但是其的調整函數ACE_ptr_align_binary不知處于什麼考慮,不是按照機器的對齊長度而是采用的 ACE_CDR::MAX_ALIGNMENT(64bit,長度為8BYTPES)作為參數位址。那麼ACE_OutputCDR的内部位址是按照8位元組作為對齊的,但是ACE_InputCDR卻沒有将内部位址調整為模除64等于0的位址上,而隻是調整為模除32(在32位機器上)等于0的位址。
void
ACE_CDR::mb_align (ACE_Message_Block *mb)
{
#if !defined (ACE_CDR_IGNORE_ALIGNMENT)
//如果使用位元組對齊方式,使用最大的對齊方式調整記憶體。調整為模除64等于0的位址上。
char * const start = ACE_ptr_align_binary (mb->base (),
ACE_CDR::MAX_ALIGNMENT);
#else
……
}
使用一段簡單的代碼可以測試發現這個問題。
char *tmp_buffer = new char [2048];
//使用一個無法對齊的參數作為ACE_InputCDR,ACE_OutputCDR的參數位址,
char *tmp_data = tmp_buffer +1;
// output_cdr調整了對齊的起始位址為8位元組的預設
ACE_OutputCDR output_cdr(tmp_data,512);
ACE_InputCDR input_cdr(tmp_data,512);
ACE_CDR::ULong cdr_long = 123;
bool bret =false;
//
bret = output_cdr.write_ulong(cdr_long);
// cdr_long 不等于123,而是一個錯誤無效資料。
bret = input_cdr.read_ulong(cdr_long);
其實如果編解碼的BUFF都采用相同的對齊方式,那麼理論上也不應該出現問題,最多是出現為了對齊而進行填補的空隙,但是這樣能帶來CPU的效率提升,也是好事。但是由于ACE_OutputCDR的一個位址調整。卻可能導緻編解碼的BUFFER不一緻,我不能肯定這到底是一個錯誤還是作者有他自己的考慮。
這個問題到5.6.1還存在。我已經送出了問題報告。
當然有一個方法解決這個問題。就是定義宏ACE_CDR_IGNORE_ALIGNMENT【注】,隻要定義了這個宏,ACE就不會使用位元組對齊處理CDR編碼。使用這個方法的,編碼占用空間會壓縮一些,但效率上可能低一點(其實未必,因為為了位元組對齊還要耗費一些計算時間),
【注】ACE不知道為什麼在代碼中使用兩個不使用位元組對齊的宏,一個是在CDR_Base.h CDR_Base.cpp 檔案中使用的是ACE_CDR_IGNORE_ALIGNMENT,在CDR_Stream.cpp和CDR_Stream.h檔案上使用的宏ACE_LACKS_CDR_ALIGNMENT。
我一般将兩個宏都定義上。
13 盡量使用STL而不是ACE的容器
這個純屬個人感覺(偏見)。我有如下理由不使用ACE的容器:
l 一些實作不符合大家對于容器的認識,比如ACE_DLList,在其中存放的居然是對象的指針而不是拷貝。你還必須記住去釋放ACE_DLList内部管理的指針。
l ACE容器的疊代器不符合STL的要求,進而造成ACE的容器無法使用STL的各種模闆算法和函數。總不能因為ACE容器失去STL算法這片森林吧。
l 現在的編譯器上已經非常普遍實作了STL,想找一個還不支援STL的編譯器應該都不容易了。
l ACE的容器中間有大量指針,是以ACE的容器也不可能用在共享記憶體中。其的應用場景和STL沒有本質差別。
ACE的文檔《The.ACE.Programmers.Guide》中間也說過:
That being said, the standard C++ containers are recommended for application development when you are using ACE.
是以在可以使用STL的情況下,還是優先使用STL。
14 ACE的日志的不如意
ACE的日志部分是一個非常漂亮的實作,在多線程和多程序模型下都能較好的效率和安全使用。但是卻又少量的不足,讓人意猶未盡。
14.1 無法替換的時間戳格式
ACE日志對于時間戳的格式是固定的,采用的是格式,這個格式在西方人看起來估計還比較順眼,在東方人眼中卻不如人意。更好的方式當然是時間戳的函數可以重載。或者用函數對象(指針)作為參數傳入。
雖然這部分代碼可以重載解決這個問題,但是要大動幹戈隻修正這個問題感覺卻又不值得的。
14.2 日志政策的初始化方式别扭
ACE提供了一個日志政策類ACE_Logging_Strategy輔助大家定義日志政策。但是他的初始化參數卻是指令行參數,而不是變量參數。
int
ACE_Logging_Strategy::init (int argc, ACE_TCHAR *argv[])
你必須使用這樣的指令行去初始化日志政策子產品。
-m1024 -N10 -fSTDERR|OSTREAM -s../log/c4ad.log
試問有幾個伺服器的開發人員會将這些日志政策的初始化放到指令行參數上去。
14.3 沒有按天(時間)分割日志檔案的方式
ACE_Logging_Strategy的日志檔案的分割政策采用的是按照檔案大小分割檔案,檔案的序号采用滾動的,但這種日志分割方式無法根據檔案時間了解日志内容,(由于檔案序号要滾動,序号檔案的最後修改時間都一樣),你隻能grep所有的日志尋找你要的内容。
而在我看來,最好日志分割方式肯定是按照日期進行分割日志檔案。每天建立一個新的日志檔案,可以友善分割日志。清理和管理的工作量大大降低。
14.4 日志槽的方式
ACE_Logging_Strategy采用的是日志槽的方式Enable或者Disable某些級别的日志。但是感覺多少有點不自然的,ACE自己的日志級别本身就是分級的。個人感覺應該是如果日志輸出的日志級别大于定義的級别就能輸出應該是一個更好的選擇。
解決ACE_Logging_Strategy的問題最好的辦法還是擴充這個類。實作自己的日志政策類。
15 ACE_Time_Value的指派效率
ACE_Time_Value是使用ACE會大量使用類。但是他的部分函數沒有高效的實作。比如構造函數:
ACE_INLINE
ACE_Time_Value::ACE_Time_Value (time_t sec, suseconds_t usec)
和set函數
ACE_INLINE void
ACE_Time_Value::set (time_t sec, suseconds_t usec)
為了規範使用者的指派,在這些函數的最後都會調用normalize函數。
void ACE_Time_Value::normalize (void)
但如果你的指派的微秒數值不合适(過大)時,normalize卻不是一個高效實作。下面簡單摘取normalize的一段代碼。
void
ACE_Time_Value::normalize (void)
{
//如果指派的大于微秒數值大于1s。
if (this->tv_.tv_usec >= ACE_ONE_SECOND_IN_USECS)
{
//作者都認為這個代碼要優化
//那麼進入循環,每次減去1000000的微秒機關,在秒的機關+1,上帝呀。
do
{
++this->tv_.tv_sec;
this->tv_.tv_usec -= ACE_ONE_SECOND_IN_USECS;
}
while (this->tv_.tv_usec >= ACE_ONE_SECOND_IN_USECS);
}
…………
}
很不了解為什麼會寫成如此的低效。為什麼不直接使用除法呢,我很不了解。是以如果你在代碼的主循環中如果使用了ACE_Time_Value,使用上面的那些函數就可能掉入陷阱。
解決方法是盡量使用函數sec和usec指派,這些函數不會調用normalize,這兩個函數會直接指派。如果非要使用上面的那些函數方式,也一定不要使用過大的(錯誤的)時間參數。
這個問題到5.6.1還沒有得到修正。
16 非阻塞網絡函數封裝不一緻
ACE的非阻塞網絡函數參數設計有不合理的地方。ACE_SOCK_Stream和ACE_SOCK_Connector在非阻塞的的調用的接口對于ACE_Time_Value *timeout參數的使用不一緻,一個要使用NULL,一個卻要使用ACE_Time_Value::zero。
ACE_SOCK_Stream,非阻塞調用send函數的時候【注】,timeout參數必須填寫為NULL。它最後調用的是ACE::send。将ACE_Time_Value填寫為ACE_Time_Value::zero (0,0)是不行的。如果填寫ACE_Time_Value::zero,會大大降低這個非阻塞調用的性能。
ssize_t
ACE::send (ACE_HANDLE handle,
const void *buf,
size_t n,
int flags,
const ACE_Time_Value *timeout)
{
if (timeout == 0)
return ACE_OS::send (handle, (const char *) buf, n, flags);
else
{
…………
}
}
timeout);
注意使用非阻塞的的IO要調用recv,send函數,而不要調用recv_n,send_n這些函數接口,這些函數接口如果timeout參數傳遞NULL,表示阻塞。
另外非阻塞IO還是要自己設定Socket的選項。
但是ACE_SOCK_Connector卻采用另外一個封裝方式,其是傳入一個NULL表示阻塞,而傳入ACE_Time_Value::zero (0,0)表示進行非阻塞連結操作。
* @param timeout Pointer to an @c ACE_Time_Value object with amount
* of time to wait to connect. If the pointer is 0
* then the call blocks until the connection attempt
* is complete, whether it succeeds or fails. If
* *timeout == {0, 0} then the connection is done
* using nonblocking mode. In this case, if the
* connection can't be made immediately, this method
* returns -1 and errno == EWOULDBLOCK.
int connect (ACE_SOCK_Stream &new_stream,
const ACE_Addr &remote_sap,
const ACE_Time_Value *timeout = 0,
const ACE_Addr &local_sap = ACE_Addr::sap_any,
int reuse_addr = 0,
int flags = 0,
int perms = 0,
int protocol = 0);
大家在處理這些IO時務必當心。
17 過于前衛的Makefile方式
這個”陷阱”的說法有點吹毛求疵,ACE提供了一種很前衛的Makefile方式,他定義了Makefile的基礎變量,以及包括規則。如果使用他來輔助Makefile的書寫,特别是在跨平台開發中,你可以大大節省Makefile開發時間。
BIN = hello_ace
BUILD = $(VBIN)
SRC = $(addsuffix .cpp,$(BIN))
LIBS = -lMyOtherLib
LDFLAGS = -L$(PROJ_ROOT)/lib
#---------------------------------------------------
#Include macros and targets
#---------------------------------------------------
include $(ACE_ROOT)/include/makeinclude/wrapper_macros.GNU
include $(ACE_ROOT)/include/makeinclude/macros.GNU
include $(ACE_ROOT)/include/makeinclude/rules.common.GNU
include $(ACE_ROOT)/include/makeinclude/rules.nonested.GNU
include $(ACE_ROOT)/include/makeinclude/rules.bin.GNU
include $(ACE_ROOT)/include/makeinclude/rules.local.GNU
但是麻煩就在于ACE的這些Makefile方法幾乎沒有一個文檔幫助說明,我一直無法了解$VBIN到底是什麼。這也許,另外,定義到規則這一層也大大限制了大家對Makefile的擴充能力。這就有一點點高不成低不就的味道了,Makefile的新手幾乎不可能了解ACE的Makefile,老手又會因為特殊的需求得不到滿足而躊躇。而我個人一般隻使用ACE定義的Makefile變量。這些變量大部分在wrapper_macros.GNU,platform_macros.GNU
表2 ACE Mafile的變量定義
變量
描述
AR
ar 指令的名字
ARFLAGS
ar 的參數
CC
C編譯器的指令的
CXX
C++編譯器的指令
RC
資源編譯器指令的名字
COMPILE.c
編譯C檔案的指令行, 一般為(CC) $(CFLAGS) $(CPPFLAGS) -c
COMPILE.cc
編譯C++檔案的指令行,一般為(CXX) $(CCFLAGS) $(CPPFLAGS) $(PTDIRS) –c
COMPILEESO.cc
$(CXX) $(CCFLAGS) $(CPPFLAGS) $(PTDIRS),沒太搞明白,不知道為什麼和SO有關,好像是為了修正錯誤增加的。不理也罷
CPPFLAGS
C,C++語言編譯的預标志,比如DEFINDE等. CPPFLAGS += $(DEFFLAGS) $(INCLDIRS)
CFLAGS
C語言編譯選項
CCFLAGS
C++語言編譯選項
DCFLAGS
Debugging 程式的C語言編譯選項,一般在有debug=1變量時有效
DCCFLAGS
Debugging 程式的C++語言編譯選項,一般在有debug=1變量時有效
DEFFLAGS
C++ 預處理的DEFINE部分
DLD
dynamic linker 動态庫link指令的名字,
LD
linker 指令的名字
IDL
CORBA IDL compiler 指令的名字
INCLDIRS
INCLUDE的頭檔案
LDFLAGS
ld linker flags
LINK.c
連結C檔案的指令行
LINK.cc
連結C++檔案的指令行,一般為(PURELINK) $(PRELINK) $(LD) $(CCFLAGS) $(CPPFLAGS) $(PTDIRS)
MAKEFLAGS
Flags that are passed into the compilation from the commandline
OCFLAGS
Optimizing 程式的C語言編譯選項
OCCFLAGS
Optimizing 程式的C++語言編譯選項
PIC
PIC就是position independent code
PCFLAGS
profiling 程式的C語言編譯選項 profiling是什麼不要問我。
PCCFLAGS
profiling 程式的C++語言編譯選項
PRELINK
LINK之前執行的指令
PURELINK
purify 執行的指令,purify是什麼不要問我。
PWD
得到目前目錄的指令
PTDIRS
模闆檔案的路徑定義
RM
删除工具的指令
ACE_MKDIR
遞歸建立的目錄
SOFLAGS
生成.so庫時候的參數
SOLINK.cc
生成.so庫時候的指令行
VAR
Variant identifier suffix
VDIR
Directory for object code .obj/
VSHDIR
Directory for shared object code .shobj/
看起來變量很多,其實要記住和使用的可以很少,你需要留意的主要是.cc結尾的變量就可以了。我們可以使用ACE MakreFile的變量,友善我們的Makefile開發。比如:
我的Makefile,就使用了$(LINK.cc), $(COMPILE.cc)兩個宏。
#使用ACE的wrapper_macros.GNU的定義變量
include $(ACE_ROOT)/include/makeinclude/wrapper_macros.GNU
#得到C,CPP檔案的清單
SRC_FILE = $(wildcard . //作者都認為這個代碼要優化 //那麼進入循環,每次減去1000000的微秒機關,在秒的機關+1,上帝呀。do { ++this->tv_.tv_sec; this->tv_.tv_usec -= ACE_ONE_SECOND_IN_USECS; } while (this->tv_.tv_usec >= ACE_ONE_SECOND_IN_USECS); } …………}
複制代碼
很不了解為什麼會寫成如此的低效。為什麼不直接使用除法呢,我很不了解。是以如果你在代碼的主循環中如果使用了ACE_Time_Value,使用上面的那些函數就可能掉入陷阱。
解決方法是盡量使用函數sec和usec指派,這些函數不會調用normalize,這兩個函數會直接指派。如果非要使用上面的那些函數方式,也一定不要使用過大的(錯誤的)時間參數。
這個問題到5.6.1還沒有得到修正。
16 非阻塞網絡函數封裝不一緻
ACE的非阻塞網絡函數參數設計有不合理的地方。ACE_SOCK_Stream和ACE_SOCK_Connector在非阻塞的的調用的接口對于ACE_Time_Value *timeout參數的使用不一緻,一個要使用NULL,一個卻要使用ACE_Time_Value::zero。
ACE_SOCK_Stream,非阻塞調用send函數的時候【注】,timeout參數必須填寫為NULL。它最後調用的是ACE::send。将ACE_Time_Value填寫為ACE_Time_Value::zero (0,0)是不行的。如果填寫ACE_Time_Value::zero,會大大降低這個非阻塞調用的性能。
ssize_tACE::send (ACE_HANDLE handle, const void *buf, size_t n, int flags, const ACE_Time_Value *timeout){ if (timeout == 0) return ACE_OS::send (handle, (const char *) buf, n, flags); else { ………… }} timeout);
複制代碼
注意使用非阻塞的的IO要調用recv,send函數,而不要調用recv_n,send_n這些函數接口,這些函數接口如果timeout參數傳遞NULL,表示阻塞。
另外非阻塞IO還是要自己設定Socket的選項。
但是ACE_SOCK_Connector卻采用另外一個封裝方式,其是傳入一個NULL表示阻塞,而傳入ACE_Time_Value::zero (0,0)表示進行非阻塞連結操作。
* @param timeout Pointer to an @c ACE_Time_Value object with amount
* of time to wait to connect. If the pointer is 0 * then the call blocks until the connection attempt * is complete, whether it succeeds or fails. If * *timeout == {0, 0} then the connection is done * using nonblocking mode. In this case, if the * connection can't be made immediately, this method * returns -1 and errno == EWOULDBLOCK. int connect (ACE_SOCK_Stream &new_stream, const ACE_Addr &remote_sap, const ACE_Time_Value *timeout = 0, const ACE_Addr &local_sap = ACE_Addr::sap_any, int reuse_addr = 0, int flags = 0, int perms = 0, int protocol = 0);
複制代碼
大家在處理這些IO時務必當心。
17 過于前衛的Makefile方式
這個”陷阱”的說法有點吹毛求疵,ACE提供了一種很前衛的Makefile方式,他定義了Makefile的基礎變量,以及包括規則。如果使用他來輔助Makefile的書寫,特别是在跨平台開發中,你可以大大節省Makefile開發時間。
BIN = hello_aceBUILD = $(VBIN)SRC = $(addsuffix .cpp,$(BIN))LIBS = -lMyOtherLibLDFLAGS = -L$(PROJ_ROOT)/lib#---------------------------------------------------#Include macros and targets#---------------------------------------------------include $(ACE_ROOT)/include/makeinclude/wrapper_macros.GNUinclude $(ACE_ROOT)/include/makeinclude/macros.GNUinclude $(ACE_ROOT)/include/makeinclude/rules.common.GNUinclude $(ACE_ROOT)/include/makeinclude/rules.nonested.GNUinclude $(ACE_ROOT)/include/makeinclude/rules.bin.GNUinclude $(ACE_ROOT)/include/makeinclude/rules.local.GNU
複制代碼
但是麻煩就在于ACE的這些Makefile方法幾乎沒有一個文檔幫助說明,我一直無法了解$VBIN到底是什麼。這也許,另外,定義到規則這一層也大大限制了大家對Makefile的擴充能力。這就有一點點高不成低不就的味道了,Makefile的新手幾乎不可能了解ACE的Makefile,老手又會因為特殊的需求得不到滿足而躊躇。而我個人一般隻使用ACE定義的Makefile變量。這些變量大部分在wrapper_macros.GNU,platform_macros.GNU
表2 ACE Mafile的變量定義
變量
描述
AR
ar 指令的名字
ARFLAGS
ar 的參數
CC
C編譯器的指令的
CXX
C++編譯器的指令
RC
資源編譯器指令的名字
COMPILE.c
編譯C檔案的指令行, 一般為(CC) $(CFLAGS) $(CPPFLAGS) -c
COMPILE.cc
編譯C++檔案的指令行,一般為(CXX) $(CCFLAGS) $(CPPFLAGS) $(PTDIRS) –c
COMPILEESO.cc
$(CXX) $(CCFLAGS) $(CPPFLAGS) $(PTDIRS),沒太搞明白,不知道為什麼和SO有關,好像是為了修正錯誤增加的。不理也罷
CPPFLAGS
C,C++語言編譯的預标志,比如DEFINDE等. CPPFLAGS += $(DEFFLAGS) $(INCLDIRS)
CFLAGS
C語言編譯選項
CCFLAGS
C++語言編譯選項
DCFLAGS
Debugging 程式的C語言編譯選項,一般在有debug=1變量時有效
DCCFLAGS
Debugging 程式的C++語言編譯選項,一般在有debug=1變量時有效
DEFFLAGS
C++ 預處理的DEFINE部分
DLD
dynamic linker 動态庫link指令的名字,
LD
linker 指令的名字
IDL
CORBA IDL compiler 指令的名字
INCLDIRS
INCLUDE的頭檔案
LDFLAGS
ld linker flags
LINK.c
連結C檔案的指令行
LINK.cc
連結C++檔案的指令行,一般為(PURELINK) $(PRELINK) $(LD) $(CCFLAGS) $(CPPFLAGS) $(PTDIRS)
MAKEFLAGS
Flags that are passed into the compilation from the commandline
OCFLAGS
Optimizing 程式的C語言編譯選項
OCCFLAGS
Optimizing 程式的C++語言編譯選項
PIC
PIC就是position independent code
PCFLAGS
profiling 程式的C語言編譯選項 profiling是什麼不要問我。
PCCFLAGS
profiling 程式的C++語言編譯選項
PRELINK
LINK之前執行的指令
PURELINK
purify 執行的指令,purify是什麼不要問我。
PWD
得到目前目錄的指令
PTDIRS
模闆檔案的路徑定義
RM
删除工具的指令
ACE_MKDIR
遞歸建立的目錄
SOFLAGS
生成.so庫時候的參數
SOLINK.cc
生成.so庫時候的指令行
VAR
Variant identifier suffix
VDIR
Directory for object code .obj/
VSHDIR
Directory for shared object code .shobj/
看起來變量很多,其實要記住和使用的可以很少,你需要留意的主要是.cc結尾的變量就可以了。我們可以使用ACE MakreFile的變量,友善我們的Makefile開發。比如:
我的Makefile,就使用了$(LINK.cc), $(COMPILE.cc)兩個宏。
#使用ACE的wrapper_macros.GNU的定義變量include $(ACE_ROOT)/include/makeinclude/wrapper_macros.GNU #得到C,CPP檔案的清單SRC_FILE = $(wildcard .#else#define ACE_DEBUG(X) / do { / ACE_Log_Msg *ace___ = ACE_Log_Msg::instance (); / ace___->log X; / } while (0)#endif//使用執行個體,ACE_DEBUG((LM_ERROR,"i=%d./n",i++));
複制代碼
比較起來,對于Windows下的TRACE宏的定義如下:
#ifdef _DEBUG#define TRACE ATLTRACE#else#define TRACE __noop #endif
複制代碼
而ACE_DEBUG的定義比TRACE的定義是多一層(X)的,是以你必須寫兩層括号,ACE實際上将内層括号的内容全部作為宏參數使用了。
我曾經對這兩層括号疑惑了很久。因為我覺得可以采用其他方法繞開兩個括号,(你可以寫一個日志類嘗試一下)
#if defined (ACE_NLOGGING)// 直接定義為一個函數的名字,當然這兒還要改寫其他的很多代碼#define Z_DEBUG ACE_Log_Msg::instance ()->log #else#define Z_DEBUG#endif
複制代碼
這樣的在沒有定義ACE_NLOGGING的時候,Z_DEBUG(LM_ERROR,"i=%d./n",i++);會被替換成,(LM_ERROR,"i=%d./n",i++),這樣也不會有任何輸出效果。
直到有一次發現GCC2.9的環境下編譯類似代碼,GCC會對這樣的代碼會産生告警,我大緻明白了ACE_DEBUG設計者的苦衷。隻有雙層括号的方法才能徹底讓這行代碼不起任何告警。
另外使用兩層括号也有性能上的好處,大家注意代碼被替換成(LM_ERROR,"i=%d./n",i++)後,i++的代碼還是要執行,在我自己測試中,即使是在GCC的O3級别的優化編譯中,這樣的代碼也不會被優化掉。而如果采用ACE_DEBUG的設計,統一替換為do {} while (0),這行代碼則必然将被優化掉。而對于MSVC的編譯器,他提供一個特别的辨別符__noop幫助編譯器優化。
21 總結和如何用好ACE21.1 實踐,不斷嘗試
大學畢業生中能成為好的程式員絕對不是純粹考試得高分死記公式拿獎學金的同學 ,而是那些熬夜寫代碼的狂人,哈哈。
計算機是一門實踐科學,你隻有不斷嘗試才能進步。
21.2 閱讀的ACE代碼
好像是Linus(雖然他好像有點抵觸C++,哈哈),好像是Linus Torvalds在回答一個提問者時說:“請去閱讀我的代碼”。了解一個實作,發現問題的最好方式還是閱讀源代碼。代碼面前,了無秘密。
當然ACE的代碼閱讀起來不是一件那麼舒心的事情。開發者們采用的是一些非常傳統的UNIX習慣,比如對齊方式采用2個空格縮進,單行if語句不用{}包含,稍顯奇特的inc檔案方式,另外,為了支援跨平台特性,ACE的代碼用了大量的宏。這都無疑增加了閱讀的難度。不過總體說了,ACE的代碼比較起Linux核心代碼和很多其他類庫的代碼還是好的多,至少注釋很清晰,而且Doxgen生産的文檔很酷,也夠用。
21.3 了解作業系統和平台特性
由于ACE是一個跨平台實作。如果你了解平台的實作。不光你閱讀代碼的速度會快很多,也會讓你對實作的困惑就會越少,讓你的代碼避開效率的陷阱,你的實作就會越高效。
21.4 好好學習C++
不需要OO的封裝,不用美妙的設計模式,沒有對效率的執着追求,沒有驚豔的範化設計,用C++幹什麼?但沒有這些信仰,也就不會有ACE,而且沒有這些信仰要程式員做什麼?
21.5 慎用高階特性
在ACE的使用過程中,發現ACE的主要問題出在一些高階實作上。是以如果你要使用高階特性最好能了解背後的實作。
21.6 為ACE作出貢獻
多用ACE,将發現的問題回報給ACE的開發者和ACE社群。
22 後記22.1 作者介紹
筆名:雁渡寒潭([email protected])
曾星 騰訊公司互動娛樂背景開發程式員,目前從事遊戲背景設計開發
個人興趣範圍:大規模分布系統的架構設計,高容量,大壓力的伺服器設計;跨平台開發;資料庫的設計,原理和調優;多核(CPU)環境下的程式設計;OO和設計模式;C++和STL以及模闆,ACE。歡迎大家交流。
22.2 參考文檔
表3 參考的文檔
參考書目
作者/譯者
說明
《C++ Network Programming Volume 1_Mastering Complexity With ACE and Patterns》
Douglas C. Schmidt, Stephen D. Huston
很多問題在這本書的副欄都有描述,如果你看的很認真,也許不會想我這樣碰暗礁。
《C++網絡程式設計卷1:運用ACE和模式消除複雜性》
於春景
《C++ Network Programming Volume 2 - Systematic Reuse with ACE and Frameworks》
Douglas C. Schmidt, Stephen D. Huston
很多問題在這本書的副欄都有描述,如果你看的很認真,也許不會想我這樣碰暗礁。
《C++網絡程式設計,卷2,基于ACE和架構的系統化複用》
馬維達
《The.ACE.Programmers.Guide》
Stephen D. Huston, James CE Johnson, Umar Syyid
《ACE程式員指南》
馬維達
《ACE自适配通信環境中文技術文檔》
馬維達
ACE html
ACE用Doxgen自動生成的文檔
22.3 文章說明和版權聲明
此文檔是耗費兩年時間總結一些自己在使用ACE的7年中發現的一些問題,在湊夠了20個标題後才進行釋出。後面也許會根據自己的一些新的發現修正補充一下文檔,也許。
本着自由的精神,閱讀者可以無須授權就可以自由的轉載這個文檔,我隻保留作者的署名權利,也就是說,你轉載隻需保留這段說明和文檔的完整性(但你不能修改這個文檔,謝謝)。
這篇文檔也是為了回饋一下這些年來為自由軟體奮鬥的人,也謝謝周圍陪我一起玩ACE的Rong,Sonicmao,Awayfang等兄弟們。最後感謝一下Annie,她忍受了我整理文檔而不陪她看電視。