Boost.Interprocess使用手册翻译之十二.架构和内部实现(Architecture and internals)

来源:互联网 发布:知乎日报rss地址 编辑:程序博客网 时间:2024/06/03 19:11

十二.架构和内部实现

基本指南

从内存算法到托管内存片段

分配器和容器

Boost.Interprocess的性能

基本指南

当构建Boost.Interprocess架构时,我采用了一些基本指南,它们被总结为以下几点:

  • Boost.Interprocess至少能在UNIX和Windows系统间移植。这意味着不仅要统一接口,还有行为。这就是为什么Boost.Interprocess为共享内存和命名同步机制选择了内核或文件系统持久化。共享内存的进程持久化也是可取的,但在UNIX系统下实现起来相当困难。
  • Boost.Interprocess进程间同步原语(primitives)应该和线程同步原语相同。Boost.Interprocess旨在有一个和C++标准线程API兼容的接口。
  • Boost.Interprocess应该是模块化、可定制,但高效率的。这就是为什么Boost.Interprocess基于模板和内存算法,索引类型、互斥类型和其他类都是模板化的。
  • Boost.Interprocess架构应该允许和基于线程编程的相同的并发性。不同的互斥水平被定义以便当扩展一个共享内存向量时,一个进程能够同时分配原始内存,并且另一个进程能安全地搜索一个具名对象。
  • Boost.Interprocess容器完全不了解Boost.Interprocess。所有具体的行为都包含在类STL分配器中。这允许STL提供商轻微的修改(或者更好的说法,泛化)它们标准的容器实现并且获得一个完全的std::allocator和boost::interprocess::allocator兼容容器。这也使得Boost.Interprocess容器与标准算法兼容。

Boost.Interprocess构建于三个基本类上:内存算法,片段管理器,托管内存片段:

从内存算法到托管内存片段

内存算法(The memory algorithm)

片段管理器(The segment manager)

Boost.Interprocess托管内存片段(managed memory segments)

内存算法(The memory algorithm)

memory algorithm是一个对象,它被置于共享内存/内存映射文件片段的头几个字节中。memory algorithm能够返回此片段的部分至用户,并标记它们为已用,并且用户能够返回这些部分至memory algorithm以便memory algorithm再次标记它们为空闲。尽管有一个例外:一些超过内存算法对象尾部的字节被保留,并且不能被用于动态分配。这些“保留”区域将被用于放置其他额外的对象在一个众所周知的地方。

总结一下,memory algorithm具有和标准C库函数malloc/free相同的使命,但它仅能返回其置于的片段的部分。一个内存片段的布局可能是:


memory algorithm照顾到了内存同步,就好像malloc/free保证两个线程能同时调用malloc/free。它通常采用放置一个进程间共享的互斥量做为memory algorithm成员的方式实现。需要小心的是memory algorithm完全不了解内存片段(如果它是共享内存、共享内存文件等等)。对memory algorithm来说,内存片段仅是一块固定大小的内存缓冲区而已。

memory algorithm也是其余Boost.Interprocess框架(片段管理器、分配器、容器等)的一个配置点,因为它定义了两种基本类型做为成员类型定义:

[cpp] view plain copy
  1. typedef /*implementation dependent*/ void_pointer;  
  2. typedef /*implementation dependent*/ mutex_family;  

void_pointer类型定义定义了指针类型,它将被用在Boost.Interprocess框架(片段管理器、分配器、容器等)中。如果memoryalgorithm准备放置入映射在不同基地址上的共享内存/内存映射文件中时,此指针类型将被定义为offset_ptr<void>或一个类似的相对指针。如果memory algorithm将被与固定地址映射一起使用,void_pointer能被定义为void*。

Boost.Interprocess memory algorithm其余的接口在Writing a new shared memory allocation algorithm 章节中描述。做为memory algorithm的例子,你可以看simple_seq_fit或 rbtree_best_fit类的实现。

片段管理器(The segment manager)

segment manager是一个对象,它也被置于托管内存片段(共享内存、内存映射文件)的头几个字节中,它构建于memory algorithm之上,提供了更复杂的服务。为什么segment manager和memory algorithm能同时置于片段的头几个字节中呢?这是因为segment manager包含了memory algorithm:真相是memory algorithm嵌入在segment manager中:


