这是可能的,尽管您可能需要考虑特定于体系结构的缓存一致性问题。一些体系结构根本不允许同时从多个虚拟地址访问同一页面而不会失去一致性。所以,有些架构可以很好地处理这个问题,有些则不行。
编辑添加: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() 放大备份文件后创建到同一个文件的第二个映射。只有在合适的宽限期之后——当你知道没有线程正在使用映射时(也许使用原子计数器来跟踪它?)——你取消映射原始映射。
实际中,当一个映射需要放大的时候,你先把backing文件放大,然后试试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 的 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 处理程序中。它不仅必须能够解码所有机器指令并模拟它们(或至少是那些写入内存的指令),而且如果新映射尚未完全复制,它还必须忙于等待。它很复杂,绝对不可移植,并且非常特定于架构......但可能。