Boost 15 进程间通信

1. 介绍

Boost.Interprocess库简化了使用通用的进程间通信和同步机制。并且提供这些机制的部件:

 * 共享内存

 * 内存映射文件

 * 信号量,互斥量,条件变量和可升级的互斥量类型,该类型可以放入共享内存和内存映射文件中

 * 命名版本的同步对象

 * 文件锁

 * 相对指针

 * 消息队列

 

Boost.Interprocess还提供了更高级的进程间机制,用于动态分配共享内存或者内存映射文件。:

 * 在共享内存或者文件映射中动态创建匿名或者命名对象。

 * 与共享内存或者文件映射兼容的类似STL的容器

 * 可在共享内存和文件映射中使用的类似STL的分配器,类似内存池。

 

1.1. 构建Boost.Interprocess

没必要编译Boost.Interprocess,因为都是头文件。只需要添加Boost头文件目录到你的编译器包含路径中。

Boost.Interprocess依赖于Boost.DateTime库,但是该库是需要编译的。

 

1.2. 测试过的编译器

 

2. 快速试用

2.1. 将共享内存作为无名内存块池使用

你只需要分配共享内存段的一部分,拷贝消息到该缓冲区中,发送该内存部分的偏移给另外进程,然后就完成了。让我们看看例子:

#include <boost/interprocess/managed_shared_memory.hpp>
#include <cstdlib>
#include <sstream>


int main( int argc,char* argv[]){
    using namespace boost::interprocess;

    if( argc==1 ){ // 父进程
        struct shm_remove{
            shm_remove(){ shared_memory_object::remove(“MySharedMemory”);}

            ~shm_remove(){ shared_memory_object::remove(“MySharedMemory”); }
        }remover;

        // 创建一个被管的共享内存段
        managed_shared_memory segment(create_only,”MySharedMemory”,65536);

        // 分配段的一部分(原始内存)
        managed_shared_memory::size_type free_memory = segment.get_free_memory();
        void* shptr = segment.allocate(1024/*分配的字节数*/);

        // 检测是否没变
        if( free_memory <= segment.get_free_memory() )
            return 1;

        // 从地址获取到的handle可以标识任何字节的共享内存段,即使它们被映射到不同内存空间
        managed_shared_memory::handle_t handle = segment.get_handle_from_address( shptr );

        std::stringstream s;
        s << argv[0] <<” “<< handle;
        s << std::ends;

        // 启动子进程
        if( 0!-std::system(s.str().c_str()) )
            return 1;

        // 检测被释放的内存
        if( free_memory != segment.get_free_memory() )
            return 1;
    }else{
        // 子进程中打开被管理的段
        managed_share_memory segment(open_only,”MySharedMemory”);
        managed_shared_memory::handle_t handle = 0;

        std::stringstream s;
        s << argv[1];
        s >> handle;

        // 通过handle获取缓冲区地址
        void * msg = segment.get_address_from_handle( handle );

        // 释放父进程分配的内存
        segment.deallocate(msg);
    }
    return 0;
}

2.2. 创建命名共享内存对象

用户希望创建共享内存段对象,指定对象的名字。这样另外的进程可以找到它们,并且删除。代码如下:

 

#include <boost/interprocess/managed_shared_memory.hpp>

#include <cstdlib>

#include <cstddef>

#include <cassert>

#include <utility>

 

int main( int argc,char* argv[] )

{

using namespace boost::interprocess;

typedef std::pair<double,int> MyType;

 

if( argc==1){

// 父进程

// 在构造函数和析构函数中移除共享内存

struct shm_remove{

shm_remove(){ shared_memory_object::remove(“MySharedMemory”); }

~shm_remove(){ shared_memory_object::remove( “MySharedMemory”);}

} remover;

 

// 构造被管理的共享内存

managed_shared_memory segment( create_only,”MysharedMemory”,65536 );

 

// 创建一个MyType的对象初始化为(0.0, 0)

MyType* instance = segment.construct<MyType>

(“MyType instance”) // 对象名

( 0.0, 0 ); // 构造函数参数

 

// 创建一个数组有10个元素,并都初始化为(0.0, 0 )

MyType* array = segment.construct<MyType>

(“MyType array”)

[10]

(0.0, 0);

 

// 构造一个有3个元素的数组

float float_initializer[3] = { 0.0, 1.0, 2.0 };

int int_initializer[3] = { 0, 1, 2 };

 

MyType * array_it = segment.construct_it<MyType>

(“MyType array from it”) // 对象名称

[3] // 元素个数

( &float_initializer[0],&int_initializer[0] ); // 初始化参数

 

// 启动子进程

std::string s( argv[0] );

s += “ child ”;

if( 0!=std::system(s.c_str()));

return 1;

 

// 检测子进程是否清除掉所有对象

if( segment.find<MyType>(“MyType array”).first ||

segment.find<MyType>(“MyType instance”).first ||

segment.find<MyType>(“MyType array from it”).first )

return 1;

}else{

// 子进程

// 打开共享内存

managed_shared_memory segment( Open_only, “MySharedMemory”);

 

std::pair<MyType*,managed_shared_memory::size_type> res;

 

// 查找数组

res = segment.find<MyType>(“MyType array”);

// 长度应该为10

if( res.second!=10)

return 1;

 

// 查找对象

res = segment.find<MyType>(“MyType instance”);

if( res.second != 1 )

return 1;

 

// 查找由迭代器构建的数组

res = segment.find<MyType>(“MyType array from it”);

if( res.second != 3 )

return 1;

 

// 都找到了,删除所有对象

segment.destroy<MyType>(“MyType array”);

segment.destroy<MyType>(“MyType instance”);

segment.destroy<MyType>(“MyType array from it”);

}

 

return 0;

}

 

2.3. 使用共享内存的偏移精灵指针

Boost.Interprocess提供了offset_ptr精灵指针族,保存有该存储区内该指针的地址偏移。

 

#include <boost/interprocess/managed_shared_memory.hpp>

#include <boost/interprocess/offset_ptr.hpp>

 

using namespace boost::interprocess;

 

// 共享内存链表节点

struct list_node{

offset_ptr<list_node> next;

int value;

}

 

int main(){

// 删除共享内存

struct shm_remove{

shm_remove(){ shared_memory_object::remove(“MySharedMemory”);}

~shm_remove(){ shared_memory_object::remove(“MySharedMemory”); }

} remover;

 

// 创建共享内存

managed_shared_memory segment( create_only,”MySharedMemory”,65536);

 

// 在共享内存中创建10个元素的链表

offset_ptr<list_node> prev = 0, current, first;

 

int i;

for( i=0; i<10;++i, prev = current ){

current = static_cast<list_node*>(segment.allocate(sizeof(list_node)));

current->value = i;

current->next = 0;

if( !prev )

first = current;

else

prev->next = current;

}

 

// 与其他进程通信

// ...

// 结束后,销毁链表

for( current = first; current; /**/ ){

prev = current;

current = current->next;

segment.deallocate(prev.get());

}

 

return 0;

}

 

为了帮助基本数据结构,Boost.Interprocess提供了容器,类似vector,list,map。所有用户应该避免使用手工数据结构,只需要像标准容器一样。

2.4. 在共享内存中创建vector

Boost.Interprocess允许在共享内存和内存映射文件中创建复杂的对象。例如:可以在共享内存中创建类似STL的容器。要实现这样的功能,我们只需要创建一个特殊的共享内存段,声明一个Boost.Interprocess分配器和构造向量。

在共享内存中允许构造复杂结构的类是boost::interprocess::managed_shared_memory.

 

#include <boosst/interprocess/managed_shared_memory.hpp>

#include <boost/interprocess/containers/vector.hpp>

#include <boost/imterprocess/allocators/allocator.hpp>

#include <string>

#include <cstdlib>

 

using namespace boost::interprocess;

 

// 定义一个STL兼容的分配器,从managed_shared_memory中分配整型。

// 这个分配器允许放置容器到段里

typedef allocator<int,managed_shared_memory::segment_manager> ShmemAllocator;

 

typedef vector<int, ShemAllocator> MyVector;

 

// 主函数,argc==1运行父进程,argc==2运行子进程

int main( int argc,char* argv[] ){

if( argc==1 ){ // 父进程

struct shm_remove{

shm_remove(){ shared_memory_object::remove(“MySharedMemory”); }

~shm_remove(){ shared_memory_object::remove(“MySharedMemory”); }

} remover;

 

managed_shared_memory segment(create_only,”MySharedMemory”,65536);

 

// 初始化共享内存STL兼容分配器

const ShmemAllocator alloc_ins( segment.get_segment_manager() );

 

// 构造一个向量,命名为MyVector

MyVector * myvector = segment.construct<MyVector>(“MyVector”)(alloc_inst);

 

for( int i=0;i<100;++i )

myvector->push_back(i);

 

std::string s(argv[0]);

s += “child”;

if( 0!= std::system( s.c_str()))

return 1;

 

if( segment.find<MyVector>(“MyVector”).first )

return 1;

}else{ // 子进程

managed_shared_memory segment(open_only,”MySharedMemory”));

 

MyVector* myvecor = segment.find<MyVector>(“MyVector”).first;

 

std::sort(myvector->rbegin(),myvector->rend));

 

