歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> C++容器模板在共享內存中的使用

C++容器模板在共享內存中的使用

日期:2017/3/1 9:33:12   编辑:Linux編程

本文用於探討在共享內存中使用容器的好處,以及幾種在共享內存中C++模板容器的方法。

1 為什麼要在共享內存中使用模板容器?

為什麼要避開普通內存而選擇共享內存,那肯定是使用共享內存的優勢:

  • 共享內存可以在多進程間共享,到達進程間通信的方式。
  • 共享內存可以在進程的生命周期以外仍然存在。這就可以保證在短暫停止服務(服務進程coredump,更新變更)後,服務進程仍然可以繼續使用這些共享內存的數據。

如果這些優勢在加上C++容器模板使用方便,開發快速的優勢,無疑是雙劍合璧,成為服務器開發的利刃。

2 在共享內存中使用模板容器最大難點是?

但如果要要做到讓容器在模板中使用,最大的麻煩是什麼?就是指針。(同步當然也是一個問題,但我這兒強調的是容器的移植)

當然一般而言,這個指針的地址一般還是指向共享內存的內部數據。為什麼不會出現指向各自私有數據的情況?,如果2個進程A,共享的數據裡面的一個指針在進程A表示進程A的私有地址,在進程B裡面標識B的私有地址,這明顯是你邏輯設計有問題。

而內部指針對於我們上面提到的2個優點,其都是天敵。另外也要注意,如果數據需要多進程共享,你的數據也必須是POD的數據,如果有虛表指針,那麼也不可能實現共享。

多進程之間的共用的共享內存,地址很可能是不一樣的。當然共享內存的API上一般都可以建議固定起始地址,但既然是建議,那就可能不遵守,而且這需要你熟悉進程的地址空間分布,而且對於開發者和運維者,一旦使用的共享內存多了,使用固定地址絕對是噩夢。

而對於服務器,上一次映射的地址,有可能和重啟後的映射地址不一致。

//mmap函數的第一個參數,就是建議地址
void *mmap (void *addr,
            size_t len,
            int prot,
            int flags,
            ZEN_HANDLE handle,
            size_t off = 0);
//Window的API的最後一個參數lpBaseAddress也是建議地址。
LPVOID WINAPI MapViewOfFileEx(
  _In_      HANDLE hFileMappingObject,
  _In_      DWORD dwDesiredAccess,
  _In_      DWORD dwFileOffsetHigh,
  _In_      DWORD dwFileOffsetLow,
  _In_      SIZE_T dwNumberOfBytesToMap,
  _In_opt_  LPVOID lpBaseAddress
); 

而對於解決指針這種問題最好的方法(或者說唯一的方法)就是不記錄指針,而記錄相對的偏移地址,所有的計算都根據偏移地址處理。

目前探討在共享內存中使用模板的方法,我見到過的思路和實現大致3種,一種是定制STL的容器內存分配器,一種是ACE提供的使用地址無關的分配方法,一種是BOOST的interprocess的實現,我們分開聊聊這些方法的優點和缺點。

3 定制STL的分配器

如果早年(04年以前)在網上的論壇搜索答案,大部分給的答案是這個,表面看這也是一個比較好和簡單的答案,最大程度的利用STL容器現有的代碼。

template <class _Tp, class _Alloc> class list;

但其實這個答案並不一定靠譜。寫一個共享內存的分配器肯定不是什麼難事。難在如果我們要把容器放入共享內存的那幾個目的,使用STL的容器的實現。為什麼呢,還是因為指針。

首先,很多STL容器實現裡面是有大量指針的,比如list的環形隊列的prev指針和next指針,map底層紅黑樹實現的3個指針,這些在容器內部都是用真正的內存地址表示的。

所以說這個答案完全要看你的STL的內部實現是否有指針,如果有,那基本不可行(當然你把數據放入共享內存是可以的,但你無法共享和重用)。比如SGI的實現和STLport的實現。

4 ACE的與位置無關的分配

我看到的第二個(大約2005年)方法是ACE的,在《ACE Programmer's Guide, The: Practical Design Patterns for Network and Systems Programming》中文名稱《ACE程序員指南》一書中有相應的說明,ACE的方法是提供了一個地址無關化的內存分配器(准確說應該是控制塊ACE_PI_Control_Block),同時提供一個ACE_Based_Pointer_Basic模板來記錄相對地址。而ACE_Based_Pointer_Basic模板其通過重載operator T *()函數達到幾乎和指針一樣的行為(實際會調用addr(),得到真正的地址)。ACE的實現有點意思,我們也費點力氣剖析一下。

如果上圖的例子,進程A,B共享一段共享內存,分別映射在不同的地址上,共享內存中有一個結構S,S中要記錄一個這段共享內存中的另外一個地址char *,結構S可以使用ACE的ACE_Based_Pointer_Basic<char> 記錄這個地址,ACE_Based_Pointer_Basic<char>分別使用2個長度記錄自己到共享內存起始地址的長度,以及需要記錄的地址到共享內存的起始位置的地址。然後兩個進程就都可以通過this指針,和2個偏移長度,計算得到需要記錄的地址。

ACE的實現上,自己的內存分配器記錄了分配的地址空間起始地址和長度,ACE_Based_Pointer_Basic<T>在構造的時候會根據自己的地址判斷自己需要計算的起始地址是什麼。而且在封裝上考慮比較舒服。但需要提醒的是,你需要記錄的地址必須仍然是這塊共享內存上的,否則……(不解釋)。

而且要說明的是ACE的自有容器雖然也支持使用共享內存的分配器,但由於ACE容器的內部也有大量的指針,而不是記錄相對地址,所以ACE的容器其實也不能在共享內存中。所以ACE的學術氣息更加濃烈一點,實用性並不高(ACE的容器本身也不好用)。所以可以說,ACE踹開了那扇門,但並沒有進入這個殿堂。

