什么是共享內存
共享內存是最快速的進程間通信機制。操作系統在幾個進程的地址空間上映射一段內存,然后這幾個進程可以在不需要調用操作系統函數的情況下在那段內存上進行讀/寫操作。但是,在進程讀寫共享內存時,我們需要一些同步機制。
考慮一下服務端進程使用網絡機制在同一台機器上發送一個HTML文件至客戶端將會發生什么:
- 服務端必須讀取這個文件至內存,然后將其傳至網絡函數,這些網絡函數拷貝那段內存至操作系統的內部內存。
- 客戶端使用那些網絡函數從操作系統的內部內存拷貝數據至它自己的內存。
如上所示,這里存在兩次拷貝,一次是從內存至網絡,另一次是從網絡至內存。這些拷貝使用操作系統調度,這往往開銷比較大。共享內存避免了這種開銷,但是我們需要在進程間同步:
- 服務端映射一個共享內存至其地址空間,並且獲取同步機制。服務端使用同步機制獲取對這段內存的獨占訪問,並且拷貝文件至這段內存中。
- 客戶端映射這個共享內存至其地址空間。等待服務端釋放獨占訪問,然后使用數據。
使用共享內存,我們能夠避免兩次數據拷貝,但是我們必須同步對共享內存段的訪問。
創建能在進程間共享的內存片段
為了使用共享內存,我們必須執行兩個基本步驟:
- 向操作系統申請一塊能在進程間共享的內存。使用者能夠使用共享內存對象創建/銷毀/打開這個內存:一個代表內存的對象,這段內存能同時被映射至多個進程的地址空間。
- 將這個內存的部分或全部與被調用進程的地址空間聯系起來。操作系統在被調用進程的地址空間上尋找一塊足夠大的內存地址范圍,然后將這個地址范圍標記為特殊范圍。在地址范圍上的變化將會被另一個映射了同樣的共享內存對象的進程自動監測到。
一旦成功完成了以上兩步,進程可以開始在地址空間上讀寫,然后與另一個進程發送和接收數據。現在,我們看看如何使用Boost.Interprocess做這些事:
頭文件
為了管理共享內存,你需要包含下面這個頭文件:
- #include <boost/interprocess/shared_memory_object.hpp>
創建共享內存片段
如上述,我們必須使用類 shared_memory_object 來創建、打開和銷毀能被幾個進程映射的共享內存段。我們可以指定共享內存對象的訪問模式(只讀或讀寫),就好像它是一個文件一樣:
- 創建共享內存段。如果已經創建了,會拋異常:
- using boost::interprocess;
- shared_memory_object shm_obj
- (create_only //only create
- ,"shared_memory" //name
- ,read_write //read-write mode
- );
- 打開或創建一個共享內存段:
- using boost::interprocess;
- shared_memory_object shm_obj
- (open_or_create //open or create
- ,"shared_memory" //name
- ,read_only //read-only mode
- );
- 僅打開一個共享內存段。如果不存在,會拋異常:
- using boost::interprocess;
- shared_memory_object shm_obj
- (open_only //only open
- ,"shared_memory" //name
- ,read_write //read-write mode
- );
當一個共享內存對象被創建了,它的大小是0。為了設置共享內存的大小,使用者需在一個已經以讀寫方式打開的共享內存中調用truncate 函數:
shm_obj.truncate(10000);
因為共享內存具有內核或文件系統持久化性質,因此用戶必須顯式銷毀它。如果共享內存不存在、文件被打開或文件仍舊被其他進程內存映射,則刪除操作可能會失敗且返回false:
- using boost::interprocess;
- shared_memory_object::remove("shared_memory");
更多關於shared_memory_object的詳情,請參考 boost::interprocess::shared_memory_object。
映射共享內存片段
一旦被創建或打開,一個進程必須映射共享內存對象至進程的地址空間。使用者可以映射整個或部分共享內存。使用類mapped_region完成映射過程。這個類代表了一個內存區域,這個內存區域已經被從共享內存或其他映射兼容的設備(例如,文件)映射。一個mapped_region能從任何memory_mappable對象創建,所以如你想象,shared_memory_object就是一個memory_mappable對象:
- using boost::interprocess;
- std::size_t ShmSize = ...
- //Map the second half of the memory
- mapped_region region
- ( shm //Memory-mappable object
- , read_write //Access mode
- , ShmSize/2 //Offset from the beginning of shm
- , ShmSize-ShmSize/2 //Length of the region
- );
- //Get the address of the region
- region.get_address();
- //Get the size of the region
- region.get_size();
使用者可以從可映射的對象中指定映射區域的起始偏移量以及映射區域的大小。如果未指定偏移量或大小,則整個映射對象(在此情況下是共享內存)被映射。如果僅指定了偏移量而沒有指定大小,則映射區域覆蓋了從偏移量到可映射對象結尾的整個區域。
更多關於mapped_region的詳情,請參考 boost::interprocess::mapped_region。
一個簡單的例子
讓我們看看一個簡單的使用共享內存的例子。一個服務端進程創建了一個共享內存對象,映射它並且初始化所有字節至同一個值。之后,客戶端進程打開共享內存,映射它並且檢查數據是不是被正確的初始化了。
- #include <boost/interprocess/shared_memory_object.hpp>
- #include <boost/interprocess/mapped_region.hpp>
- #include <cstring>
- #include <cstdlib>
- #include <string>
- int main(int argc, char *argv[])
- {
- using namespace boost::interprocess;
- if(argc == 1){ //Parent process
- //Remove shared memory on construction and destruction
- struct shm_remove
- {
- shm_remove() { shared_memory_object::remove("MySharedMemory"); }
- ~shm_remove(){ shared_memory_object::remove("MySharedMemory"); }
- } remover;
- //Create a shared memory object.
- shared_memory_object shm (create_only, "MySharedMemory", read_write);
- //Set size
- shm.truncate(1000);
- //Map the whole shared memory in this process
- mapped_region region(shm, read_write);
- //Write all the memory to 1
- std::memset(region.get_address(), 1, region.get_size());
- //Launch child process
- std::string s(argv[0]); s += " child ";
- if(0 != std::system(s.c_str()))
- return 1;
- }
- else{
- //Open already created shared memory object.
- shared_memory_object shm (open_only, "MySharedMemory", read_only);
- //Map the whole shared memory in this process
- mapped_region region(shm, read_only);
- //Check that memory was initialized to 1
- char *mem = static_cast<char*>(region.get_address());
- for(std::size_t i = 0; i < region.get_size(); ++i)
- if(*mem++ != 1)
- return 1; //Error checking memory
- }
- return 0;
- }
對沒有共享內存對象的系統進行模擬
Boost.Interprocess在POSIX語義環境下提供了可移植的共享內存。一些操作系統不支持POSIX形式定義的共享內存:
- Windows操作系統提供了使用分頁文件支持內存的共享內存,但是生命周期的意義與POSIX定義得不同(更多詳情,參考原生Windows共享內存章節)。
- 一些UNIX系統不能完全支持POSIX共享內存對象。
在這些平台上,共享內存采用映射文件來模擬。這些映射文件創建在臨時文件夾下的"boost_interprocess"文件夾中。在Windows平台下,如果"Common AppData" 關鍵字出現在注冊表中,"boost_interprocess" 文件夾就創建在那個文件夾下(XP系統通常是"C:\Documentsand Settings\All Users\Application Data" ,Vista則是"C:\ProgramData")。對沒有注冊表項的Windows平台或是Unix系統,共享內存被創建在系統臨時文件夾下("/tmp"或類似)。
由於采用了這種模擬方式,共享內存在部分這些操作系統中具有文件系統生命周期。
刪除共享內存
shared_memory_object提供了一個靜態刪除函數用於刪除一個共享內存對象。
如果共享內存對象不存在或是被另一個進程打開,則函數調用會失敗。需要注意的是這個函數與標准的C函數int remove(constchar *path)類似。在UNIX系統中,shared_memory_object::remove調用shm_unlink:
該函數將刪除名稱所指出的字符串命名的共享內存對象名稱。
- 當斷開連接時,存在一個或多個對此共享內存對象的引用,則在函數返回前,名稱會鮮卑刪除,但是內存對象內容的刪除會延遲至所有對共享內存對象的打開或映射的引用被刪除后進行。
- 即使對象在最后一個函數調用后繼續存在,復用此名字將導致創建一個 boost::interprocess::shared_memory_object實例,就好像采用此名稱的共享內存對象不存在一樣(也即,嘗試打開以此名字命名的對象會失敗,並且一個采用此名字的新對象會被創建)。
在Windows操作系統中,當前版本支持對UNIX斷開行為通常可接受的仿真:文件會用一個隨機名字重命名,並被標記以便最后一個打開的句柄關閉時刪除它。
UNIX系統的匿名共享內存
當涉及多個進程時,創建一個共享內存片段並映射它是有點乏味的。當在UNIX系統下進程間通過調用操作系統的fork()聯系時,一個更簡單的方法是使用匿名共享內存。
此特征已使用在UNIX系統中,用於映射設備\ dev\zero或只在POSIX mmap系統調用中使用MAP_ANONYMOUS。
此特征在Boost.Interprocess使用函數anonymous_shared_memory() 進行了重包裝,此函數返回一個mapped_region 對象,此對象承載了一個能夠被相關進程共享的匿名共享內存片段。
以下是例子:
- #include <boost/interprocess/anonymous_shared_memory.hpp>
- #include <boost/interprocess/mapped_region.hpp>
- #include <iostream>
- #include <cstring>
- int main ()
- {
- using namespace boost::interprocess;
- try{
- //Create an anonymous shared memory segment with size 1000
- mapped_region region(anonymous_shared_memory(1000));
- //Write all the memory to 1
- std::memset(region.get_address(), 1, region.get_size());
- //The segment is unmapped when "region" goes out of scope
- }
- catch(interprocess_exception &ex){
- std::cout << ex.what() << std::endl;
- return 1;
- }
- return 0;
- }
一旦片段建立,可以使用fork()調用以便內存區域能夠被用於通信兩個相關進程。
Windows原生共享內存
Windows操作系統也提供了共享內存,但這種共享內存的生命周期與內核或文件系統的生命周期非常不同。這種共享內存在頁面文件的支持下創建,並且當關聯此共享內存的最后一個進程銷毀后它自動銷毀。
基於此原因,若使用本地windows共享內存,則沒有有效的方法去模擬內核或文件系統持久性。Boost.Interprocess使用內存映射文件模擬共享內存。這保證了在POSIX與Windows操作系統間的兼容性。
然而,訪問原生windows共享內存是Boost.Interprocess使用者的一個基本要求,因為他們想訪問由其他進程不使用Boost.Interprocess創建的共享內存。為了管理原生windows共享內存,Boost.Interprocess提供了類windows_shared_memory。
Windows共享內存的創建與可移植的共享內存創建有點不同:當創建對象時,內存片段的大小必須指定,並且不同像共享內存對象那樣使用truncate 方法。
需要注意的是,當關聯共享內存的最后一個對象銷毀后,共享內存會被銷毀,因此原生windows共享內存沒有持久性。原生windows共享內存還有一些其他限制:一個進程能夠打開或映射由其他進程創建的全部共享內存,但是它不知道內存的大小。這種限制是由Windows API引入的,因此使用者在打開內存片段時,必須以某種方式傳輸內存片段的大小給進程。
在服務端和用戶應用間共享內存也是不同的。為了在服務端和用戶應用間共享內存,共享內存的名字必須以全局名空間前綴“Global\\”開頭。這個全局名空間使得多個客戶端會話可以與一個服務端應用程序通信。服務器組件能夠在全局名空間上創建共享內存。然后一個客戶端會話可以使用“Global”前綴打開那個內存。
在全局名空間從一個非0會話上創建共享內存對象是一個需要特權的操作。
我們重復一下在可移植的共享內存對象上使用的例子:一個服務端進程創建了一個共享內存對象,映射它並且初始化所有字節至同一個值。之后,客戶端進程打開共享內存,映射它並且檢查數據是不是被正確的初始化了。需要小心的是,如果在客戶端連接共享內存前,服務端就存在了,則客戶端連接會失敗,因為當沒有進程關聯這塊內存時,共享內存片段會被銷毀。
以下是服務端進程:
- #include <boost/interprocess/windows_shared_memory.hpp>
- #include <boost/interprocess/mapped_region.hpp>
- #include <cstring>
- #include <cstdlib>
- #include <string>
- int main(int argc, char *argv[])
- {
- using namespace boost::interprocess;
- if(argc == 1){ //Parent process
- //Create a native windows shared memory object.
- windows_shared_memory shm (create_only, "MySharedMemory", read_write, 1000);
- //Map the whole shared memory in this process
- mapped_region region(shm, read_write);
- //Write all the memory to 1
- std::memset(region.get_address(), 1, region.get_size());
- //Launch child process
- std::string s(argv[0]); s += " child ";
- if(0 != std::system(s.c_str()))
- return 1;
- //windows_shared_memory is destroyed when the last attached process dies...
- }
- else{
- //Open already created shared memory object.
- windows_shared_memory shm (open_only, "MySharedMemory", read_only);
- //Map the whole shared memory in this process
- mapped_region region(shm, read_only);
- //Check that memory was initialized to 1
- char *mem = static_cast<char*>(region.get_address());
- for(std::size_t i = 0; i < region.get_size(); ++i)
- if(*mem++ != 1)
- return 1; //Error checking memory
- return 0;
- }
- return 0;
- }
如上所示,原生windows共享內存需要同步措施以保證在客戶端登陸前,共享內存不會被銷毀。
XSI共享內存
在許多UNIX系統中,操作系統提供了另外一種共享內存機制,XSI(X/Open系統接口)共享內存段,也即著名的“System V”共享內存。這種共享內存機制非常流行且可移植,並且它不是基於文件映射語義,而是使用特殊函數(shmget, shmat, shmdt, shmctl等等)。
與POSIX共享內存段不同,XSI共享內存段不是由名字標識而是用通常由ftok創建的關鍵字標識。XSI共享內存具有內核生命周期並且必須顯式釋放。XSI共享內存不支持copy-on-write和部分共享內存映射,但它支持匿名共享內存。
Boost.Interprocess提供了簡單的(xsi_shared_memory)和易管理的(managed_xsi_shared_memory)共享內存類來簡化XSI共享內存的使用。它還使用了簡單的xsi_key類來封裝關鍵字構建。
我們再重復一下在可移植的共享內存對象上使用的例子:一個服務端進程創建了一個共享內存對象,映射它並且初始化所有字節至同一個值。之后,客戶端進程打開共享內存,映射它並且檢查數據是不是被正確的初始化了。
以下是服務端進程:
- #include <boost/interprocess/xsi_shared_memory.hpp>
- #include <boost/interprocess/mapped_region.hpp>
- #include <cstring>
- #include <cstdlib>
- #include <string>
- using namespace boost::interprocess;
- void remove_old_shared_memory(const xsi_key &key)
- {
- try{
- xsi_shared_memory xsi(open_only, key);
- xsi_shared_memory::remove(xsi.get_shmid());
- }
- catch(interprocess_exception &e){
- if(e.get_error_code() != not_found_error)
- throw;
- }
- }
- int main(int argc, char *argv[])
- {
- if(argc == 1){ //Parent process
- //Build XSI key (ftok based)
- xsi_key key(argv[0], 1);
- remove_old_shared_memory(key);
- //Create a shared memory object.
- xsi_shared_memory shm (create_only, key, 1000);
- //Remove shared memory on destruction
- struct shm_remove
- {
- int shmid_;
- shm_remove(int shmid) : shmid_(shmid){}
- ~shm_remove(){ xsi_shared_memory::remove(shmid_); }
- } remover(shm.get_shmid());
- //Map the whole shared memory in this process
- mapped_region region(shm, read_write);
- //Write all the memory to 1
- std::memset(region.get_address(), 1, region.get_size());
- //Launch child process
- std::string s(argv[0]); s += " child ";
- if(0 != std::system(s.c_str()))
- return 1;
- }
- else{
- //Build XSI key (ftok based)
- xsi_key key(argv[0], 1);
- //Create a shared memory object.
- xsi_shared_memory shm (open_only, key);
- //Map the whole shared memory in this process
- mapped_region region(shm, read_only);
- //Check that memory was initialized to 1
- char *mem = static_cast<char*>(region.get_address());
- for(std::size_t i = 0; i < region.get_size(); ++i)
- if(*mem++ != 1)
- return 1; //Error checking memory
- }
- return 0;
- }
內存映射文件
什么是內存映射文件
文件映射是一個文件的內容和一個進程的部分地址空間的關聯。系統創建一個文件映射來聯系文件和進程的地址空間。一個映射區域是地址空間的一部分,進程使用這部分來訪問文件的內容。一個單個的文件映射可以有幾個映射區域,以便使用者能關聯文件的多個部分和進程的地址空間,而不要映射整個文件至地址空間,因為文件的大小可能會比整個進程地址空間還大(在通常32位系統下的一個9GB的DVD鏡像文件)。進程使用指針從文件讀寫數據,就好像使用動態內存一樣。文件映射有以下幾個優點:
- 統一資源使用。文件和內存能使用相同的函數來操作。
- 文件數據自動同步以及從操作系統緩存。
- 在文件中復用C++功能(STL容器,算法)。
- 在兩個或多個應用間共享內存。
- 允許高效的處理一個大文件,而不需要將整個文件映射至內存中。
- 如果幾個進程使用同樣的文件映射來創建一個文件的映射區域,每個進程視圖都包含了磁盤上文件的相同副本。
文件映射不僅用於進程間通信,它也能用於簡化文件使用,因此使用者不需要使用文件管理函數來寫文件。使用者僅需將數據寫入進程的內存,然后操作系統將數據轉儲至文件。
當兩個進程在內存中映射了同一份文件,則一個進程用於寫數據的在內存能夠被另外一個進程檢測到,因此內存映射文件能夠被用於進程間通信機制。我們可以認為內存映射文件提供了與共享內存相同的進程間通信機制,並且還具有額外的文件系統持久化性質。然而,因為操作系統必須同步文件內容和內存內容,因此內存映射文件沒有共享內存快。
使用映射文件
為了使用內存映射文件,我們需要執行以下兩個基本步驟:
- 創建一個可映射的對象用來代表文件系統中已經創建的某個文件。這個對象將用於創建此文件的多個映射區域。
- 將整個或部分文件與被調用進程的地址空間關聯。操作系統在被調用進程的地址空間上搜尋一塊足夠大的內存地址范圍,並且標記地址范圍為一個特殊范圍。在地址范圍上的任何改變會自動被另一個映射了同一個文件的進程檢測到,並且這些改變會自動傳輸至磁盤上。
一旦成功完成了以上兩步,進程可以開始在地址空間上讀寫,然后與另一個進程發送和接收數據。同時同步文件內容和映射區域的改變。現在,讓我們一起看看如何用Boost.Interprocess做到這點。
頭文件
為了管理映射文件,你僅需包含如下頭文件:
#include <boost/interprocess/file_mapping.hpp>
創建一個文件映射
首先,我們必須連接一個文件的內容與進程的地址空間。為了做到這點,我們必須創建一個代表那個文件的可映射對象。創建一個文件映射對象在Boost.Interprocess中實現如下:
- using boost::interprocess;
- file_mapping m_file
- ("/usr/home/file" //filename
- ,read_write //read-write mode
- );
現在,我們可以使用新創建的對象來創建內存區域。更多關於這個類的詳情,請參考 boost::interprocess::file_mapping。
映射文件在內存中的內容
當創建了一個文件映射后,一個進程僅需在進程地址空間上映射共享內存。使用者可以映射整個共享內存或僅僅一部分。使用mapped_region類完成映射過程。如前所述,這個類代表了一塊內存區域,此區域映射自共享內存或其他具有映射能力的設備:
- using boost::interprocess;
- std::size_t FileSize = ...
- //Map the second half of the file
- mapped_region region
- ( m_file //Memory-mappable object
- , read_write //Access mode
- , FileSize/2 //Offset from the beginning of shm
- , FileSize-FileSize/2 //Length of the region
- );
- //Get the address of the region
- region.get_address();
- //Get the size of the region
- region.get_size();
使用者可以從可映射的對象中指定映射區域的起始偏移量以及映射區域的大小。如果未指定偏移量或大小,則整個文件被映射。如果僅指定了偏移量而沒有指定大小,則映射區域覆蓋了從偏移量到文件結尾的整個區域。
如果多個進程映射了同一個文件,並某進程修改了也被其他進程映射的一塊內存區域范圍
,則修改馬上會被其他進程檢測到。然后,磁盤上的文件內容不是立即更新的,因為這會影響性能(寫磁盤比寫內存要慢幾倍)。如果使用者想確定文件內容被更新了,他可以刷新視圖的一部分至磁盤。當函數返回后,刷新進程啟動,但是不保證所有數據都寫入了磁盤:
- //Flush the whole region
- region.flush();
- //Flush from an offset until the end of the region
- region.flush(offset);
- //Flush a memory range starting on an offset
- region.flush(offset, size);
記住偏移量不是文件上的偏移量,而是映射區域的偏移量。如果一個區域覆蓋了一個文件的下半部分並且刷新了整個區域,僅文件的這一半能保證被刷新了。
更多關於mapped_region的詳情,可參考 boost::interprocess::mapped_region。
一個簡單的例子
我們賦值在共享內存章節中提到的例子,使用內存映射文件。一個服務端進程創建了一個內存映射文件並且初始化所有字節至同一個值。之后,客戶端進程打開內存映射文件並且檢查數據是不是被正確的初始化了。(譯注:原文此處誤為“共享內存”)
- #include <boost/interprocess/file_mapping.hpp>
- #include <boost/interprocess/mapped_region.hpp>
- #include <iostream>
- #include <fstream>
- #include <string>
- #include <vector>
- #include <cstring>
- #include <cstddef>
- #include <cstdlib>
- int main(int argc, char *argv[])
- {
- using namespace boost::interprocess;
- //Define file names
- const char *FileName = "file.bin";
- const std::size_t FileSize = 10000;
- if(argc == 1){ //Parent process executes this
- { //Create a file
- file_mapping::remove(FileName);
- std::filebuf fbuf;
- fbuf.open(FileName, std::ios_base::in | std::ios_base::out
- | std::ios_base::trunc | std::ios_base::binary);
- //Set the size
- fbuf.pubseekoff(FileSize-1, std::ios_base::beg);
- fbuf.sputc(0);
- }
- //Remove on exit
- struct file_remove
- {
- file_remove(const char *FileName)
- : FileName_(FileName) {}
- ~file_remove(){ file_mapping::remove(FileName_); }
- const char *FileName_;
- } remover(FileName);
- //Create a file mapping
- file_mapping m_file(FileName, read_write);
- //Map the whole file with read-write permissions in this process
- mapped_region region(m_file, read_write);
- //Get the address of the mapped region
- void * addr = region.get_address();
- std::size_t size = region.get_size();
- //Write all the memory to 1
- std::memset(addr, 1, size);
- //Launch child process
- std::string s(argv[0]); s += " child ";
- if(0 != std::system(s.c_str()))
- return 1;
- }
- else{ //Child process executes this
- { //Open the file mapping and map it as read-only
- file_mapping m_file(FileName, read_only);
- mapped_region region(m_file, read_only);
- //Get the address of the mapped region
- void * addr = region.get_address();
- std::size_t size = region.get_size();
- //Check that memory was initialized to 1
- const char *mem = static_cast<char*>(addr);
- for(std::size_t i = 0; i < size; ++i)
- if(*mem++ != 1)
- return 1; //Error checking memory
- }
- { //Now test it reading the file
- std::filebuf fbuf;
- fbuf.open(FileName, std::ios_base::in | std::ios_base::binary);
- //Read it to memory
- std::vector<char> vect(FileSize, 0);
- fbuf.sgetn(&vect[0], std::streamsize(vect.size()));
- //Check that memory was initialized to 1
- const char *mem = static_cast<char*>(&vect[0]);
- for(std::size_t i = 0; i < FileSize; ++i)
- if(*mem++ != 1)
- return 1; //Error checking memory
- }
- }
- return 0;
- }
更多關於映射區域
一個操作它們的類
如我們所見,shared_memory_object和file_mapping objects都能被用於創建mapped_region對象。使用相同的類從共享內存對象或文件映射創建映射區域,這樣有許多優點。
例如,可以在STL容器映射區域混合使用共享內存和內存映射文件。僅依賴於映射區域的庫能夠與共享內存或內存映射文件一起使用,而不需要重新編譯它們。
在數個進程中的映射地址
在我們已經看到的例子中,文件或是共享內存內容被映射到進程的地址空間上,但是地址是由操作系統選擇的。
如果多個進程映射同一個文件或共享內存,映射地址在每個進程中肯定是不同的。因為每個進程都可能在不同的方面使用到了它們的地址空間(例如,或多或少分配一些動態內存),因此不保證文件/共享內存會映射到相同的地址上。
如果兩個進程映射同一個對象到不同的地址上,則在那塊內存上使用指針是無效的,因為指針(一個絕對地址)僅對寫它的進程有意義。解決這個問題的方式是使用對象間的偏移量(距離)而不是指針:如果兩個對象由同一進程位於同樣共享內存片段,在另一個進程中,各對象的地址可能是不同的,但是他們之間的距離(字節數)是相同的。
所以,對映射共享內存或內存映射文件的第一個建議就是避免使用原始指針,除非你了解你做的一切。當一個置於映射區域的對象想指向置於相同映射區域的另一個對象時,使用數據或相對指針間的偏移量來得到指針的功能。Boost.Interprocess提供了一個名為boost::interprocess::offset_ptr 的智能指針,它能安全是使用在共享內存中,並且能用於指向另一個置於同一共享內存/內存映射文件中的對象。
固定地址映射
使用相對指針沒有使用原始指針方便,因此如果一個使用者能夠成功將同樣的文件或共享內存對象映射至兩個進程的相同地址,使用原始指針就是個好主意了。
為了映射一個對象至固定地址,使用者可以在映射區域的構造函數中指定地址:
- mapped_region region ( shm //Map shared memory
- , read_write //Map it as read-write
- , 0 //Map from offset 0
- , 0 //Map until the end
- , (void*)0x3F000000 //Map it exactly there
- );
然而,用戶不能在任何地址上映射這個區域,即使地址未被使用。標記映射區域起點的偏移參數也是被限制的。這些限制將在下一章節解釋。
映射偏移和地址限制
如上述,使用者不能映射可內存映射的對象至任何地址上,但可以指定可映射對象的偏移量為任意值,此可映射對象等同於映射區域的起點。大多數操作系統限制映射地址和可映射對象的偏移量值為頁面大小的倍數。這源於操作系統在整個頁面上執行映射操作的事實。
如果使用了固定的映射地址,參數offset 和address必須為那個值的整數倍。在32位操作系統中,這個值一般為4KB或8KB。
- //These might fail because the offset is not a multiple of the page size
- //and we are using fixed address mapping
- mapped_region region1( shm //Map shared memory
- , read_write //Map it as read-write
- , 1 //Map from offset 1
- , 1 //Map 1 byte
- , (void*)0x3F000000 //Aligned mapping address
- );
- //These might fail because the address is not a multiple of the page size
- mapped_region region2( shm //Map shared memory
- , read_write //Map it as read-write
- , 0 //Map from offset 0
- , 1 //Map 1 byte
- , (void*)0x3F000001 //Not aligned mapping address
- );
因為操作系統在整個頁面上進行映射操作,因此指定一個不是頁面大小整數倍的映射大小或偏移量會浪費更多的資源。如果使用者指定了如下1字節映射:
- //Map one byte of the shared memory object.
- //A whole memory page will be used for this.
- mapped_region region ( shm //Map shared memory
- , read_write //Map it as read-write
- , 0 //Map from offset 0
- , 1 //Map 1 byte
- );
操作系統將保留一整個頁面,並且此頁面不會再被其它映射使用,因此我們將浪費(頁面大小 - 1)字節。如果我們想有效利用系統資源,我們應該創建整數倍於頁面大小的區域。如果使用者為一個有2*頁面大小的文件指定了如下兩個映射區域:
- //Map the first quarter of the file
- //This will use a whole page
- mapped_region region1( shm //Map shared memory
- , read_write //Map it as read-write
- , 0 //Map from offset 0
- , page_size/2 //Map page_size/2 bytes
- );
- //Map the rest of the file
- //This will use a 2 pages
- mapped_region region2( shm //Map shared memory
- , read_write //Map it as read-write
- , page_size/2 //Map from offset 0
- , 3*page_size/2 //Map the rest of the shared memory
- );
此例中,頁面的一半空間浪費在第一個映射中,另一半空間浪費在第二個映射中,因為偏移量不是頁面大小的整數倍。使用最小資源的映射應該是映射整個頁面文件:
- //Map the whole first half: uses 1 page
- mapped_region region1( shm //Map shared memory
- , read_write //Map it as read-write
- , 0 //Map from offset 0
- , page_size //Map a full page_size
- );
- //Map the second half: uses 1 page
- mapped_region region2( shm //Map shared memory
- , read_write //Map it as read-write
- , page_size //Map from offset 0
- , page_size //Map the rest
- );
我們怎么得到頁面大小呢?類mapped_region有一個靜態函數返回頁面大小值:
- //Obtain the page size of the system
- std::size_t page_size = mapped_region::get_page_size();
操作系統可能會限制每個進程或每個系統能使用的映射內存區域的數目。
在映射區域構建對象的限制
當兩個進程為同一個可映射對象創建一個映射區域時,兩個進程可以通過讀寫那塊內存進行通信。某一進程能夠在那塊內存中構建一個C++對象以便另一進程能夠使用它。但是,一塊被多個進程共享的映射區域並不能承載所有其他對象,因為不是所有類都能做為進程共享對象,特別是如果映射區域在各進程中被映射至不同的地址上。
用偏移指針代替原始指針
當放置一個對象至映射區域,並且每個進程映射那塊區域至不同的地址上時,原始指針是個問題,因為它們僅在放置它們的那個進程中有效。未解決此問題,Boost.Interprocess提供了一個特殊的智能指針來替代原始指針。因此,包含原始指針(或是Boost的智能指針,其內部包含了原始指針)的用戶類不能被放置在進程共享映射區域中。如果你想從不同的進程中使用這些共享對象,這些指針必須用偏移指針來放置,並且這些指針必須僅指向放置在同一映射區域的對象。
當然,置於進程間共享的映射區域的指針僅能指向一個此映射區域的對象,指針可以指向一個僅在一個進程中有效的地址,而且其他進程在訪問那個地址時可能會崩潰。
引用限制
引用遇到了與指針同樣的問題(主要是因為它們的行為方式類似指針)。然而,不可能在C++中創建一個完成可行的智能引用(例如,操作符. ()不能被重載)。基於此原因,如果使用者想在共享內存中放置一個對象,此對象不能包含任何(不論智能與否)引用變量做為成員。
引用僅能使用在如下情況,如果映射區域共享一個被映射在所有進程同樣基地址上的內存段。和指針一樣,一個位於某映射區域上的引用僅能指向一個此映射區域中的對象。
虛函數限制
虛函數表指針和虛函數表位於包含此對象的進程地址空間上,所以,如果我們在共享區域放置一個帶虛函數的類或虛基類,則虛指針對其它進程而言是無效的,它們將崩潰。
這個問題解決起來非常困難,因為每個進程都需要不同的虛函數表指針並且包含此指針的對象在許多進程間共享。及時我們在每個進程中映射映射區域至相同的地址,在每個進程中,虛函數表也可能在不同的地址上。為了使進程間共享對象的虛函數能夠有效工作,需要對編譯器做重大改進並且虛函數會蒙受性能損失。這就是為什么Boost.Interprocess沒有任何計划在進程間共享的映射區域上支持虛函數以及虛繼承。
小心靜態成員變量
類的靜態成員是被該類的所有實例共享的全局對象。基於此原因,靜態成員在進程中是做為全局變量對待的。
當構建一個帶靜態變量的類時,每個進程均有靜態變量的副本,因此更新某一進程中靜態變量的值不會改變其在另一個進程中的值。因此請小心使用這些類。如果靜態變量僅僅是進程啟動時就初始化的常量,那它們是沒有危險的,但是它們的值是完全不變的(例如,形如enums使用時)並且它們的值對所有進程均相同。