segment.destory<MyVector>(“MyVector”);

}

return 0;

}

 

The parent process will create an special shared memory class that allows easy construction of many complex data structures associated with a name. The parent process executes the same program with an additional argument so the child process opens the shared memory and uses the vector and erases it.

2.5. 在共享内存中创建map

 

#include <boost/interprocess/managed_shared_memory.hpp>

#include <boost/interprocess/containers/map.hpp>

#include <boost/interprocess/allocators/allocator.hpp>

#include <functional>

#include <utility>

 

int main(){

using namespace boost::interprocess;

 

struct shm_remove{

shm_remove(){ shared_memory_object::remove(“MySharedMemory”); }

~shm_remove(){ shared_memory_object::remove(“MySharedMemory”); }

} remover;

 

managed_shared_memory segment( create_only, “MySharedMemory”,65536);

 

typedef int KeyType;

typedef float MappedType;

typedef std::pair<const int,float> ValueType;

 

typedef allocator<ValueType,managed_shared_memory::segment_manager> ShmemAllocator;

 

typedef map<KeyType,MappedType,std::less<KeyType>, ShmemAllocator> MyMap;

 

ShmemAllocator alloc_inst( segment.get_segment_manager() );

 

MyMap * mymap = segment.construct<MyMap>(“MyMap”)

(std::less<int>(),

alloc_inst);

 

for( int i=0;i<100;++i ){

mymap->insert( std::pair<const int,float>(i,(float)i);

}

return 0;

}

 

3. 基本说明

 

3.1. 进程和线程

Boost.Interprocess不仅工作在进程间,也工作在线程间。Boost.Interprocess的同步机制可以在不同进程的线程之间同步,也可以在相同进程的不同线程间同步。

3.2. 进程间共享信息

在传统的编程模式中,一个操作系统有多个进程运行在其自身的地址空间内。想要共享信息时,我们有一些可以替代的方法:

l 两个进程使用一个文件共享信息。要访问数据,每个进程使用文件的读写机制。当更新或读取共享文件时,我们需要一些机制来同步,将读进程从写进程中保护起来。

l 两个进程共享驻留在操作系统内核中的信息。这正是,例如,传统的消息队列。同步由操作系统内核保证。

l 两个进程可以共享一个内存区间。这正是经典的共享内存或者内存映射文件机制。一旦进程设置好内存区域,进程就可以像其他内存段一样读写数据,而不需要调用内核。这种操作也需要在进程间手工同步。

 

3.3. Interprocess机制的持久性

进程间通信机制的最重大的议题之一是进程间通信机制的生命期。明白进程间通信机制什么时候从系统中消失是很重要的。在Boost.Interprocess中,我们有3中持久性:

l 进程范围的持久性。这种机制持续到所有打开它的进程关闭该机制,进程退出或者崩溃。

l 内核范围的持久性。这种机制持续到操作系统内核重启或者显式的删除它。

l 文件系统范围的持久性。这种机制一直持续到显式的删除为止。

 

有些原著的POSIX和Windows IPC机制在持久性上有比较大的差异,很难保证它们在Windows和POSIX系统之间的可移植性。Boost.Interprocess类有以下的持久性:

Mechanism

Persistence

Shared memory

Kernel or Filesystem

Memory mapped file

Filesystem

Process-shared mutex types

Process

Process-shared semaphore

Process

Process-shared condition

Process

File lock

Process

Message queue

Kernel or Filesystem

Named mutex

Kernel or Filesystem

Named semaphore

Kernel or Filesystem

Named condition

Kernel or Filesystem

如你所见,Boost.Interprocess定义了一些具有“内核或文件系统”持久性的机制。这是因为POSIX允许原著进程间通信实现具有这种功能。用户可以实现共享内存,使用映射文件和获取文件系统持久性。

3.4. Interprocess机制的名称

有些进程间机制是在共享内存或内存映射文件中创建的匿名对象,但是有些进程间机制需要一个名字或者标识,这样两个没有关系的进程可以使用相同的interprocess机制对象。例如:共享内存,命名互斥量,命名信号量。

用于标识一个interprocess机制的名称是不具备可移植性的,纵使是在不同的UNIX系统之间。正因为如此,Boost.Interprocess限制名字:

l 以字母开始,小写或大写。如:Sharedmemory,sharedmemory,sHaReDmOry

l 可以包含字母,下划线,数字。如:shm1,shm2and3,Shm4Plus4

3.5. 构造,析构和Interprocess命名资源的生命周期

命名的Boost.Interprocess资源具有内核或者文件系统级别的持久性,包括共享内存,内存映射文件,命名互斥量,命名条件变量,命名信号量。意味着即使所有打开该资源的进程都结束了,资源将任然可以被从新打开并访问;这些资源只能被显式的调用它们的静态移除函数。这种行为很容易理解,因为它和控制文件打开,创建,删除的函数机制是一样的。

Boost.Interprocess和文件系统对比

Named Interprocess resource

Corresponding std file

Corresponding POSIX operation

Constructor

std::fstream constructor

open

Destructor

std::fstream destructor

close

Member remove

None. std::remove

unlink

下面是POSIX和Boost.Interprocess的共享内存和命名信号量之间的对比。

Table 15.3. Boost.Interprocess-POSIX shared memory

shared_memory_object operation

POSIX operation

Constructor

shm_open

Destructor

close

Member remove

shm_unlink

 

Table 15.4. Boost.Interprocess-POSIX named semaphore

named_semaphore operation

POSIX operation

Constructor

sem_open

Destructor

close

Member remove

sem_unlink

最重要的属性是析构命名资源不会移除资源,而只是释放系统为该进程分配的资源。要从系统移除该资源必须使用remove操作。

3.6. 权限

Boost.Interprocess提供的命名资源必须处理平台相关的权限,包括创建文件的权限。如果一位程序员希望在不同用户之间分享共享内存,内存映射文件或者命名的同步机制,必须指明哪些权限。可悲的是,传统的UNIX和Windows的权限差别非常大,Boost.Interprocess不打算标准化权限,但是也不会忽略它们。

所有的命名资源创建函数有一个可选的权限对象,可以使用平台相关权限进行配置。

由于每种机制可以被不同机制模拟。权限类型可能会改变。为了防止这种情况,Boost.Interprocess依赖类似文件的权限,共享内存需要文件read-write-delete权限。

4. 在进程间共享内存

4.1. 共享内存

4.1.1. 什么是共享内存?

共享内存是最快的进程间通信机制。操作系统在多个进程的地址空间映射一个内存段,这些进程就可以读写这个内存段了,而不需要调用操作系统函数。然而,我们需要在这些读写操作中增加同步机制。

考虑当一个服务进程想要通过网络机制发送一个HTML文件给它的一个客户端进程时,会发生什么?

l 服务器必须读取文件到内存,并且通过网络函数,将它的内存拷贝到操作系统内部内存。

l 服务器使用网络函数拷贝操作系统内存数据到其进程空间内存。

 

正如我们可以看到的,有两份拷贝,一种是内存到网络,而另外一种是网络到内存。而且这两份拷贝是靠操作系统调用来实现的,一般都是比较费资源的。共享内存避免了这种天花板,但是我们需要同步两个进程:

l 服务器进程映射一个共享内存到地址空间,并且也获取一个同步机制。服务器使用同步机制获取排他地访问内存,将文件拷贝到内存。

l 客户端映射该共享内存到地址空间。等待服务器释放排他访问,然后读取该内存。

 

使用共享内存,我们可以避免两份数据拷贝,但是我们必须在共享内存段访问上增加同步。

4.1.2. 创建可进程间共享的内存段

要使用共享内存,必须执行以下两步基本步骤:

l 向操作系统请求一个可以共享的内存段。该用户可以创建、销毁、打开这个内存,通过一个共享内存对象:该对象代表可以同时被多个进程映射到其自己地址空间的内存。

l 将该内存的一部分或者全部关联到调用的进程的地址空间。操作系统在该进程地址空间内寻找一个足够大的内存地址范围,并且标记该地址范围为一个特殊范围。其他进程关联到的地址范围各不相同。

 

这两步成功完成后,进程可以读写该地址空间。下面看看Boost.Interprocess如何做的。

4.1.3. 头文件

管理共享内存,必须包含下列头文件:

#include <boost/interprocess/shared_memory_object.hpp>

 

4.1.4. 创建共享内存段

我们必须使用shared_memory_object类来创建、打开和销毁可以被多个进程映射的共享内存段。我们必须指明访问该共享内存对象的模式(只读,后者读写),就像一个文件一样:

l 创建一个共享内存段。如果已经存在,则抛出异常:

using boost::interprocess;

shared_memory_object shm_obj

( create_only // 只是创建

,”shared_memory”// 名字

,read_write // 读写模式

);

 

l 打开或者创建一个共享段

using boost::interprocess;

shared_memory_object shm_obj

( open_or_create // 打开或者创建

,”shared_memory” // 名字

,read_only // 只读模式

);

 

l 只打开一个共享内存段。如果不存在抛出异常:

using boost::interprocess;

shared_memory_object shm_obj

( open_only // 只是打开

,”shared_memory” // 名字

,read_write // 读写模式

);

当一个共享内存对象被创建,其大小为0.用户必须使用truncate函数设置该共享内存大小。该对象必须具备read-write模式:

shm_obj.truncate(10000);

remove函数可能会失败返回false,因为共享内存不存在,或者文件被打开或者文件还被其他进程映射:

using boost::interprocess;

shared_memory_object::remove(“shared_memory”);

 

4.1.5. 映射共享内存段

一旦创建或者打开,进程只需要映射共享内存对象到其地址空间。用户可以映射整个共享内存,也可以映射一部分到地址空间。映射进程使用类mapped_region类。该类代表一个从共享内存对象或其他可映射的设备获取的内存区域。事实上shared_memory_object是memory_mappable对象。

using boost::interprocess;

std::size_t ShmSize = ...

 

// 映射后半部分

mapped_regin region

( shm // 可映射对象

, read_write // 访问模式

, ShmSize/2 // 起始偏移

,ShmSize-ShmSize/2 // 长度

);

 

// 获取映射后区域地址

region.get_address();

 

// 获取区域的长度

region.get_size();

 

The user can specify the offset from the mappable object where the mapped region should start and the size of the mapped region. If no offset or size is specified, the whole mappable object (in this case, shared memory) is mapped. If the offset is specified, but not the size, the mapped region covers from the offset until the end of the mappable object.

For more details regarding mapped_region see the boost::interprocess::mapped_region class reference.

 

4.1.6. 一个简单例子

让我们看一个共享内存用法的简单例子:一个服务器进程创建了一个共享内存对象,映射并且初始化所有字节。然后客户端进程打开这个共享内存,映射并检测数据的正确性:

#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 ){ // 父进程

// 在构造和析构中移除共享内存

struct shm_remove{

shm_remove(){ shared_memory_object::remove(“MySharedMemory”);}

~shm_remove(){ shared_memory_object::remove(“MySharedMemory”);}

} remover;

 

// 创建共享内存

shared_memory_object shm( create_only, “MySharedMemory”, read_write );

shm.truncate(1000);

 

mapped_region region(shm,read_write);

std::memset(region,get_address(),1,region.get_size());

 

std::string s(argv[0]);

s += “ child “;

if( 0!=std::system(s.c_str()))

return 1;

}else{ // 子进程

shared_memory_object shm( open_only,”MySharedMemory”,read_only );

mapped_region region(shm,read_only);

 

char *mem = static_cast<char*>(region.get_address());

for( std::size_t i=0;i<region.get_size();++i )

if( *mem++ != 1 )

return 1;

}

}

 

 