5 BOOST的interprocess容器內存分配器

這幾年BOOST開始流行,BOOST的interprocess庫中一個在共享內存容器內存分配器的實現,但要注意其配合使用容器vector,list,是BOOST自己的container容器。這個分配器並不能和現有大部分STL實現配合使用。

可以說其實BOOST的實現和ACE的思路是類似的,方法也是分配一個塊共享內存,為這塊共享內存生成一個容器內存分配器,這個分配器為這個容器服務,使用共享內存容器分配器後,容器內部所有的地址記錄相對地址,而不是絕對地址。

template <class PointedType, class DifferenceType, class OffsetType, std::size_t OffsetAlignment>
class offset_ptr

對比ACE,BOOST實現的不同,一方面BOOST的共享內存管理和容器內存分配器的思路很清晰,整體設計思路還是在STL的體系之下,ACE誕生的年代過早,容器整體體系和STL完全不相容,另外一方面,BOOST在相對地址的處理上也簡單一點。他只記錄offset_ptr對象的this地址到需要記錄的地址之間的長度。

另外,BOOST代碼雖然條理上和STL容器一致,但BOOST的代碼閱讀難度至少double於STLPort,傳統調試跟蹤代碼的方式單步跟蹤雖然有效,但是很多變量都無法查詢到實際值,宏滿天飛。期待剖析BOOST代碼的大神出現。

6 共享內存的容量

而且另外一個小問題是,我們申請的共享內存的大小都是有一個大小限度的,而STL容器往往有隨需增長的特點,而這個特點和共享內存其實也有一些不調和性。

ACE的問題在這個問題上給出過一些學術解,依靠信號,異常等方式,給你機會自己擴展內存,但估計也就限於學術探討范圍。

BOOST在這個問題上更加明了,當內存不夠分配的時候拋出bad_alloc異常。反而更加清晰一點。

7 另外一種解思考,固定最大長度的容器

BOOST的實現固然不錯,但也有幾個並不那麼完美的地方,而且當時可以參考的思路還沒有這樣多,(我自己實現自己容器的時候是2005年,BOOST的interprocess的庫在08年才出現)

第一,放入N個T類型的數據的容器到底需要多大共享內存?因為容器本身是有消耗,而這點BOOST並沒有接口告訴我。對於使用共享內存的容器,我們都知道我們需要使用的最大數量是多少。

第二,如果最大的尺寸我們已經知道。那麼其實我們對於所有的可以在一開始就分配好空間,而不是在每次push_back的時候調用alloctor去分配地址,其實alloctor內部仍然使用了紅黑樹去管理所有的分配地址,坦白說麻煩。而且由於最大尺寸固定,我們所有的數據的內部位置關系都可以采用數組下標定位。這樣也就一樣省去了指針的麻煩。

綜合上面的考慮。我們當時的設計思路大致是,根據你傳入的參數判定告知調用者所需的內存大小,調用者自己分配好內存(可以是共享內存),根據分配的內存地址構造一個容器,容器的操作和模板基本一致,也提供迭代器等方法等方法訪問,容器的內部結構如下圖。

這個方法和STL容器的語法基本兼容,性能比BOOST的那個速度應該要快一點(不用在每次都alloc一個node節點)。寸有所長,尺有所短,這也算一個思路把。

具體的代碼以後估計會開源。

8 總結

在共享內存中使用模板容器的關鍵問題是指針的問題,相對地���是解決這個問題比較好的方法。一個比較通用的方案是將所有的指針改成一個相對地址記錄,還有一種思路對於容器的處理方式是將容器的所有數據按最大數量分配好,使用下標處理。

9 參考文檔

《STL源碼剖析》 侯捷

《ACE程序員指南》 馬維達

《BOOST Documentation》

------------------------------分割線------------------------------

C++ Primer Plus 第6版 中文版 清晰有書簽PDF+源代碼 http://www.linuxidc.com/Linux/2014-05/101227.htm

讀C++ Primer 之構造函數陷阱 http://www.linuxidc.com/Linux/2011-08/40176.htm

讀C++ Primer 之智能指針 http://www.linuxidc.com/Linux/2011-08/40177.htm

讀C++ Primer 之句柄類 http://www.linuxidc.com/Linux/2011-08/40175.htm

將C語言梳理一下,分布在以下10個章節中:

  1. Linux-C成長之路(一):Linux下C編程概要 http://www.linuxidc.com/Linux/2014-05/101242.htm
  2. Linux-C成長之路(二):基本數據類型 http://www.linuxidc.com/Linux/2014-05/101242p2.htm
  3. Linux-C成長之路(三):基本IO函數操作 http://www.linuxidc.com/Linux/2014-05/101242p3.htm
  4. Linux-C成長之路(四):運算符 http://www.linuxidc.com/Linux/2014-05/101242p4.htm
  5. Linux-C成長之路(五):控制流 http://www.linuxidc.com/Linux/2014-05/101242p5.htm
  6. Linux-C成長之路(六):函數要義 http://www.linuxidc.com/Linux/2014-05/101242p6.htm
  7. Linux-C成長之路(七):數組與指針 http://www.linuxidc.com/Linux/2014-05/101242p7.htm
  8. Linux-C成長之路(八):存儲類,動態內存 http://www.linuxidc.com/Linux/2014-05/101242p8.htm
  9. Linux-C成長之路(九):復合數據類型 http://www.linuxidc.com/Linux/2014-05/101242p9.htm
  10. Linux-C成長之路(十):其他高級議題

Copyright © Linux教程網 All Rights Reserved