【问题标题】:Aliasing physical memory on WindowsWindows 上的物理内存别名
【发布时间】:2015-10-15 15:07:51
【问题描述】:

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

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

我想做的是:

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

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

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

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

【问题讨论】:

  • 阅读我能找到的所有文档,所有对 COW 的引用都在写作的上下文中,通过 COW 映射为自己创建一个私有副本。我在内部如何工作的心理模型是通过我的问题中的异常机制之类的,我一直假设如果您要求对一系列地址进行 rw 保护,那么无论存在哪些其他映射,您都会真正得到,并且在那个世界中,由于作者的写入没有被困住,因此无法实现 reader-snapshot-via-COW 行为;我的假设错了吗?
  • 结合使用 AWE 和 VirtualProtect() 或许可以做到这一点。如果做不到这一点,则可能来自设备驱动程序。但是,写时复制不会涉及至少与原子交换和正确方法一样多的内存复制吗?无论哪种方式,写入的每个块都会被复制一次。
  • 不,AWE 不起作用:“物理页面不能同时映射到多个虚拟地址。”

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


【解决方案1】:

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

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

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

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

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

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

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

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

但是,W 每次需要进行更改时都显式地进行这些虚拟写入可能会很尴尬,例如,如果数据实际上没有按块排列,或者如果每个块内有多个虚拟字节将是不方便。因此,我们应该考虑方法C

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

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

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

    注意:我的研究表明,尽管它涉及在页面错误处理程序中故意调用页面错误,但它并不是很明确地表明它会起作用。应该支持它,因为任何异常处理程序都没有合理的方法来确保它不引用被分页的数据,但是由于我没有找到一个明确的声明,因此建议进行一些实验。

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

最终的变体通过使用单个视图来避免处理代码需要知道缓冲根本,而不管 W 是在读取还是在写入。 方法D如下:

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

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

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

我认为这种方法效率最低,因为我预计显式更改块的权限会比使用保护页面慢得多。它还可能导致页表的更多碎片。但是,如果结果证明它的性能足够好,它几乎肯定是最方便的解决方案。


一些补充说明:

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

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

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

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

  • 如果块需要以特定顺序链接,修订号可能不够,但您可以为 R 和 W 设置单独的链。同步需要您重新链接块,但这仍然可能比修改页表更快。

【讨论】:

    【解决方案2】:

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

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

    我觉得你看到的问题太笼统了,你试图在太低的抽象级别上解决它。

    并发/性能要求有多重?你能锁定你的数据库,这样读写就不会同时发生吗?

    你的观点到底是什么?能否在启动时创建视图,当新消息到达时,更新线程 W 上的 DB,并更新线程 R 上的视图?

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

    【讨论】:

    • 什么都不需要在同一个地址上看到不同的数据;它实际上是相反的,我想将相同的数据映射到不同的地址。 “为页表浪费千兆字节的 RAM”只有在不同视图的大部分数据集不同时才是正确的,并且替代方案更糟(双缓冲意味着数据集的两个完整副本,每次读取都锁定整个内容并且write 本质上意味着将读者和作者序列化,如果你正在这样做,为什么还要费心将它们放在单独的线程上?我们的想法是将它们解耦!)
    • “只有当数据集的大部分对于不同的视图是不同的时,才会为页表浪费千兆字节的 RAM”——在对数据集进行多次更改后,您将有连续的 VMEM 区域随机映射到-放置的一组 RAM 页面。每页需要两个页表条目(因为您的数据集映射到两个 VMEM 位置)。 “双缓冲意味着数据集的两个完整副本”——如果你有 16GB 的数据集,你就不可能有 16GB 的视图。即使你正在开发一个视图可能非常复杂的视频游戏,VRAM 也是 2-4GB,所以你的视图不能比这个大很多
    • “为每次读取和写入锁定整个事物本质上意味着将读取器和写入器序列化”——您是否尝试过实现锁定? C++ 临界区/C# 监视器非常高效。 “为什么还要费心把它们放在单独的线程上”——因为“接收远程数据,对其执行一些工作”将在后台线程中完成,锁定只需要“发布”更改。此外,您还可以优化:让后台线程一秒钟不发布更改,然后将它们全部发布到一个锁中。
    猜你喜欢
    • 1970-01-01
    • 2018-05-07
    • 2012-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-28
    相关资源
    最近更新 更多