4.1.7. 为不具备共享内存对象系统模拟

Boost.Interprocess以POSIX形式提供了可移植的共享内存。有些操作系统不支持POSIX定义的共享内存:

l Windows操作系统提的供共享内存由分页文件支持的内存,但是其生命期语义不同于POSIX中定义。

l 有些UNIX系统不完全支持POSIX中共享内存对象。

 

在那些平台中,共享内存使用映射文件来模拟。这些文件在临时文件夹的boost_interprocess目录内。在Windows平台,如果注册表中有“Common AppData”键值,boost_interprocess文件夹就建在该目录下,in XP usually "C:\Documents and Settings\All Users\Application Data" and in Vista "C:\ProgramData"). For Windows platforms without that registry key and Unix systems, shared memory is created in the system temporary files directory ("/tmp" or similar).

由于是模拟的,这些共享内存具有文件系统的持久性。

4.1.8. 移除共享内存

shared_memory_object类提供了一个静态函数remove用于移除共享内存对象。

这个函数可能会执行失败:比如共享内存对象不存在,或者被其他进程打开着。需要注意的是这个函数与标准C中的int remove( const char* path)函数相似, shared_memory_object::remove调用shm_unlink:

l 该函数将会移除由name指定的共享内存的名字

l 如果一个或多个引用存在,此时执行分离,函数返回前名字会被删除,但是内存对象的内容删除会被拖延到所有打开和映射都关闭后。

l 如果在调用最后一个函数后,对象继续退出,再用这个名字将会导致重新创建一个共享内存对象,但是好像这个名字的对象从来没有创建过一样。

 

在windows系统中,当前版本支持一种通常可接受的UNIX的解除动作的模拟行为:当最后一个打开句柄被关闭时,文件名被重命名为一个随机名,并且标记为已删除。

4.1.9. UNIX系统中的匿名共享内存

当关联多个进程时,创建共享内存段,并且映射它,这些操作有一点枯燥。如果是UNIX系统中石油fork()函数关联起来的,一种更简单的方法可以使用,就是使用匿名共享内存。

这个特性在UNIX系统中被实现时,映射设备\dev\zero或者传递参数MAP_ANONYMOUS给mmap系统调用。

在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{

mapped_region region(anonymous_shared_memory(1000));

std::memset(region.get_address(),1,region.get_size());

}catch( interprocess_exception & ex ){

std::cout << ex.what() << std::endl;

return 1;

}

 

return 0;

}

4.1.10. Windows的原著共享内存

Windows操作系统也有共享内存,但是它的生命期却不是kernel或者filesystem。它是由页面文件做支撑创建的,当最后一个进程与它分离时,会被自动销毁。

因为这个原因,没有很好的方法可以使用原著windows共享内存来模拟kernel或者filesystem持久性。Boost.Interprocess模拟共享内存时使用内存映射文件。这样就能保证在POSIX和Windows操作系统之间的可移植性了。

然而Boost.Interprocess用户对原著Windows的共享内存的访问的需求也是需要的,因为他们有时需要和不是使用Boost.Interprocess创建共享内存的进程交互。这样Boost.Interprocess也提供了管理windows原著共享内存的类:windows_shared_memory。

Windows共享内存的创建与可移植共享内存不太一样:段大小必须在创建时指定,而不能使用truncate来设置。注意的是,当使用共享内存的最后一个进程销毁时,共享内存也会被销毁。

后台服务程序和应用程序的共享内存也不一样。要在服务程序和应用程序之间共享内存,该共享内存的名字必须以”Global\\”开头,是一个全局名称。服务进程可以创建全局名称的共享内存,然后客户端程序可以打开该共享内存。

The creation of a shared memory object in the global namespace from a session other than session zero is a privileged operation.

 

举例:

#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;

}

4.1.11. XSI共享内存

 In many UNIX systems, the OS offers another shared memory memory mechanism, XSI (X/Open System Interfaces) shared memory segments, also known as "System V" shared memory. This shared memory mechanism is quite popular and portable, and it's not based in file-mapping semantics, but it uses special functions (shmget, shmat, shmdt, shmctl...).

 

Unlike POSIX shared memory segments, XSI shared memory segments are not identified by names but by 'keys' usually created with ftok. XSI shared memory segments have kernel lifetime and must be explicitly removed. XSI shared memory does not support copy-on-write and partial shared memory mapping but it supports anonymous shared memory.

 

Boost.Interprocess offers simple (xsi_shared_memory) and managed (managed_xsi_shared_memory) shared memory classes to ease the use of XSI shared memory. It also wraps key creation with the simple xsi_key class.

 

Let's repeat the same example presented for the portable shared memory object: A server process creates a shared memory object, maps it and initializes all the bytes to a value. After that, a client process opens the shared memory, maps it, and checks that the data is correctly initialized.

 

This is the server process:

 

#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;

}

 

4.2. 内存映射文件

4.2.1. 什么事内存映射文件?

文件映射就是将文件的内容和一个进程的地址空间相关联。系统创建文件映射来关联文件和地址空间。映射区间是进程使用的地址段,可以访问文件内容。一个文件映射,可以有多个映射区间,因此用户可以不需要映射文件的全部内容,而只是映射一部分到地址空间,因为有时候文件会非常大,超过进程的地址空间。进程使用指针读写文件,就像动态内存。文件映射有以下优点:

