我需要一种方法将页面从一个虚拟地址范围复制到另一个虚拟地址范围而不实际复制数据。范围很大,延迟很重要。 mremap可以做到这一点,但问题是它还会删除旧的映射。因为我需要在多线程环境中执行此操作,所以我需要同时使用旧映射,稍后当我确定没有其他线程可以使用它时,我将释放它。这有可能,但是hacky,没有修改内核?该解决方案只需要使用最新的Linux内核。
答案 0 :(得分:9)
虽然存在特定于体系结构的缓存一致性问题,但您可能需要考虑这些问题。某些体系结构根本不允许同时从多个虚拟地址访问同一页面而不会失去一致性。因此,有些架构会管理这个,其他架构则不会。
编辑添加:AMD64 Architecture Programmer's Manual vol. 2, System Programming,第7.8.7节更改内存类型,状态:
物理页面不应该通过不同的虚拟映射分配给它的不同缓存类型;它们应该是所有可缓存类型(WB,WT,WP)或所有非缓存类型(UC,WC,CD)。否则,这可能会导致缓存一致性丢失,从而导致陈旧数据和不可预测的行为。
因此,在AMD64上,只要使用相同的mmap()
和prot
,flags
同一个文件或共享内存区域应该是安全的。它应该使内核对每个映射使用相同的可缓存类型。
第一步是始终使用内存映射的文件备份。使用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_FAILED
errno==ENOMEM
。我希望所有的Linux内核都能以相同的方式运行,但除了在x86_64上测试3.8.0-30-generic之外没有任何证据。
另请注意,在Linux中,POSIX共享内存是使用特殊文件系统实现的,通常是在/dev/shm
(或/run/shm
上安装的/dev/shm
作为符号链接的tmpfs)。 shm_open()
等。 al由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
处理程序中。它不仅必须能够解码所有机器指令并模拟它们(或者至少是那些写入内存的指令),而且它还必须忙 - 等待新映射尚未完全复制。它很复杂,绝对不可移植,并且非常具有架构特性......但可能。
答案 1 :(得分: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机械。你最初的问题是否不清楚 你需要一个别名,或者CoW副本也可以。
更新:为此,您还需要指定MREMAP_MAYMOVE标志 明显。
答案 2 :(得分:1)
这是在5.7内核中添加的,作为mremap(2)的新标志,称为MREMAP_DONTUNMAP。移动页面表条目后,这会将现有映射保留在原处。