segment manager初始化memory algorithm,并且告诉内存管理器它不使用此部分内存,这块内存用于其余segment manager的成员的动态分配。segment manager的其他成员是一个递归互斥量(由memory algorithm的mutex_family::recursive_mutex类型定义成员定义)和两个索引(maps):一个用于执行具名分配,另一个用于执行“唯一实例”分配。

  • 第一个索引是一个map,它以一个指向c-string(具名对象的名字)的指针做为键,和一个动态分配对象信息的结构(最重要的是对象的地址和大小)。
  • 第二个索引用于实现“唯一实例”,基本上与第一个索引相同,但是对象的名称来源于typeid(T).name()操作。

用于在索引中存储键值对(pair)的内存也是通过memory algorithm分配的,因此我们可以告诉大家,内部索引与构建在内存片段上的普通用户对象是相似的。其余用于存储对象名称、对象自身和构造/析构元数据的内存则在一个单独的allocate()调用中采用memory algorithm分配。

如上示,segment manager完全不了解共享内存/内存映射文件。segment manager本身不分配内存片段部分,它只是调用memory algorithm从剩余的内存片段中分配所需的内存。segment manager是一个构建在memory algorithm之上的类,它提供具名对象构建、唯一实例构建以及很多其他服务。

segment manager在Boost.Interprocess中是由segment_manager类实现的。

[cpp] view plain copy
  1. template<class CharType  
  2.         ,class MemoryAlgorithm  
  3.         ,template<class IndexConfig> class IndexType>  
  4. class segment_manager;  

如上示,segment manager非常通用:我们能指定用于区分具名对象的字符类型,我们能指定动态控制内存片段部分的memory algorithm ,并且我们也能指定存储映射(名指针,对象信息)的索引类型。我们能构建我们自己的索引类型,就好像Buildingcustom indexes 章节中解释地一样。

Boost.Interprocess托管内存片段(managed memory segments)

构建共享内存/内存映射文件的Boost.Interprocessmanaged memory segments放置segment manager并且发送用户的请求至segment manager。例如,basic_managed_shared_memory 是一个Boost.Interprocess托管内存片段,它与共享内存一起工作。basic_managed_mapped_file与内存映射文件一起工作......。

基本上,Boost.Interprocess managed memory segment的接口与segment manager相同,但它也提供了函数来“打开”、“创建”或“打开或创建”共享内存/内存映射文件片段并且初始化所有需要的资源。托管内存片段类不是构建在共享内存或内存映射文件中,它们是通常的C++类,存储了一个segment manager(构建在享内存或内存映射文件上)的指针。

此外,managed memory segments提供了特定的函数:managed_mapped_file提供函数用于刷新内存内容至文件,managed_heap_memory提供函数用于扩展内存,……

大部分Boost.Interprocess managed memory segments的函数能够在所有托管内存片段间共享,因为很多时候它们仅发送函数至segment manager。基于此,在Boost.Interprocess中,所有的托管内存片段都派生于一个共同的类,此类实现了内存独立(memory-independent)函数(共享内存、内存映射文件):boost::interprocess::ipcdetail::basic_managed_memory_impl。

派生于此类,对不同的内存后端,Boost.Interprocess实现了数个托管内存类:

  • basic_managed_shared_memory (for shared memory).
  • basic_managed_mapped_file (for memory mapped files).
  • basic_managed_heap_memory (for heap allocated memory).
  • basic_managed_external_buffer (for user provided external buffer).

分配器和容器

Boost.Interprocess分配器

Boost.Interprocess 独立存储池的实现

Boost.Interprocess自适应池的实现

Boost.Interprocess容器

Boost.Interprocess分配器

Boost.Interprocess的类STL(STL-like)分配器是相当简单的,且遵从通常的C++分配器方法。通常,STL容器的分配器基于new/delete操作之上,在此之上它们实现了池(pools)、竞技场(arenas)和其他分配技巧。

在Boost.Interprocess分配器中,方法是相似的,但是所有分配器都是基于segment manager。segment manager是唯一一个提供从简单内存分配到具名对象创建的。Boost.Interprocess分配器总是存储一个segment manager指针,以便它们能从内存片段获取内存或在分配器间共享普通池。

