【问题标题】:Linux mremap without freeing the old mapping?Linux mremap 没有释放旧映射?
【发布时间】:2020-04-23 07:04:17
【问题描述】:

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

【问题讨论】:

  • 出于好奇,如果内存仍在旧地址被访问,为什么还要重新映射?
  • 是否有另一种机制可以将相同的页面映射到不同地址的新的、更大的映射?这将回答这个问题。
  • 我不知道你的问题的答案(AFAIK 这是不可能的),但我很好奇你为什么需要它。毕竟,如果你只需要扩大映射,那应该可以不用重新定位它,只要你保持相邻的映射间隔良好,除非你运行的是 32 位内核,否则这应该不是问题。跨度>
  • 在 x64 上可能存在数十万个大型映射,这既困难又不可靠。我宁愿修改内核。这些映射实际上是映射的 shm_open 名称。我认为可以使用 ftruncate 扩展映射,然后在同一进程中将新的较大区域映射到重叠的新 mmap 中。这可能吗?
  • 您是在映射实际文件还是匿名区域?我认为如果它是一个文件,那么你可以再次使用mmap同一个文件并获得一组不同的地址。

标签: c linux


【解决方案1】:

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

编辑添加:AMD64 Architecture Programmer's Manual vol. 2, System Programming,第 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() 放大备份文件后创建到同一个文件的第二个映射。只有在合适的宽限期之后——当你知道没有线程正在使用映射时(也许使用原子计数器来跟踪它?)——你取消映射原始映射。

实际中,当一个映射需要放大的时候,你先把backing文件放大,然后试试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/shm 是符号链接)。 shm_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 在评论中指出,默认情况下 Linux mmap() 将返回连续地址,在这种情况下,除非它是最后一个,否则映射总是会失败,或者映射只是在那里未映射。

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

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

【讨论】:

  • 我在谷歌上搜索 x64 虚拟地址别名时遇到了问题,它是否有效,你知道吗?问题是我有多达 1000 个进程,每个进程都使用大量的虚拟地址空间,在一个有 256GB 内存和只有 512GB 磁盘的盒子上,其中一半被指定用于其他用途。所以文件支持是不可能的,但是 afaik shm_open 句柄就像可命名的、可共享的映射一样工作。由于它们是可命名的,因此您可以尝试在同一进程中映射同一个(即将测试内核对此做了什么。)
  • 失败了,我更喜欢在内核中更改 mremap(添加一个额外的标志来保留旧映射。)我想这相对容易,甚至可能在上游被接受,但是我的想象力没有t 总是与现实相交。
  • mremap 根本不需要,可以映射 shm_open fd,用 ftruncate 扩展,然后在新的重叠映射中再次映射。你得到两个单独的虚拟地址,写入一个并从另一个读取就可以了。请更新您的答案,以便以后找到此答案的任何人都清楚这一点,我会接受。
  • @Eloff,wrt。没有mremap(): mmap()MAP_FIXED 标志将愉快地覆盖ALL 现有映射,甚至那些属于其他文件(或共享内存区域,在Linux 中基本相同)的映射。因此,如果您使用mmap(addr,newsize,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_NORESERVE|MAP_FIXED,fd,0) 来“增长”该区域,那么如果新区域与另一个映射重叠,您将不会收到错误消息;它只会成功。另一方面,使用mremap(),如果放大的映射与另一个映射发生冲突,则会出现错误。所以,你确实需要使用mremap()
  • @strcat:也许您应该考虑提供自己的答案,而不是仅仅因为您的“感觉”而投反对票?坦率地说,如果您无法指出这些问题是无用的还是微不足道的,我认为答案会被否决。我有没有让你生气?我关心的唯一原因是我关心我的答案的质量——我自己从不对其他答案投票——并且随时准备承认我的错误,并尝试修复它。到目前为止,我有点喜欢在我的 200 个答案中只有两个反对票。你的是第三个,第一个我完全看不懂。
【解决方案2】:

是的,你可以这样做。

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副本也可以。

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

【讨论】:

  • “如果你将 0 作为“old_size”传递,它根本不会取消映射任何东西”——这在技术上是正确的,但这也意味着从一开始就不会重新映射任何虚拟地址,从而进行调用到 mremap 没用,让 OP 的问题没有解决。页面确实从 [old_address, old_address + min(old_size, new_size)] 移动到 [new_address, new_address + min(old_size, new_size)]。
  • 请检查您的事实。你说的完全是错误的,任何小的测试用例都可以证实这一点。
  • 还有一点不清楚 OP 真正想要什么:他需要内存别名,还是需要原始区域的 CoW 副本?
  • 这个(能够重新映射 MAP_PRIVATE)是一个很好的功能,但似乎不再支持,因为它被视为一个错误:man7.org/linux/man-pages/man2/mremap.2.html
【解决方案3】:

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

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

【讨论】:

  • 相比将 0 设置为 old_size 到 mremap 有什么优势?
猜你喜欢
  • 2018-03-06
  • 2011-07-22
  • 2022-12-03
  • 2012-08-13
  • 2019-01-27
  • 1970-01-01
  • 1970-01-01
  • 2011-02-02
  • 1970-01-01
相关资源
最近更新 更多