什么是共享内存
共享内存是最快速的进程间通信机制。操作系统在几个进程的地址空间上映射一段内存,然后这几个进程可以在不需要调用操作系统函数的情况下在那段内存上进行读/写操作。但是,在进程读写共享内存时,我们需要一些同步机制。
考虑一下服务端进程使用网络机制在同一台机器上发送一个HTML文件至客户端将会发生什么:
- 服务端必须读取这个文件至内存,然后将其传至网络函数,这些网络函数拷贝那段内存至操作系统的内部内存。
- 客户端使用那些网络函数从操作系统的内部内存拷贝数据至它自己的内存。
如上所示,这里存在两次拷贝,一次是从内存至网络,另一次是从网络至内存。这些拷贝使用操作系统调度,这往往开销比较大。共享内存避免了这种开销,但是我们需要在进程间同步:
- 服务端映射一个共享内存至其地址空间,并且获取同步机制。服务端使用同步机制获取对这段内存的独占访问,并且拷贝文件至这段内存中。
- 客户端映射这个共享内存至其地址空间。等待服务端释放独占访问,然后使用数据。
使用共享内存,我们能够避免两次数据拷贝,但是我们必须同步对共享内存段的访问。
创建能在进程间共享的内存片段
为了使用共享内存,我们必须执行两个基本步骤:
- 向操作系统申请一块能在进程间共享的内存。使用者能够使用共享内存对象创建/销毁/打开这个内存:一个代表内存的对象,这段内存能同时被映射至多个进程的地址空间。
- 将这个内存的部分或全部与被调用进程的地址空间联系起来。操作系统在被调用进程的地址空间上寻找一块足够大的内存地址范围,然后将这个地址范围标记为特殊范围。在地址范围上的变化将会被另一个映射了同样的共享内存对象的进程自动监测到。
一旦成功完成了以上两步,进程可以开始在地址空间上读写,然后与另一个进程发送和接收数据。现在,我们看看如何使用Boost.Interprocess做这些事:
头文件
为了管理共享内存,你需要包含下面这个头文件:
[cpp] view plain copy
- #include <boost/interprocess/shared_memory_object.hpp>
创建共享内存片段
如上述,我们必须使用类 shared_memory_object 来创建、打开和销毁能被几个进程映射的共享内存段。我们可以指定共享内存对象的访问模式(只读或读写),就好像它是一个文件一样:
- 创建共享内存段。如果已经创建了,会抛异常:
[cpp] view plain copy
- using boost::interprocess;
- shared_memory_object shm_obj
- (create_only //only create
- ,"shared_memory" //name
- ,read_write //read-write mode
- );
- 打开或创建一个共享内存段:
[cpp] view plain copy
- using boost::interprocess;
- shared_memory_object shm_obj
- (open_or_create //open or create
- ,"shared_memory" //name
- ,read_only //read-only mode
- );
- 仅打开一个共享内存段。如果不存在,会抛异常:
[cpp] view plain copy
- 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:
[cpp] view plain copy
- 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对象:
[cpp] view plain copy
- 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。
一个简单的例子
让我们看看一个简单的使用共享内存的例子。一个服务端进程创建了一个共享内存对象,映射它并且初始化所有字节至同一个值。之后,客户端进程打开共享内存,映射它并且检查数据是不是被正确的初始化了。
[cpp] view plain copy
- #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 对象,此对象承载了一个能够被相关进程共享的匿名共享内存片段。
以下是例子:
[cpp] view plain copy
- #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会话上创建共享内存对象是一个需要特权的操作。
我们重复一下在可移植的共享内存对象上使用的例子:一个服务端进程创建了一个共享内存对象,映射它并且初始化所有字节至同一个值。之后,客户端进程打开共享内存,映射它并且检查数据是不是被正确的初始化了。需要小心的是,如果在客户端连接共享内存前,服务端就存在了,则客户端连接会失败,因为当没有进程关联这块内存时,共享内存片段会被销毁。
以下是服务端进程:
[cpp] view plain copy
- #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类来封装关键字构建。
我们再重复一下在可移植的共享内存对象上使用的例子:一个服务端进程创建了一个共享内存对象,映射它并且初始化所有字节至同一个值。之后,客户端进程打开共享内存,映射它并且检查数据是不是被正确的初始化了。
以下是服务端进程:
[cpp] view plain copy
- #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中实现如下:
[cpp] view plain copy
- using boost::interprocess;
- file_mapping m_file
- ("/usr/home/file" //filename
- ,read_write //read-write mode
- );
现在,我们可以使用新创建的对象来创建内存区域。更多关于这个类的详情,请参考 boost::interprocess::file_mapping。
映射文件在内存中的内容
当创建了一个文件映射后,一个进程仅需在进程地址空间上映射共享内存。使用者可以映射整个共享内存或仅仅一部分。使用mapped_region类完成映射过程。如前所述,这个类代表了一块内存区域,此区域映射自共享内存或其他具有映射能力的设备:
[cpp] view plain copy
- 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();
使用者可以从可映射的对象中指定映射区域的起始偏移量以及映射区域的大小。如果未指定偏移量或大小,则整个文件被映射。如果仅指定了偏移量而没有指定大小,则映射区域覆盖了从偏移量到文件结尾的整个区域。
如果多个进程映射了同一个文件,并某进程修改了也被其他进程映射的一块内存区域范围
,则修改马上会被其他进程检测到。然后,磁盘上的文件内容不是立即更新的,因为这会影响性能(写磁盘比写内存要慢几倍)。如果使用者想确定文件内容被更新了,他可以刷新视图的一部分至磁盘。当函数返回后,刷新进程启动,但是不保证所有数据都写入了磁盘:
[cpp] view plain copy
- //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。
一个简单的例子
我们赋值在共享内存章节中提到的例子,使用内存映射文件。一个服务端进程创建了一个内存映射文件并且初始化所有字节至同一个值。之后,客户端进程打开内存映射文件并且检查数据是不是被正确的初始化了。(译注:原文此处误为“共享内存”)
[cpp] view plain copy
- #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 的智能指针,它能安全是使用在共享内存中,并且能用于指向另一个置于同一共享内存/内存映射文件中的对象。
固定地址映射
使用相对指针没有使用原始指针方便,因此如果一个使用者能够成功将同样的文件或共享内存对象映射至两个进程的相同地址,使用原始指针就是个好主意了。
为了映射一个对象至固定地址,使用者可以在映射区域的构造函数中指定地址:
[cpp] view plain copy
- 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。
[cpp] view plain copy
- //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字节映射:
[cpp] view plain copy
- //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*页面大小的文件指定了如下两个映射区域:
[cpp] view plain copy
- //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
- );
此例中,页面的一半空间浪费在第一个映射中,另一半空间浪费在第二个映射中,因为偏移量不是页面大小的整数倍。使用最小资源的映射应该是映射整个页面文件:
[cpp] view plain copy
- //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有一个静态函数返回页面大小值:
[cpp] view plain copy
- //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使用时)并且它们的值对所有进程均相同。