1. 程式人生 > >Boost.Interprocess使用手冊翻譯之四:在程序間共享記憶體 (Sharing memory between processes)

Boost.Interprocess使用手冊翻譯之四:在程序間共享記憶體 (Sharing memory between processes)

共享記憶體

共享記憶體是最快速的程序間通訊機制。作業系統在幾個程序的地址空間上對映一段記憶體,然後這幾個程序可以在不需要呼叫作業系統函式的情況下在那段記憶體上進行讀/寫操作。但是,在程序讀寫共享記憶體時,我們需要一些同步機制。

考慮一下服務端程序使用網路機制在同一臺機器上傳送一個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");

一旦被建立或開啟,一個程序必須對映共享記憶體物件至程序的地址空間。使用者可以對映整個或部分共享記憶體。使用類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();

使用者可以從可對映的物件中指定對映區域的起始偏移量以及對映區域的大小。如果未指定偏移量或大小,則整個對映物件(在此情況下是共享記憶體)被對映。如果僅指定了偏移量而沒有指定大小,則對映區域覆蓋了從偏移量到可對映物件結尾的整個區域。

讓我們看看一個簡單的使用共享記憶體的例子。一個服務端程序建立了一個共享記憶體物件,對映它並且初始化所有位元組至同一個值。之後,客戶端程序開啟共享記憶體,對映它並且檢查資料是不是被正確的初始化了。

#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"或類似)。

由於採用了這種模擬方式,共享記憶體在部分這些作業系統中具有檔案系統生命週期。

如果共享記憶體物件不存在或是被另一個程序開啟,則函式呼叫會失敗。需要注意的是這個函式與標準的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共享記憶體,則沒有有效的方法去模擬核心或檔案系統永續性。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
   );

當建立了一個檔案對映後,一個程序僅需在程序地址空間上對映共享記憶體。使用者可以對映整個共享記憶體或僅僅一部分。使用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);

記住偏移量不是檔案上的偏移量,而是對映區域的偏移量。如果一個區域覆蓋了一個檔案的下半部分並且重新整理了整個區域,僅檔案的這一半能保證被重新整理了。

我們賦值在共享記憶體章節中提到的例子,使用記憶體對映檔案。一個服務端程序建立了一個記憶體對映檔案並且初始化所有位元組至同一個值。之後,客戶端程序開啟記憶體對映檔案並且檢查資料是不是被正確的初始化了。(譯註:原文此處誤為“共享記憶體”)

#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使用時)並且它們的值對所有程序均相同。