正如你能想到的,分配器的成员指针不是原始指针,而是由segment_manager::void_pointer定义的指针类型。此外,Boost.Interprocess分配器的指针类型定义也与segment_manager::void_pointer具有相同的类型。

这意味着如果我们的分配器算法定义 void_pointer为offset_ptr<void>, boost::interprocess::allocator<T>将存储一个offset_ptr<segment_manager> 指向segment manager,且boost::interprocess::allocator<T>::pointer类型将是offset_ptr<T>。这样,Boost.Interprocess分配器能被置于segment manager托管的内存片段中,即共享内存、内存映射文件等。

Boost.Interprocess独立存储池的实现

独立存储池是简单的,它遵从经典独立存储算法。

  • 内存池使用segment manager的原始内存分配函数来分配内存块。
  • 内存块包含了一个指针用于组织一个单链表内存块。内存池将包含指向第一个内存块的指针。
  • 内存块的其余部分被分割为请求大小的节点,并且没有内存被用作每个节点的负载。由于空闲节点的内存未被使用,因此此内存被用来放置一个指针用于组织一个空闲节点的单链表。内存池具有一个指向第一个空闲节点的指针。
  • 分配一个节点仅仅是从链表中获取第一个空闲节点。如果链表为空,则分配一个新的内存块,链接在内存块链表中,然后新的空闲内存被链接至空闲节点链表中。
  • 析构就是返回节点至空闲节点链表中。
  • 当内存池销毁后,内存块链表被遍历并且内存返还至segment manager。

内存池由private_node_pool and shared_node_pool类实现。

Boost.Interprocess自适应池的实现

自适应池是独立链表的一个变种,但它们具有更复杂的方法:

  • 内存池使用segment manager分配对其的内存块,而不是使用原始分配器。这是一个必须的特性,因为节点能够通过应用一个简单的伪装(mask)至其地址上来获取它的内存块信息。
  • 内存块包含了指针以用于形成一个内存块的双向链表,同时也包含了一个额外的指针用于创建一个内存块中空闲节点的单向链表。因此与独立存储算法不同,空闲节点链表作用于每个块上。
  • 内存池以空闲节点增序的方式维护内存块。这能提高局部性,同时减少跨内存块节点分配时的离散性,促进了完全空闲内存块的建立。
  • 内存池具有一个指向最小(但不是零)空闲节点内存块的指针。此内存块被称为“激活的”内存块。
  • 分配一个节点仅仅是返回“激活的”内存块的第一个空闲节点。内存块链表按照空闲节点总数重排序。如果需要的话,“激活的”内存池指针也被更新。
  • 如果内存池耗尽了节点,则一个新的内存池被分配,然后推入内存块链表的尾部。如果需要的话,“激活的”内存池指针也被更新。
  • 析构就是返回节点至空闲节点链表中,然后相应的更新“激活的”内存池。
  • 如果完全空闲块的数目超过限度,内存块被返还至segment manager。
  • 当内存池被销毁,内存块链表被遍历并且内存返还至segment manager。

自适应池由 private_adaptive_node_pool and adaptive_node_pool类实现。

Boost.Interprocess容器

Boost.Interprocess容器是boost::interprocess名空间中标准STL容器的对应,但具有下面一些细节:

  • Boost.Interprocess容器不假设被一个分配器分配的内存能被其他同类型的分配器释放。它们总是用operator==() 来比较分配器以便了解是否可行。
  • Boost.Interprocess容器的内部结构指针具有与容器分配器定义的指针类型相同的类型。这允许放置容器至映射在不同基地址上的托管内存片段中。

Boost.Interprocess的性能

原始内存分配器的性能

具名分配器的性能

此小节尝试解释Boost.Interprocess的性能特性,以便如果你需要更好性能时能优化Boost.Interprocess的使用。

原始内存分配器的性能

使用Boost.Interprocess,你能有两种类型的原始内存分配器:

  • 显式的:用户调用 managed_shared_memory/managed_mapped_file等的allocate()和deallocate()函数来托管内存片段。这些调用被翻译为MemoryAlgorithm::allocate()函数,这意味着你仅需要关联托管内存片段的内存算法需要分配数据的时间。
  • 隐式的:例如,你在与Boost.Interprocess的容器一起使用boost::interprocess::allocator<...> 。每一次vector/string必须重分配它的缓冲区或每一次你插入一个对象至节点容器时,此分配器调用与显式方法相同的MemoryAlgorithm::allocate()函数,。