l 统一资源使用。文件和内存可以使用相同函数对待

l 自动从操作系统对文件数据同步和缓存

l 在文件中利用C++工具(STL容器,算法)

l 在多个应用程序中共享内存

l 允许高效地工作在大文件上,不需要映射整个文件到内存

l 如果多个进程使用相同的文件映射来创建映射区间,每个进程有相同的文件拷贝。

 

文件映射不仅仅用于进程间通信,还可以用于简化文件用法,这样用户没必要使用文件管理函数来写文件。用户只是往进程地址内写数据,操作系统会将数据导入文件。

当两个进程映射同一个文件到内存,一个进程写的数据会被另外一个进程读到,所以,内存映射文件可以作为进程间通信的机制。我们可以说内存映射文件提供与共享内存相同的进程间通信服务,只是是filesystem级别的持久性。然而,因为操作系统需要同步数据到文件中,所以,内存映射文件没有共享内存快。

4.2.2. 使用映射文件

要使用内存映射文件,我们有两步基本操作:

l 创建一个可映射的对象代表一个文件系统内存在的文件。这个对象将来用于创建该文件的多个映射区间。

l 关联整个文件或者文件的一部分到调用的进程地址空间。操作系统在调用进程内寻找足够大的内存地址范围,并标记该范围为特殊应用。其他也映射该文件的进程地址区间会不同。

 

 

4.2.3. 头文件

#include <boost/interprocess/file_mapping.hpp>

 

4.2.4. 创建文件映射

创建代表文件的可映射对象:

using boost::interprocess;

file_mapping m_file

(“/usr/home/file” // 文件名

,read_write // 读写模式

);

 

4.2.5. 映射文件内容到内存

创建文件映射之后,进程只需要将功能内存映射到进程的地址空间。用户可以映射全部文件,也可以只映射一部分。映射过程使用类mapped_region实现。

using boost::interprocess;

std::size_t FileSize = ...

 

mapped_region region

( m_file

, read_write

, FileSize / 2

, FileSize - FileSize/2

);

 

region.get_address();

 

region.get_size();

如果多个进程映射了相同文件,当一个进程对映射的内存区间内容修改了,而其他进程也映射了该区间,那么其他进程立即能看到该进程的修改。但是,硬盘上文件的内容是不会里面同步更新的,因为那会伤害性能。如果用户希望确保已经更新到文件内容,可以flush一个区间到磁盘。当函数返回时,flush过程已经开始,但是不能保证完成。

// flush全部区间

region.flush();

 

// flush从offset开始后的所有数据

region.flush( offset );

 

// flush从offset开始后size长度的数据

region.flush( offset, size );

 

记住这里的offset不是文件的全局偏移,而是映射区间内部偏移。

4.2.6. 简单例子

 

#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;

 

const char* FileName = “file.bin”;

const std::size_t FileSize = 10000;

 

if( argc==1 ){ // 父进程

{ // 创建文件

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);

fbuf.pubseekoff( FileSize -1, std::ios_base::beg);

fbuf.sputc(0);

}

 

// 退出时删除文件

struct file_remove{

file_remove( const char* FileName ):FileName_(FileName){}

~file_remove(){ file_mapping::remove(FileName_);}

const char* FileName_;

}

 

// 创建一个文件映射

file_mapping m_file(FileName, read_write );

 

// 映射整个文件到内存空间

mapped_region region(m_file,read_write);

 

std::memset( region.get_address(),1,region.get_size());

 

// 启动子进程

std::string s(argv[0]);

s += “ chile “;

if( 0!=std::system(c.c_str()) )

return 1;

}else{ // 子进程

{ // 打开文件映射,并以只读模式映射

file_mapping m_file(FileName,read_only);

mapped_region region(m_file,read_only);

 

const char* mem = static_cast<char*>(region.get_address());

for( std::size_t i = 0;i<region.get_size();++i )

if( *mem++ != 1 )

return 1;

}

{ // 测试读取文件

std::filebuf fbuf;

fbuf.open( FileName, std::ios_base::in|std::ios_base::binary);

 

std::vector<char> vect(FileSize,0);

fbuf.sgetn(&vect[0],std::streamsize(vect.size()));

 

const char* mem = static_cast<char*>(&vect[0]);

for( std::size_t i=0;i<FileSize;++i )

if( *mem++ != 1 )

return 1;

}

}

return 0;

}

4.3. 深入映射区域

4.3.1. 一个类管理它们所有

正如我们看到的,shared_memory_object和file_mapping对象都可以用于创建mapped_region对象。他们使用同一个映射区间类有很多优点。

One can, for example, mix in STL containers mapped regions from shared memory and memory mapped files. Libraries that only depend on mapped regions can be used to work with shared memory or memory mapped files without recompiling them.

4.3.2. 在多个进程中映射地址

在例子中我们看到,映射到进程空间的地址是由操作系统选择的。如果多个进程映射同一个对象,映射区域的地址,在各进程中肯定是不一样的。这样在映射的内存区间使用指针是无效的,因为只有保存指针值得进程有效。解决办法就是使用偏移而不是指针:两个对象地址的距离在不同进程中是一样的。

所以,第一条建议是在映射共享内存或者内存映射文件时,避免使用原始指针,除非你了解你在做什么。使用数据之间的偏移,或者相对指针来获取指针功能。Boost.Interprocess提供一个智能指针类boost::interprocess::offset_ptr,可以用于安全的放在共享内存中,执行共享内存中的其他对象。

 

4.3.3. 固定地址映射

使用相对指针和使用原始指针比,效率要差一些。如果用户可以将共享内存或者映射文件加载到相同的地址空间,使用共享内存将是最好的选择。

要映射到一个固定地址,使用下面的构造函数:

mapped_region region( shm // 映射共享内存

, read_write // 使用读写模式

,0 // 偏移为0

,0 // 全部长度

, (void*)0x3F000000 // 期望的地址

);

然而,用户不能将地址映射到随意位置,即使该地址没有被占用。后面再限制里会讨论。

4.3.4. 映射偏移和地址局限性

大部分的操作系统要求映射的地址和被映射对象的偏移值必须是页大小的倍数。这是因为操作系映射时是整个页映射的。

如果使用固定地址映射,偏移和地址参数应该是页大小的整数倍。对于32位系统页大小典型值有:4KB,8KB。

因为操作系统执行映射时,是整页的,指定大小或者偏移不是页大小整数倍时,会浪费更多的系统资源。比如你映射1字节时:

mapped_region region( shm,read_write,0,1);

操作系统会保留整个页面,其他的映射将不会再使用它,所以,我们将浪费pagesize-1的空间。如果我们想要高效操作系统资源,我们可以创建区间大小是页大小整数倍的区间。

mapped_region region1( shm, read_write

,0 // 偏移为0

,page_size/2 ); // 大小是页面的一半,实际是一个页面

 

mapped_region region2(shm,read_write

, page_size/2 // 实际偏移是0,浪费一半

3*page_size/2); //

如何获取页面大小呢。

std::size_t page_size = mapped_region::get_page_size();

 

4.4. 在映射区间构造对象的限制

当两个进程都创建了相同可映射对象的映射区间时,两个进程可以通过该内存读写来通信。一个进程可以在该映射区间构造C++对象,这样另外一个进程可以使用。然而,共享的映射区间并不能承载所有的C++对象,因为不是所有的类都是准进程共享对象,尤其是映射区间地址还不一样。

4.4.1. 偏移指针代替原始指针

将对象放入一个映射区间,并且在不同进程中映射的区间地址不一样,原始指针是个问题,因为他们只在放入区间的进程有效。为了解决这个问题,Boost.Interprocess提供了一种特殊的智能指针用于替代原始指针。这样用户类中含有原始指针的不能安全的放入映射区间。这些指针必须用偏移指针替换,并且这些指针只能指向同一个映射区间内的对象。

4.4.2. 禁用引用

引用也有同指针一样的问题(原因是他们实现像指针)。然而,不可能创建一个完全可工作的智能引用,在当前的C++中(例:运算符.()不能被覆盖)。正因如此,如果用户想要放一个对象到共享内存,对象内不能有引用成员。

 

4.4.3. 禁用虚性

虚表指针和虚表存在于创建对象的进程地址空间,所以如果我们要放一个有虚函数或者虚基类的类,其他进程会崩溃。

这个问题非常难解决,因为每个进程需要不同的虚表指针。

4.4.4. 注意静态成员

类的静态成员是全局对象,在进程中作为全局变量。

5. 映射地址无关的指针:offset_ptr

当为两个进程通信创建共享内存和内存映射文件时,内存段在每个进程中有不同地址。

#include <boost/interprocess/shared_memory_object.hpp>

//...

using boost::interprocess;

 

