9

我需要一种将页面从一个虚拟地址范围复制到另一个而不实际复制数据的方法。范围很大,延迟很重要。mremap 可以做到这一点,但问题是它也会删除旧的映射。由于我需要在多线程环境中执行此操作,因此我需要同时使用旧映射,所以稍后当我确定没有其他线程可以使用它时,我将释放它。在不修改内核的情况下,这可能吗?该解决方案只需要使用最新的 Linux 内核。

4

3 回答 3

11

这是可能的,尽管您可能需要考虑特定于体系结构的缓存一致性问题。一些体系结构根本不允许同时从多个虚拟地址访问同一页面而不会失去一致性。因此,一些架构可以很好地处理这个问题,而其他架构则不会。

编辑添加:AMD64 架构程序员手册卷。2,系统编程,第 7.8.7 节更改内存类型,状态:

一个物理页面不应该通过不同的虚拟映射分配不同的缓存类型;它们应该全部是可缓存类型(WB、WT、WP)或全部是不可缓存类型(UC、WC、CD)。否则,这可能会导致缓存一致性丢失,从而导致数据陈旧和不可预知的行为。

因此,在 AMD64 上,再次使用相同的文件或共享内存区域应该是安全mmap()的,只要使用相同的protflags;它应该导致内核对每个映射使用相同的可缓存类型。


第一步是始终为内存映射使用文件支持。使用mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_NORESERVE, fd, 0)以便映射不保留交换。(如果您忘记了这一点,那么您将比许多工作负载的实际实际限制更快地遇到交换限制。)由文件备份引起的额外开销绝对可以忽略不计。

编辑添加:用户 strcmp 指出当前内核不将地址空间随机化应用于地址。幸运的是,这很容易解决,只需将随机生成的地址提供给mmap()而不是NULL. 在 x86-64 上,用户地址空间是 47 位的,地址应该是页对齐的;您可以使用例如Xorshift*来生成地址,然后屏蔽掉不需要的位:& 0x00007FFFFE00000例如,将给出 2097152 字节对齐的 47 位地址。

因为支持是一个文件,所以您可以在使用ftruncate(). 只有在合适的宽限期之后——当你知道没有线程正在使用映射时(也许使用原子计数器来跟踪它?)——你取消映射原始映射。

在实践中,当一个映射需要放大时,你先放大后备文件,然后mremap(mapping, oldsize, newsize, 0)在不移动映射的情况下尝试查看映射是否可以增长。只有当就地重新映射失败时,才需要切换到新映射。

编辑添加:您肯定想要使用mremap()而不是仅仅使用mmap()MAP_FIXED创建更大的映射,因为取消映射mmap()(原子地)任何现有映射,包括那些属于其他文件或共享内存区域的映射。使用mremap(),如果放大的映射与现有映射重叠,则会出现错误;使用mmap()MAP_FIXED,新映射重叠的任何现有映射都将被忽略(未映射)。

不幸的是,我必须承认我没有验证内核是否检测到现有映射之间的冲突,或者它只是假设程序员知道这种冲突——毕竟,程序员必须知道每个映射的地址和长度,因此应该知道映射是否会与另一个现有的映射发生冲突。编辑添加:3.8 系列内核会,如果放大的映射MAP_FAILEDerrno==ENOMEM现有映射发生冲突,则返回。我希望所有 Linux 内核的行为方式都相同,但除了在 x86_64 上对 3.8.0-30-generic 进行测试外,没有任何证据。

另请注意,在 Linux 中,POSIX 共享内存是使用特殊文件系统实现的,通常是安装在/dev/shm(或作为符号链接)/run/shm的tmpfs。等/dev/shmshm_open()都是由 C 库实现的。我个人不会使用大型 POSIX 共享内存功能,而是使用专门安装的 tmpfs 用于自定义应用程序。如果没有其他原因,安全控制(能够在其中创建新“文件”的用户和组)管理起来要容易得多,也更清晰。


如果映射是并且必须是匿名的,您仍然可以使用它mremap(mapping, oldsize, newsize, 0)尝试调整它的大小;它可能会失败。

即使有数十万个映射,64 位地址空间也是巨大的,而且失败的情况很少见。所以,虽然你也必须处理失败的情况,但不一定要很快 编辑修改:在 x86-64 上,地址空间是 47 位,映射必须从页边界开始(普通页 12 位,2M 巨页 21 位,1G 巨页 30 位),所以只有地址空间中有 35、26 或 17 位可用于映射。因此,即使建议使用随机地址,冲突也会更加频繁。(对于 2M 个映射,1024 个映射偶尔会发生碰撞,但在 65536 个映射中,发生碰撞(调整大小失败)的概率约为 2.3%。)

编辑添加:用户 strcmp 在评论中指出,默认情况下 Linuxmmap()将返回连续地址,在这种情况下,除非它是最后一个,否则映射将始终失败,或者映射只是在那里未映射。

我知道在 Linux 中工作的方法很复杂,而且非常特定于体系结构。您可以以只读方式重新映射原始映射、创建新的匿名映射并在那里复制旧内容。您需要一个SIGSEGV处理程序(SIGSEGV为尝试写入现在只读映射的特定线程引发信号,这是SIGSEGVLinux 中为数不多的可恢复情况之一,即使 POSIX 不同意)检查导致问题的指令,模拟它(改为修改新映射的内容),然后跳过有问题的指令。在宽限期之后,当不再有线程访问旧的、现在是只读的映射时,您可以拆除该映射。

当然,所有的麻烦都在SIGSEGV处理程序中。它不仅必须能够解码所有机器指令并模拟它们(或至少是那些写入内存的指令),而且如果新映射尚未完全复制,它还必须忙于等待。它很复杂,绝对不可移植,并且非常特定于架构......但可能。

于 2013-09-19T22:25:40.757 回答
4

是的,你可以这样做。

mremap(old_address, old_size, new_size, flags) 仅删除大小为“old_size”的旧映射。因此,如果您将 0 作为“old_size”传递,它根本不会取消映射任何内容。

注意:这仅适用于共享映射,因此此类 mremap() 应用于先前使用 MAP_SHARED 映射的区域。这实际上就是所有这些,即您甚至不需要文件支持的映射,您可以成功地将“MAP_SHARED | MAP_ANONYMOUS”组合用于 mmap() 标志。一些非常旧的操作系统可能不支持“MAP_SHARED | MAP_ANONYMOUS”,但在 linux 上你是安全的。

如果您在 MAP_PRIVATE 区域上尝试这样做,结果将与 memcpy() 大致相似,即不会创建内存别名。但它仍将使用 CoW 机器。从您最初的问题中不清楚您是否需要别名,或者 CoW 副本也可以。

更新:为此,您还需要明确指定 MREMAP_MAYMOVE 标志。

于 2016-03-02T11:34:03.763 回答
3

这是在 5.7 内核中作为新标志添加到 mremap(2) 中的,称为 MREMAP_DONTUNMAP。这会在移动页表条目后保留现有映射。

https://github.com/torvalds/linux/commit/e346b3813067d4b17383f975f197a9aa28a3b077#diff-14bbdb979be70309bb5e7818efccacc8

于 2020-04-22T23:04:17.920 回答