如果你认为这种内存分配是你应用程序的一个瓶颈,你有如下的替代方案:

  • 如果你使用map/set关联容器,尝试使用flat_map家族来替换map家族,如果你主要执行搜索并且插入/删除主要是在初始化时进行。现在开销主要是当有序向量必须重分配其存储和移动数据时。当你预先知道你将插入多少数据时,你也能调用这些容器的reserve()方法。然而,在这些容器中,在插入操作时迭代器会失效,因此此替代方案仅在某些应用中有效。
  • 对节点容器使用Boost.Interprocess池分配器,因为仅当池子中节点用光时,池分配器调用allocate()。这是相当有效的(大大高于目前缺省的通用算法),并且能节省很多内存。更多详情参考Segregated storage node allocators 和 Adaptive node allocators
  • 写你自己的内存分配算法。如果你有内存分配算法的经验并且你认为另一种算法比缺省的更适应你的程序,你能在所有Boost.Interprocess托管内存片段中指定它。参考章节 Writing a new shared memory allocation algorithm来了解如何使用它。如果你认为对通常的程序,它比缺省的更好,请把它贡献给Boost.Interprocess,做为缺省分配算法。

具名分配器的性能

Boost.Interprocess允许与两个线程写入到一个共同的结构相同的并行,除了当用户创建/搜索命名/唯一对象时。创建一个具名对象的步骤如下:

  • 锁定一个循环互斥量(以便你能使对象构造函数内部的具名分配器被构建)。
  • 尝试插入[具名指针,对象信息]至具名/对象索引中。此查找必须保证名字在之前没被使用过。这通过在索引中调用insert()函数实现。因此此操作的时间依赖于索引类型(有序向量、树、哈希等)。如果索引必须被重分配时,它需要调用内存算法分配函数,它是一个节点分配器,使用了池分配…
  • 分配一个缓冲区来放置对象的名称、对象本身和析构元数据(对象数目等等)。
  • 调用创建对象的构造函数。如果是一个数组,则每个数组成员一个构造函数。
  • 解锁循环互斥量。

当使用对象名销毁一个具名对象时(destroy<T>(name)),步骤如下:

  • 锁定一个循环互斥量
  • 在索引中搜索关联此名称的条目。拷贝那些信息并删除索引项。这些是通过使用索引的find(const key_type &)和erase(iterator)成员完成的。如果索引是一个平衡树或有序向量等时,这需要成员重排序。
  • 调用对象的析构函数(如果是一个数组,需要调用很多次)。
  • 使用分配算法释放包含了名字、元数据和对象自身的内存缓冲区。
  • 解锁循环互斥量。

当使用对象指针销毁一个具名对象时(destroy_ptr(T *ptr)),步骤如下:

  • 锁定一个循环互斥量
  • 依赖于索引类型,以下可能是不同的:
    • 如果索引是节点索引(通过boost::interprocess::is_node_index指定):取存储在对象旁的迭代器,然后调用erase(iterator)。如果索引是一个平衡树或有序向量等时,这需要成员重排序。
    • 如果不是节点索引:取存储在对象旁的名称然后调用erase(const key &)删除索引项。如果索引是一个平衡树或有序向量等时,这需要成员重排序。
  • 调用对象的析构函数(如果是一个数组,需要调用很多次)。
  • 使用分配算法释放包含了名字、元数据和对象自身的内存缓冲区。
  • 解锁循环互斥量。

如果你认为性能还不够好,你还有如下替代方案:

  • 可能是锁得时间太久而影响了并发性的问题。尝试减少全局索引中具名对象的数目,并且如果你的应用服务于几个客户端,尝试为每个客户端构建一个新的托管内存片段,而不是共用一个。
  • 如果你觉得缺省的不够快,使用其他Boost.Interprocess索引类型。如果你还不满足,就自己写一个索引类型。可以参考Building custom indexes。
  • 使用指针析构至少和使用对象名字析构是一样快的,并且可以更快一些(例如,在节点容器中)。因此如果问题在于过多的具名析构时,请尝试使用指针。如果索引是节点索引时,你能节省一些时间。
0 0