// 打开一个共享内存段

shared_memory_object shm_obj

( open_only,

,”shared_memory”

,read_only

);

 

// 映射整个共享内存

mapped_region region

( shm

, read_write

);

 

// 这个地址在不同进程中会不同

void* addr = region.get_address();

这样的话,在映射区间创建复杂对象就非常困难:存放在一个映射区间的C++实例可能有指向其他对象的指针。由于指针保存了一个绝对地址,而该地址只在产生该对象的进程中有效。除非所有进程将映射区间映射到统一地址。

为了在映射区间能够模拟指针,用户必须使用相对偏移替代绝对地址。对象之间的偏移在所有进程中是一样的。为了方便使用偏移,Boost.Interprocess提供offset_ptr。

offset_ptr封装了作为指针接口的所有后台操作。该类接口灵感来自Boost的智能指针,它保存了指向对象地址与其本身的偏移。想象通用32位处理器中的一个结构:

struct structure

{

int integer1; // 编译器将这个成员放在结构体的偏移是0

offset_ptr<int> ptr; // 偏移是4

int integer2; // 偏移是8

}

 

structure s;

 

// &s.integer1 - &s.ptr = -4

s.ptr = &s.integer1;

 

// &s.integer2 -&s.ptr = 4

s.ptr = &s.integer2;

 

offset_ptr最大的问题是如何表示一个空指针。null指针不能像偏移一样安全地表示,因为0地址往往不在映射区间。而每个进程映射区间的基址各不相同。

有些实现选用偏移0作为空指针,事实上按照offset_ptr的规则,是一个指向其本身的指针。但是很多使用场合是不合适的,比如很多时候链表结构或者STL容器需要指向其自身,这时0偏移指针是需要的。还有一种实现是指针保存除了偏移量之外,还保存一个布尔值是否为空指针,这种方式也不可取,增加指针对象空间,损害性能。

最后,offset_ptr定义偏移1作为空指针,表示该类型不可能指向它自己之后的一个字节:

using namespace boost::interprocess;

offset_ptr<char> ptr;

 

// 指向其地址的下一个自己的地址,是空地址

ptr = (char*)&ptr + 1;

 

// 判断是否为空

assert(!ptr);

 

// 同样是设置空指针

ptr = 0;

 

assert(!ptr);

在实践中,这个限制其实不重要,因为用户基本不会需要指向其后一个字节的地址。

offset_ptr提供了所有指针类似的操作和随机访问迭代器类型。所有该类可以在STL算法中使用。更多内容查看offset_ptr参考文档。

6. 同步机制

6.1. 同步机制概览

通过共享内存对象或者内存映射文件对象,在进程间共享内存,如果没有有效的同步的话,这种能力不是很有用。同样的问题在多线程中,线程之间共享的堆内存,全局变量等,也需要线程同步机制:互斥量和条件变量。Boost.Threads库实现了同一进程中线程之间同步的功能。Boost.Interprocess库实现了类似的机制来同步不同进程内的线程。

6.1.1. 命名的和匿名的同步机制

Boost.Interprocess提供了两类同步对象:

l 命名设施:当两个进程想要创建一个这类对象时,两个进程必须创建或者打开一个使用相同名字的对象。就像创建或打开文件一样:一个进程创建一个文件使用fstream和文件名,并且另外一个进程打开那个文件使用另外一个fstream和相同文件名。每个进程使用不同对象访问资源,但是两个进程都是使用相同底层资源。

l 匿名设施:因为这些设施都没有名字,两个进程必须通过共享内存或者内存映射文件共享同一个对象。这和传统的线程同步对象相似:两个进程都共享相同对象。不同于线程同步,同一个进程中的线程间共享全局变量和堆内存,两个进程之间共享对象只能通过映射区间。

 

每种类型都有其优点和缺点:

l 命名设施对于简单的同步任务来说更容易控制,因为两个进程不是必须创建一个共享内存区间和创建同步机制。

l 当使用内存映射对象获取自动持久性时,匿名设施可以被序列化到磁盘。用户可以在一个内存映射文件中构造一个同步设施,重启系统,重新映射文件,再次使用同步设施不会有任何问题。这种功能不能再命名同步机制中实现。

 

匿名设施和命名设施之间的接口区别在构造函数。通常,匿名设施只有一个构造函数。而命名设施有多个构造函数,而且第一个参数用于说明请求创建,打开,还是打开或创建底层设施。

using namespace boost::interprocess;

 

// 创建一个命名同步设施,如果已经存在,则抛出错误

NamedUtility(create_only,...);

 

// 打开一个命名同步设施,如果不存在,则创建

NamedUtility(open_or_create,...);

 

// 打开一个同步设施,如果不存在,则抛出错误

NamedUtility(open_only,...);

匿名同步设施只能被创建,进程间必须使用其他机制来同步到创建的进程:

using namespace boost::interprocess;

 

// 创建一个匿名同步设施

AnonymousUtility(...);

 

6.1.2. 同步机制类型

且不考虑命名和匿名属性,Boost.Interprocess提供了一下同步工具:

l 互斥量(命名的和匿名的)

l 条件变量(命名的和匿名的)

l 信号量(命名的和匿名的)

l 可升级互斥量

l 文件锁

6.2. 互斥量

6.2.1. 互斥量是什么?

互斥量代表了互斥的执行,是进程间同步的最基本形式。互斥量保证只有一个执行线程可以锁定一个互斥量对象。如果一段代码由一个互斥量的锁定和释放包围,那么就能保证同时只能有一个线程执行该段代码。当一个线程解锁后,其他线程才可以进入该代码段。

互斥量可以是递归的或者非递归的:

l 递归互斥量可以被同一个线程多次锁定。要完全解锁这个互斥量,线程必须执行解锁次数和锁定次数相同。

l 非递归互斥量不能被同一线程多次锁定。如果一个线程第二次锁定一个互斥量,结果不可预期,可能抛出错误,也可能会发生死锁。

 

6.2.2. 互斥量操作

Boost.Interprocess实现了互斥量类型的如下操作:

void lock()

调用线程获取互斥量的所有权。如果其他线程已经拥有该互斥量,调用线程会一直等待直到获取到该互斥量。互斥量被一个线程获取后,必须由该线程解锁。如果互斥量支持递归,解锁次数必须和锁定次数相同。

错误时,该函数抛出异常interprocess_exception。

 

bool try_lock()

调用线程尝试获取互斥量的所有权。如果另外一个线程拥有该互斥量,那么立即返回获取失败。如果互斥量支持递归锁,互斥量必须被解锁相同次数。

错误时,该函数抛出异常interprocess_exception。

 

bool timed_lock( const boost::posix_time::ptime &abs_time );

在没有到时间前,调用线程会一直尝试获取互斥量所有权。如果互斥量支持递归,必须被解锁相同次。

如果获取成功,返回true,如果超时,返回false。

如果有错误,抛出错误interprocess_exception。

 

void unlock();

前提是调用线程已经排他地用于互斥量的所有权。调用线程释放它已经获取到的互斥量所有权。如果互斥量支持递归,必须解锁相同次数。

如果有错误,抛出一个interprocess_exception的子类。

 

注意:Boost.Interprocess同步机制中的boost::posix_time::ptime是UTC时间点,不是本地时间点。

6.2.3. Boost.Interprocess互斥量类型

Boost.Interprocess提供以下互斥量类型:

#include <boost/interprocess/sync/interprocess_mutex.hpp>

l interprocess_mutex:一个非递归匿名互斥量。可以放在共享内存或者内存映射文件.

 

#include <boost/interprocess/sync/interprocess_recursive_mutex.hpp>

l interprocess_recursive_mutx:一个递归的匿名互斥量。可以放在共享内存或者内存映射文件。

 

#include <boost/interprocess/sync/named_mutex.hpp>

l named_mutex:一个非递归命名互斥量

 

#include <boost/interprocess/sync/named_recursive_mutex.hpp>

l named_recursive_mutex:一个递归的命名互斥量

6.2.4. 范围锁

进程在读取或者写入数据后解锁互斥量是非常重要的。但是在抛出异常的代码中,实现解锁也不容易,所有通常使用一个范围锁,即使有异常抛出,也能自动解锁。要使用范围锁,需要包含头文件:

#include <boost/interprocess/sync/scoped_lock.hpp>

原理上,范围锁类在其析构函数中调用unlock()操作,这样当异常抛出是,互斥量总是能被解锁。范围锁有很多构造函数来lock,try_lock,timed_lock或者根本不锁。

using namespace boost::interprocess;

 

MutexType mutex;

{

scoped_lock<MutexType> lock(mutex); // 锁定

}

 

{

scoped_lock<MutexType> lock(mutex, try_to_lock);

 

// 检测是否获取锁

if( lock ){

 

}

}

 

