在Windows上混淆物理内存

时间:2015-10-15 15:07:52

标签: windows database-design shared-memory virtual-memory

我有两个线程和一个大型数据集。线程R从数据集连续读取并向用户呈现数据视图。线程W不断接收远程数据,对其执行一些工作并将其发布到数据集。

线程R需要控制接收数据集一致视图的粒度。一种解决方案是双缓冲; W写入一个副本而R从另一个副本读取,当R准备好进行更新时,W的副本被原子复制到R(禁止,因为数据集很大并且大部分未更改)或者它们原子地交换副本而W带来R的旧通过重新应用自上次交换以来的增量更改来复制最新版本(烦人地跟踪这些,并且烦恼所有增量必须被处理两次)。

我想做的是以下内容:

  • 两个线程独立保留虚拟只读内存范围,并且两个范围都映射到相同的物理页面集
  • 线程W安装一个异常处理程序,用于捕获对只读页面的写入,抓取一个新的物理块,将其写入readwrite然后让写入重新尝试
  • 当R想要更新时,原子地(R已经替换的视图中的任何物理页面被释放(或返回到池中),然后这些虚拟内存地址得到W的新物理页面的支持,然后W标记其整个再次读取范围)。

这可以避免额外的内存副本,需要跟踪和重新应用增量等。

然而,AFAICT虽然Windows确实允许创建共享内存区域(甚至是自动写入时复制内存区域),但它似乎无法以任何方式显式映射物理页面。 W可以使用它将新视图发布到R。

我有什么遗漏的吗? - 是否有可能实现这样的事情,一个纯粹通过改变页面映射而没有内存复制的发布步骤?

2 个答案:

答案 0 :(得分:1)

我认为应该可以通过一点点诡计来做一些接近你所要求的事情。

我将首先描述我认为最简单,最有效但最不灵活的方法,并将其称为方法A 。要使用这种方法,数据必须按块排列,每个块必须完全包含在一个页面中:

  • 使用相同的文件映射对象为R创建一个读/写视图,为R创建一个写时复制视图。

  • 每当W想要修改数据块时,它首先在写时复制视图中对相应的块执行虚拟写操作。

    注意:我认为写入页面会导致写入时写入,即使写入实际上没有改变内容,但为了安全起见,我建议避免这种假设,你可以通过在每个数据块中包括一个虚拟字节,即R将忽略的虚拟字节。然后,W可以递增虚拟字节以确保复制相应的页面。

  • 要进行同步,请丢弃现有的写时复制视图并创建一个新视图。

我希望不必要的虚拟写入的开销可以忽略不计,但是对齐块以使它们不与页面边界重叠可能会很不方便。

如果是这样,接近B 与方法A相同,除了有多个虚拟字节,间隔放置确保每个与块重叠的页面包含至少一个虚拟字节。这增加了虚拟写入的开销,但我不希望它过多。

然而,W每次需要进行更改时都明确地进行这些虚拟写操作可能会很尴尬,例如,如果数据实际上不是以块的形式排列,或者每个块内部有多个虚拟字节,那么不方便。因此,我们应该考虑方法C

  • 为W创建只读和读写视图,以及为R创建写时复制视图。使用只读视图读取数据但读写查看写作。

  • 使用VirtualProtect和PAGE_GUARD保护读写视图中的所有页面。

  • 当触发保护页面错误时,让异常处理程序在写时复制视图中对相应页面进行虚拟写入。向量异常处理程序在我看来就像最干净的选项。

    注意:我的研究表明,尽管它涉及故意在页面错误处理程序中调用页面错误这一事实,但这不会非常明确。应该支持它,因为没有合理的方法让任何异常处理程序确保它不引用被分页的数据,但是因为我没有找到一个明确的声明,所以建议进行一些实验。 < / p>

方法C的效率可能低于A或B,因为它需要处理额外的页面错误异常,并且需要相应的额外往返内核模式。我也不确定跟踪保护页面所涉及的页表开销。但是,它可能更方便,因为从处理代码中删除虚拟写入会减少代码需要知道缓冲的程度。

最终变体通过使用单个视图避免了处理代码需要知道所有的缓冲,无论W是读还是写。 方法D 如下:

  • 为W创建一个读/写视图,为R创建一个写时复制视图。

  • 使用VirtualProtect将读/写视图的所有页面上的权限更改为只读。

  • 当触发页面错误时,让异常处理程序将错误页面上的权限更改为读/写,并在写时复制视图中对相应页面进行虚拟写入。

我认为这种方法效率最低,因为我希望显式更改块上的权限会比使用保护页面慢得多。它还可能导致页表碎片更多。但是,如果事实证明它能够充分发挥作用,那几乎肯定是最方便的解决方案。

一些附加说明:

我相信所有这些方法都应该有效,但需要注意的是在处理第一页错误时触发第二页错误时会发生什么。我对不同变体的比较效率没有信心。进行一些比较测试可能是明智的。

文件映射对象可能应该由页面文件支持,您可能希望尝试使用大页面。这会增加需要复制的数据量,但会减少页表的负载。同样,比较测试可能是合适的。

我假设您已经考虑过这一点,但未来的读者应该注意,根据数据的性质,使用映射可能并不明智。例如:

  • 可以为数据块提供两个修订号,一个指示块何时生效,另一个指示应该被视为删除。这种方法只需要很少的时间开销,R只需要在处理块时检查修订号,以便它可以跳过太新的块并删除过时的块。这涉及较少的数据复制:W只需要复制它正在处理的数据块,而不是整个页面,添加/删除块根本不需要复制任何数据。

  • 如果块需要按特定顺序链接,修订版号可能不够,但是你可能有R和W的单独链。同步会要求你重新链接块,但这仍然可能比修改页面表更快。

答案 1 :(得分:0)

即使有人会创建你想要的API,也可以做那个CoW的东西。 即使您将切换到单独的进程而不是线程(每个进程只有1个页表,您不能让2个线程在同一地址上看到不同的数据)。

如果你要修改/重新映射随机的4kb页面,你会waste gigabytes of RAM for your page table。它不仅会浪费RAM,还会降低性能。

我觉得你看到的问题太笼统了,你试图在抽象程度太低的情况下解决它。

并发/性能要求有多重?你能锁定你的数据库,以便读写永远不会同时发生吗?

您的观点到底是什么?你可以在启动时创建视图,当新消息到达时,更新线程W上的数据库,并更新线程R上的视图吗?

整个事情是持久的吗?如果是,只需使用具有事务隔离功能的嵌入式NoSQL引擎。例如ESENT,或者如果您的软件是跨平台的,LMDB可能适合。