Boost:shared_memory_object --- 共享内存
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Boost:shared_memory_object --- 共享内存相关的知识,希望对你有一定的参考价值。
什么是共享内存
共享内存是最快速的进程间通信机制。操作系统在几个进程的地址空间上映射一段内存,然后这几个进程可以在不需要调用操作系统函数的情况下在那段内存上进行读/写操作。但是,在进程读写共享内存时,我们需要一些同步机制。
考虑一下服务端进程使用网络机制在同一台机器上发送一个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)
- &nb
以上是关于Boost:shared_memory_object --- 共享内存的主要内容,如果未能解决你的问题,请参考以下文章