{

boost::posix_time::ptime abs_time = ...

scoped_lock<MutexType> lock(mutex, abs_time);

 

if( lock ){

}

}

更多内容参考scoped_lock的参考手册。

6.2.5. 匿名互斥量举例

想象两个进程需要往一个共享内存中的循环缓冲区中写入轨迹。每个进程都需要排他地访问这个循环缓冲区,并将轨迹写入,然后继续运行。

为了保护循环缓冲区,我们可以保存一个进程共享的互斥量在这里面。每个进程将在写数据之前锁定该互斥量,并且在写入轨迹后,写入一个标志。

定义一个头文件doc_anonymous_mutex_shared_data.hpp:

#include <boost/interprocess/sync/interprocess_mutex.hpp>

 

struct shared_memory_log

{

enum { NumItems = 100 };

enum { LineSize = 100 };

 

shared_memory_log()

: current_line(0)

,end_a(false)

,end_b(false)

{}

 

boost::interprocess::interprocess_mutex mutex;

 

char items[NumItems][LineSize];

int current_line;

bool end_a;

bool end_b;

}

下面是主进程,创建共享内存,构造循环缓冲区和写轨迹:

#include <boost/interprocess/shared_memory_object.hpp>

#include <boost/interprocess/mapped_region.hpp>

#include <boost/interprocess/sync/scoped_lock.hpp>

#include “doc_anonymous_mutex_shared_data.hpp”

#include <iostream>

#include <cstdio>

 

using namespace boost::interprocess;

 

int main()

{

try{

struct shm_remove

{

shm_remove() { shared_memory_object::remove(“MySharedMemory”);}

~shm_remove() { shared_memory_object::remove(“MySharedMemory”);}

} remover;

 

shared_memory_object shm

( create_only

,”MySharedMemory”

,read_write );

 

shm.truncate(sizeof(shared_memory_log));

 

mapped_region region( shm,read_write);

 

void * addr = region.get_address();

 

shared_memory_log * data = new (addr) shared_memory_log;

 

for( int i=0;i<shared_memory_log::NumItems;++i ){

scoped_lock<interprocess_mutex> lock(data->mutex);

std::sprintf( data->items[(data->current_line++) % shared_memory_log::NumItems],”%s_%d”,”process_a”,i);

if( i==(shared_memory_log::NumItems-1) )

data->end_a = true;

}

 

// 等待另外一个进程结束

while( 1 ){

scoped_lock<interprocess_mutex> lock(data->mutex);

if( data->end_b)

break;

}

}catch( interprocess_exception & ex ){

std::cout << ex.what() << std::endl;

return 1;

}

 

return 0;

}

另外一个进程打开共享内存,获取权限访问循环缓冲区并开始写入轨迹。

#include <boost/interprocess/shared_memory_object.hpp>

#include <boost/interprocess/mapped_region.hpp>

#include <boost/interprocess/sync/scoped_lock.hpp>

#include “doc_anonymous_mutex_shared_data.hpp”

#include <iostream>

#include <cstdio>

 

using namespace boost::interprocess;

int main(){

struct shm_remove{

~shm_remove(){ shared_memory_object::remove(“MySharedMemory”);}

} remover;

 

shared_memory_object shm( open_only,”MySharedMemory”,read_write );

 

mapped_region region( shm,read_write );

 

void * addr = region.get_address();

 

shared_memory_log * data = static_cast<shared_memory_log*>(addr);

 

for( int i=0;i<100;++i ){

scoped_lock<interprocess_mutex> lock(data->mutex);

std::sprintf(data->items[(data->current_line++)%shared_memory_log::NumItems]

,”process_b_%d”,i);

if( i==(shared_memory_log::NumItems-1))

data->end_b = true;

}

 

while(1){

scoped_lock<interprocess_mutex> lock(data->mutex);

if( data->end_a)

break;

}

return 0;

}

正如我们看到的,互斥量在保护数据上非常实用,但是在通知事件给其他进程却不行。因为这个原因,我们需要使用条件变量,下一节中会使用。

6.2.6. 命名互斥量举例

现在想象两个进程都想写日志到一个文件中。他们先写自己的名称,然后写消息。因为操作系统会随意中断一个进程,这样会将两个进程的消息混在一起。因此,我们需要一种方式来写完整的消息到文件中。为了实现这个目标,我们使用命名互斥量,可以让每个进程在写日志之前锁定该互斥量:

#include <boost/interprocess/sync/scoped_lock.hpp>

#include <boost/interprocess/sync/named_mutex.hpp>

#include <fstream>

#include <iostream>

#include <cstdio>

 

int main(){

using namespace boost::interprocess;

try{

struct file_remove{

file_remove() { std::remove(“file_name”); }

~file_remove(){ std::remove(“file_name”); }

} file_remover;

 

struct mutex_remove{

mutex_remove() { named_mutex::remove(“fstream_named_mutex”); }

~mutex_remove(){ named_mutex::remove(“fstream_named_mutex”); }

} remover;

 

named_mutex mutex( open_or_create, “fstream_named_mutex” );

std::ofstream file(“file_name”);

 

for( int i=0;i<10;++i ){

scoped_lock<named_mutex> lock(mutex);

file << “process name, “;

file << “This is iteration #”<<i;

file << std::endl;

}

}catch( interprocess_exception & ex ){

std::cout << ex.what() << std::endl;

return 1;

}

 

return 0;

}

 

6.3. 条件变量

6.3.1. 条件变量是什么?

在上一个例子中,互斥量用于锁定,但是我们不能用它来进行有效的等待条件满足后再执行。而条件变量却可以实现下面两件事情:

l 等待:线程被阻塞,直到其他线程通知它可以继续执行,因为导致等待的条件已经消失。

l 通知:线程发送信号给阻塞的线程,或者给所有阻塞的线程,告诉它们等待的条件已经具备。

 

等待条件变量,往往和一个互斥量关联。互斥量必须在执行等待前,被获取。当执行等待时,线程自动解锁并等待。

6.3.2. Boost.Interprocess的条件变量类型

Boost.Interprocess提供如下条件变量:

#include <boost/interprocess/sync/interprocess_condition.hpp>

l interprocess_condition:是一个匿名条件变量。可以在共享内存或者内存映射文件保存,和boost::interprocess::interprocess_mutex配合使用。

 

#include <boost/interprocess/sync/interprocess_condition_any.hpp

l interprocess_condition_any:是一个匿名条件变量。可以在共享内存或者内存映射文件中保持。可以和任何锁类型配合使用。

 

#include <boost/interprocess/sync/named_condition.hpp>

l named_condition:是一个命名条件变量,需要和named_mutex配合使用

 

#include <boost/interprocess/sync/named_condition_any.hpp>

l named_condition_any:是一个命名条件变量,可以和任何锁一起使用。

 

命名条件变量和匿名条件变量很相似,但是需要和命名互斥量配合使用。有时我们不希望将同步对象和同步数据一起保存。

· We want to change the synchronization method (from interprocess to intra-process, or without any synchronization) using the same data. Storing the process-shared anonymous synchronization with the synchronized data would forbid this.

· We want to send the synchronized data through the network or any other communication method. Sending the process-shared synchronization objects wouldn't have any sense.

 

6.3.3. 匿名条件变量举例

设想一个进程将日志写入一个简单的共享内存中,而另外一个进程逐条打印。第一个进程写日志并且等待其他进程打印。为了实现这样的功能,我们使用两个条件变量:第一个条件变量用于阻塞发送者直到第二个进程已经打印该信息;第二个条件变量用于阻塞接收者直到有新的日志可以打印。

共享日志缓存定义在doc_anonymous_condition_shared_data.hpp中:

#include <boost/interprocess/sync/interprocess_mutex.hpp>

#include <boost/interprocess/sync/interprocess_condition.hpp>

 

struct trace_queue

{

enum { LineSize = 100 };

 

trace_queue():message_in(false){}

 

boost::interprocess::interprocess_mutex mutex;

boost::interprocess::interprocess_condition cond_empty;

boost::interprocess::interprocess_condittion cond_full;

 

char items[LineSize];

bool message_in;

};

这里是主进程。创建共享内存,写消息,直到写最后一条消息。

#include <boost/interprocess/shared_memory_object.hpp>

#include <boost/interprocess/mapped_region.hpp>

#include <boost/interprocess/sync/scoped_lock.hpp>

#include <iostream>

#include <cstdio>

#include “doc_anoymous_condition_shared_data.hpp>

 

using namespace boost::interprocess;

 

int main()

{

struct shm_remove

{

shm_remove(){ shared_memory_object::remove(“MySharedMemory”); }

~shm_remove(){ shared_memory_object::remove(“MySharedMemory”); }

} remover;

 

shared_memory_object shm(create_only,”MySharedMemory”,read_write);

try{

shm.truncate( sizeof(trace_queue) );

mapped_region region( shm,read_write);

 

void * addr = region.get_address();

 

trace_queue* data = new (addr) trance_queue;

 

const int NumMsg = 100;

for( int i=0;i<NumMsg;++i ){

scoped_lock<interprocess_mutex> lock(data->mutex);

if( data->message_in ){

data->cond_full.wait(lock);

}

 

if( i==(NumMsg-1) )

std::sprintf( data->items,”%s”,”last message”);

else

std::sprintf( data->items,”%s_%d”,”my_trace”,i);

 

data->cond_empty.notify_one();

data->message_in = true;

}

}catch( interprocess_exception & ex ){

std::cout << ex.what() << std::endl;

return 1;

}

return 0;

}

第二个进程打开共享内存并且打印消息。

#include <boost/interprocess/shared_memory_object.hpp>

#include <boost/interprocess/mapped_region.hpp>

#include <boost/interprocess/sync/scoped_lock.hpp>

#include <iostream>

#include <cstring>

#include “doc_anoymous_condition_shared_data.hpp>

 

using namespace boost::interprocess;

 

int main(){

shared_memory_object shm( open_only,”MySharedMemory”,read_write);

try{

mapped_region region( shm,read_write);

void * addr = region.get_address();

 

trace_queue* data = static_cast<trace_queue*>(addr);

 

bool end_loop = false;

do{

scoped_lock<interprocess_mutex> lock(data->mutex);

if( !data->message_in ){

data->cond_empty.wait(lock);

}

 

if( std::strcmp(data->items,”last message”)==0 )

end_loop = true;

else{

std::cout << data->items << std::endl;

data->message_in = false;

data->cond_full.notify_one();

}

} while( !end_loop );

}catch( interprocess_exception& ex ){

std::cout << ex.what() <<std::endl;

return 1;

}

return 0;

}

因为条件变量,进程可以阻塞自己,而当可继续执行的条件满足时,其他进程可以唤醒它。

 

6.4. 信号量

6.4.1. 信号量是什么?

信号量是一种进程间的同步机制。基于一个内部计数。

l 等待:测试信号量的计数,如果小于等于0,则等待。否则,减少计数。

l 公布:增加信号量的计数。如果有阻塞的进程,那么唤醒其中之一。

 

如果初始信号量计数为1,等待操作就等同于互斥量的锁操作,而公布操作就等同于解锁。这种锁也被称为二进制信号量。

虽然信号量可以像互斥量一样用,它们却有个不同于互斥量的唯一特性,公布操作不要求由等待那个进程或线程来执行。谁都可以执行。

6.4.2. Boost.Interprocess的信号量类

Boost.Interprocess提供如下类型的信号量:

#include <boost/interprocess/sync/interprocess_semaphore.hpp>

l interprocess_semaphore:是一种匿名信号量,可以保存到共享内存或者内存映射文件中。

 

#include <boost/interprocess/sync/named_semaphore.hpp>

l named_semaphore:是一种命名信号量

6.4.3. 匿名信号量使用举例

我们将在共享内存中实现一个整数数组。用于在进程间传递数据。第一个进程将会写一些整数到数组中,如果数组已经满了,则会阻塞。

第二个进程会拷贝发送过来的数据,如果没有新的数据,将会阻塞。

公共的整数数组定义在doc_anonymous_semaphore_shared_data.hpp中:

#include <boost/interprocess/sync/interprocess_semaphore.hpp>

struct shared_memory_buffer{

enum { NumItems = 10 };

shared_memory_buffer():mutex(1),nempty(NumItems),nstored(0){}

 

boost::interprocess::interprocess_semaphore mutex, nempty, nstored;

int items[NumItems];

};

下面是主处理进程。创建共享内存,放入整数到整数数组,满了则阻塞。

#include <boost/interprocess/shared_memory_object.hpp>

#include <boost/interprocess/mapped_region.hpp>

#include <iostream>

#include “doc_anonymous_semaphore_shared_data.hpp”

 

using namespace boost::interprocess;

int main(){

struct shm_remove{

shm_remove() {  shared_memory_object::remove(“MySharedMemory”); }

~shm_remove() { shared_memory_object::remove( “MySharedMemory”); }

} remover;

 

shared_memory_object shm( create_only, “MySharedMemory”,read_write );

 

shm.truncate( sizeof(shared_memory_buffer));

mapped_region region( shm,read_write);

 

void* addr = region.get_address();

shared_memory_buffer * data = new (addr) shared_memory_buffer;

 

const int NumMsg = 100;

 

for( int i=0;i<NumMsg;++i ){

data->nempty.wait();

data->mutex.wait();

data->items( i% shared_memory_buffer::NumItems] = i;

data->mutex.post();

data->nstored.post();

}

return 0;

}

第二个进程打开共享内存并且拷贝接收到的数据到其自己缓冲区:

#include <boost/interprocess/shared_memory_object.hpp>

#include <boost/interprocess/mapped_region.hpp>

#include <iostream>

#include “doc_anonymous_semaphore_shared_data.hpp”

 

using namespace boost::interprocess;

 

int main(){

struct shm_remove{

~shm_remove(){ shared_memory_object::remove(“MySharedMemory”);}

} remover;

 

shared_memory_object shm( open_only,”MySharedMemory”,read_write);

mapped_region region( shm, read_write );

 

void * addr = region.get_address();

 

shared_memory_buffer * data = static_cast<shared_memory_buffer*>( addr );

const int NumMsg = 100;

int extracte_data[NumMsg];

 

for( int i=0;i<NumMsg;++i ){

data->nstored.wait();

data->mutex.wait();

extracted_data[i] = data->items[i%shared_memory_buffer::NumItems];

data->mutex.post();

data->nempty.post();

}

return 0;

}

同样的进程间通信可以使用条件变量和互斥量来实现,但是有些同步模式,使用信号量更有效。

6.5. 可共享和可升级的互斥量

6.5.1. 可共享和可升级的互斥量是什么?

可共享和可升级互斥量是特殊的互斥量类型,比普通互斥量多一些锁的可能,我们可以考虑读取数据和修改数据。如果有些线程需要修改相同数据,那么一个普通互斥量用来保护数据防止并发访问,并发被很好的限制:两个线程读数据将会被顺序安排而不是并发执行。

如果我们允许读数据可以并发,而避免都和修改或者都修改的线程并发,显然我们可以提高性能。在应用中,这尤其是肯定的。数据读取比数据修改更加普遍。共享锁有两种锁类型:

l 独占锁:与普通互斥量类似。如果一个线程获取到一个独占锁,其他线程只能在该线程释放锁之后才能获取锁,不管是独占的还是其他的。如果其他线程有其他的非独占锁,线程想要获得独占锁,将会被阻塞。独占锁应该被那些需要修改数据的线程获取。

l 共享锁:如果一个线程获取到共享锁,其他线程不能获取独占锁。如果其他线程获取独占锁,线程想要获取共享锁将被阻塞。共享锁应该被那些需要读数据的线程取得。

 

而可升级的互斥量增加一个升级锁:

l 升级锁:获取一个升级锁,类似于获取一个特权共享锁。如果一个线程获取一个升级锁,其他线程可以获取共享锁。如果任何线程获取了排他锁或者升级锁,线程想要获取升级锁,就会被阻塞。已经取得升级锁的线程,可以确保自动获取一个排他锁,当其他线程已经获取共享锁的线程释放时。这个可以用于那些可能需要修改数据,但是大部分时间是读数据的线程。该线程获取升级锁,而其他线程可以获取共享锁。如果升级锁线程读取数据,然后还需要修改数据,线程可以提示获取一个排他锁:当所有共享线程是否它们的共享锁时,升级锁自动升级为独占锁。只能有一个线程可以获取升级锁。

 

小结:

If a thread has acquired the...

Other threads can acquire...

Sharable lock

many sharable locks

Exclusive lock

no locks

 

 

If a thread has acquired the...

Other threads can acquire...

Sharable lock

many sharable locks and 1 upgradable lock

Upgradable lock

many sharable locks

Exclusive lock

no locks

 

6.5.2. 可升级互斥量的锁转移

一个可共享的互斥量没办法自动将获取到的锁转变成其他锁。

另一方面,对于一个可升级的互斥量,已经获取该锁的线程可以尝试自动获取另外锁。所有锁转换不能保证全部成功。自动意味着在转换过程中没有其他线程会取得升级锁或者独占锁,因此数据不会被修改。

If a thread has acquired the...

It can atomically release the previous lock and...

Sharable lock

try to obtain (not guaranteed) immediately the Exclusive lock if no other thread has exclusive or upgrable lock

Sharable lock

try to obtain (not guaranteed) immediately the Upgradable lock if no other thread has exclusive or upgrable lock

Upgradable lock

obtain the Exclusive lock when all sharable locks are released

Upgradable lock

obtain the Sharable lock immediately

Exclusive lock

obtain the Upgradable lock immediately

Exclusive lock

obtain the Sharable lock immediately

我们可以看出,可升级互斥量是一个强大的机制工具,可以提升并发性。然而,如果大部分时间我们需要修改数据,或者同步代码段非常短,使用普通互斥量可能效率更高。

6.5.3. 可升级互斥量操作

所有Boost.Interprocess的可升级互斥量类型实现以下操作:

6.5.3.1. 独占锁定(可共享的和可升级的互斥量)

l void lock():调用该函数的线程一直获取独占锁。错误时抛出interprocess_exception。

l bool try_lock():调用该函数的线程尝试获取独占锁。如果有其他线程持有锁(独占锁或其他),则立即返回失败,否则立即返回成功。如果错误则抛出interprocess_exception。

l bool timed_lock( const boost::posix_time::ptime & abs_time ):调用线程在预期的时间内获取独占锁。

l void unlock():前提是该线程必须已经独占该互斥量。

6.5.3.2. 共享锁定(可共享和可升级的互斥量)

l void lock_sharable():调用该函数的线程尝试获取该互斥量的共享所有权,如果其他线程拥有该线程的独占所有权,该线程将一直等待,直到获取共享所有权成功。如果有错误则抛出interprocess_exception。

l bool try_lock_sharable():调用该函数的线程无等待地尝试获取该互斥量的共享所有权。如果没有其他线程有排他所有权,则成功。

l bool timed_lock_sharable( const boost::posix_time::ptime &abs_time ):

l void unlock_sharable():

6.5.3.3. 升级锁定(可升级互斥量)

l void lock_upgradable(): 调用线程尝试获取升所有权,如果其他线程有互斥权限或者升级权限,线程一直阻塞直到成功获取。

l bool try_lock_upgradable()

l bool timed_lock_upgradable( const boost::posix_time::ptime & abs_time )

l void unlock_upgradable()

6.5.3.4. 下调(可升级互斥量)

l void unlock_and_lock_upgradable():前提条件是该线程已经获取到独占锁。线程自动释放独占所有权,并且获取升级所有权。该操作非阻塞的。

l void unlock_and_lock_sharable(): 前提条件是该线程已经取得独占权限。线程自动释放独占所有权,并且获取共享所有权。该操作非阻塞。

l void unlock_upgradable_and_lock_sharable(): 前提是线程已经取得可升级权限。线程自动释放可升级权限,并且获取共享权限。该操作非阻塞。

6.5.3.5. 提升(可升级互斥量)

l void unlock_upgradable_and_lock(): 前提是线程已经取得可升级权限。线程自动释放可升级权限,并且尝试获取独占权限。该操作会阻塞线程直到所有共享线程释放。

l bool try_unlock_upgradable_and_lock():前提是线程已经获取到可升级权限。

l bool timed_unlock_upgradable_and_lock( const boost::posix_time::ptime & abs_time ):

l bool try_unlock_sharable_and_lock():

l bool try_unlock_sharable_and_lock_upgradable():

 

6.5.4. Boost.Interprocess可共享且可升级互斥量类型

Boost.Interprocess提供如下可共享互斥量:

#include <boost/interprocess/sync/interprocess_sharable_mutex.hpp>

l interprocess_sharable_mutex:是一个非递归的,匿名共享互斥量。可以保存到共享内存或者内存映射文件中。

 

#include <boost/interprocess/sync/named_sharable_mutex.hpp>

l named_sharable_mutex:是一个非递归的,命名共享互斥量。

 

#include <boost/interprocess/sync/interprocess_upgradable_mutex.hpp>

l interprocess_upgradable_mutex:是一个非递归的,匿名可升级互斥量。可以保存到共享内存或者内存映射文件中。

 

#include <boost/interprocess/sync/named_upgradable_mutex.hpp>

l named_upgradable_mutex: 是一个非递归的,命名可升级互斥量。

6.5.5. 可共享锁和可升级锁

对于普通互斥量,释放已经取得的锁是非常重要的,即使是遇到异常抛出。Boost.Interprocess互斥量最好和scoped_lock工具配合使用,但是这个类只提供独占锁。

可是我们还有共享锁定和升级锁定,以及可升级互斥量,我们有两个新的工具:sharable_lock和upgradable_lock。两种类型和scoped_lock相似,只是sharable_lock在构造函数中获取的是共享锁,而upgradable_lock在构造函数中获取的是可升级锁。

这两种设施可以和任何同步对象一起使用。例如:用户定义互斥量类型没有课升级锁特性,可以和sharable_lock如果同步对象提供lock_sharable()函数和unlock_sharable()操作。

6.5.5.1. 共享锁定和升级锁定头文件

#include <boost/interprocess/sync/sharable_lock.hpp>

#include <boost/interprocess/sync/upgradable_lock.hpp>

sharable_lock调用unlock_sharable()在其析构函数中,而upgradable_lock在其析构函数中调用unlock_upgradable()函数。所以,可升级互斥量总是被解锁,就算是异常发生了。

using namespace boost::interprocess;

SharableOrUpgradableMutex sh_or_up_mutex;

 

{

sharable_lock<SharableOrUpgradableMutex> lock(sh_or_up_mutex);

// some code

// The mutex will be unlocked here

}

 

{

// 这个构造函数不会自动锁定

sharable_lock<SharableOrUpgradableMutex> lock(sh_or_up_mutex, defer_lock);

 

lock.lock();

 

// some code

// 互斥量会被自动解锁

}

 

{

   //This will call try_lock_sharable()

   sharable_lock<SharableOrUpgradableMutex> lock(sh_or_up_mutex, try_to_lock);

 

   //Check if the mutex has been successfully locked

   if(lock){

      //Some code

   }

   //If the mutex was locked it will be unlocked

}

 

{

   boost::posix_time::ptime abs_time = ...

 

   //This will call timed_lock_sharable()

   scoped_lock<SharableOrUpgradableMutex> lock(sh_or_up_mutex, abs_time);

 

   //Check if the mutex has been successfully locked

   if(lock){

      //Some code

   }

   //If the mutex was locked it will be unlocked

}

 

UpgradableMutex up_mutex;

 

{

   //This will call lock_upgradable()

   upgradable_lock<UpgradableMutex> lock(up_mutex);

 

   //Some code

 

   //The mutex will be unlocked here

}

 

{

   //This won't lock the mutex()

   upgradable_lock<UpgradableMutex> lock(up_mutex, defer_lock);

 

   //Lock it on demand. This will call lock_upgradable()

   lock.lock();

 

   //Some code

 

   //The mutex will be unlocked here

}

 

{

   //This will call try_lock_upgradable()

   upgradable_lock<UpgradableMutex> lock(up_mutex, try_to_lock);

 

   //Check if the mutex has been successfully locked

   if(lock){

      //Some code

   }

   //If the mutex was locked it will be unlocked

}

 

{

   boost::posix_time::ptime abs_time = ...

 

   //This will call timed_lock_upgradable()

   scoped_lock<UpgradableMutex> lock(up_mutex, abs_time);

 

   //Check if the mutex has been successfully locked

   if(lock){

      //Some code

   }

   //If the mutex was locked it will be unlocked

}

 

6.6. 通过Move语法锁转移

Interprocess使用其自己的move语法仿真代码为那些不支持右值操作的编译器。这只是个临时的解决办法,直到Boost move语义库被引入。

范围锁及其相似设施提供简单资源管理的可能,但是对于高级互斥量,比如可升级互斥量,就存在操作自动释放锁又获取另外锁。这个时候使用可升级互斥量的unlock_and_lock_sharable()之类的操作了。

这些操作可以使用锁转移操作管理更有效。一个锁转移操作明确表示一个互斥量的锁转移到另外锁执行自动解锁加上锁操作。

6.6.1. 简单锁转移

设想一个线程最开始修改了一些数据,然后,需要在很长一段时间内读取该数据。代码可以获取独占锁,修改数据并且自动释放独占锁和锁定共享锁。

6.6.2. 锁转移总结

6.6.3. 转移未上锁的锁

6.6.4. 转移失败

6.7. 文件锁

 

6.8. 消息队列

 

7. 被管的内存段

8. 分配器、容器和内存分配算法

9. 内存分配算法

10. 直接IO流格式化:向量流和缓存流

11. 权限精灵指针

12. 体系结构和内幕

13. 用户定制Boost.Interprocess

14. 感谢、提示和连接

15. 索引和参考

  • 3
    点赞
  • 6
    收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 1
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值