在Linux上,是否有一种方法可以在两个无关的进程之间共享地址映射?

3
我猜如果可能的话,最有可能是通过 mmap 实现。
我的原始问题是:在多个用户使用同一块共享内存时,mmap() 是否可以返回相同的指针? 最终,每个进程地址空间中的地址映射到相同的物理内存。 问题是是否可以以某种方式使每个进程提供的地址相同。
这为什么很重要?
考虑共享包含指针的数据结构的情况。 如果地址映射是共享的,那么这些指针对两个进程都有效。 如果地址映射不共享,则指针可能指向写入它的进程的地址空间。 如果另一个进程试图读取它,就会触发访问冲突。
对于共享库,我们依赖于位置无关代码,但仍然可能存在需要加载器在加载时调整的指针。 (我不清楚这是否只做一次,还是每个进程都得到自己的私有副本,其中包含这样的指针,以适应其自己的地址空间)。
如果地址映射不共享,则对于数据,我们需要创建使用偏移而不是绝对指针的位置无关结构(不幸的首字母缩写) 或限制自己只使用不包含任何指针的数据结构。
在我的应用程序中,我有几个我希望共享的数据结构,它们实质上是向量。 我不希望将它们持久化或序列化/反序列化。我希望每个进程使用相同的物理内存。 基本上这是一个搜索问题 - 共享数据是干草堆,每个进程搜索自己的针。 由于各种原因,我们不希望它们成为线程,尽管这将完全消除此问题,因为线程确实共享地址空间。
我可以使用自定义分配器在共享内存中创建向量。例如:
SharedMemoryAllocator sharedAlloc(somePointerFromMmap);
class Foo
{
   std::vector<Bar, SharedMemoryAllocator>   A;
   std::vector<Snafu, SharedMemoryAllocator> B;

   Foo(SharedMemoryAllocator& sharedAlloc):
      A(sharedAlloc)
      B(sharedAlloc)
   {
   }
};

如果地址映射是共享的,则可以安全地使用这样的分配器来为整个结构分配空间:
unique_ptr<Foo> fubar = sharedAlloc.new(Foo(sharedAlloc));

因为使用*fubar指向A和B的存储空间时,它们在相同的地址空间中。

另一方面,如果地址映射不共享,则只能安全地共享A和B所指向的基础内存块。

也就是说,每个进程必须拥有自己的本地Foo实例,其中每个向量都是通过连接到由分配器提供的共享内存块来构建的。这更具可移植性,但更加丑陋。

从技术上讲,在没有添加std::bless()和其他巫术的情况下,两者都不符合C ++规范。因此,我们无论如何都处于未定义(或平台定义)的行为状态。但是,'safe'使用malloc也是如此(请参见上面的链接)。


据我所知,除了在调用时请求特定地址且调用不失败的情况外,Posix或Linux对mmap()返回的地址没有保证。
因此,如果它偶尔能够工作,那么最好也只是未定义行为。通常,依赖未定义行为都是不好的选择,出于各种原因。
但也许实际上这是平台定义的,而不是未定义的?
正如提到的例外情况固定地址映射,但你如何让两个独立的进程事先达成一致,使用相同的安全地址呢?这里给出了一个可能的解决方案
这表明您需要第二个共享内存段或其他IPC机制来共享分配给要使用的共享内存块的创建者的地址。
这是正确的方法吗?它是唯一的方法吗?
观看何时使用mmap MAP_FIXED?- MAP_FIXED 的主要合法用途是重新映射需要在加载库时相对于彼此具有相同的相对地址的不同类型的内存段。
然而,其他人正在使用我建议的方式。那个问题的另一个答案提到:
  • 共享内存可能包含指针。
我发现其他一些人使用MAP_FIXED来做这件事,但我还没有找到可行的示例。
这种使用MAP_FIXED的方式是否被ASLR使其无法正常工作?
实际上,只有在非常特定的情况下才能共享地址空间:
例如,仅当:

在这种情况下,MAP_FIXED_NOREPLACE会起作用吗?

您使用shm_open()还是open()是否有区别?

我查看了boost interprocess的代码,它似乎没有使用MAP_FIXED或sendmsg。看起来它是不支持的

我目前尝试的方法是简化的:

进程A:

fd = open("foobar", O_RDWR);
void* address = mmap(nullptr, PROT_READ|PROT_WRITE, MAP_SHARED, fd);
(*address) = address;

过程 B:
fd = open("foobar", O_RDONLY);
void* addressRequested = readAddressFromFoobar();       
void* address = mmap(addressRequested, PROT_READ, MAP_SHARED|MAP_FIXED, fd);

这个操作失败并返回 E_NOMEM 错误。 如果我将 addressRequested 替换为 nullptr,它就成功了,我可以访问相同的数据,但不能依赖于任何指针。
有人能够演示或链接到一种可行的方法吗? 或者明确解释为什么在当前的 Linux 系统中这永远不可能实现。
我很清楚我们可以使用内部偏移量而不是指针来共享对象,但这会失去很多方便性。STL 类型通常假定它们可以使用指针。如果可能的话,我不希望编写自己想使用的每个容器的版本。
显然,Boost 通过提供使用智能指针而不是原始指针的容器来解决这个问题。我之前没有意识到这一点。这是一个解决通用问题的好方法,但与此问题不同。找到一个权威的解释会对 Boost Interprocess 是否支持在进程之间共享包含指针的对象? 或者说是更好的问题提供更好的答案。

1
你有没有阅读mmap系统调用的手册页面?它可以回答你的问题。 - Sam Varshavchik
1
您可以在 https://github.com/ChrisDodd/shm_malloc 上查看在Linux和多个Unix变体上运行的代码,使用mmap(MAP_FIXED)来实现共享malloc池。 - Chris Dodd
@ChrisDodd 看起来很有趣。我还不能确定如何确保每个进程都被分配相同的固定地址。也许需要更多的阅读。 - Bruce Adams
它只在malloc.c中有一个固定的基指针,可以经验性地适用于每个感兴趣的指针大小/平台。我不知道这是否总是安全的,或者仅在您替换malloc且没有其他共享库调用mmap时起作用。无论如何,我的答案只是试图在运行时发现一个合适的基础,而不是硬编码它并希望它永远不会改变。 - Useless
shm_open 似乎正是您要找的。它在两个进程上映射相同的内存页面,但还确保页面是一致的(也就是说,如果一个进程写入内存页面,另一个进程想从该页面读取,缓存会在上下文切换时被刷新,以使读取起作用)。 - xryl669
显示剩余11条评论
1个回答

0
有人能演示或链接到一种可行的方法来做到这一点,或者明确解释为什么在当前Linux中这永远不可能实现吗?
你现有的方式已经可以工作了。
它有点脆弱、琐碎,我不确定这是否是一个好主意,但它可以被制作成工作。
  1. ASLR

    由于两个进程(可能)具有不同的ASLR偏移量,因此您不能仅仅使用在第一个进程中由mmap返回的地址并假设它将在第二个进程中起作用。

    通过当前的实现,应该足以使用mmap映射一个小区域(使用addr=NULL),以发现随机化的基地址,然后请求下一个1MB边界处的真实映射。

  2. 运行时变异性

    单独考虑这一点可能已经足够了,但是另一个进程可能已经使用了相同的地址。例如,共享库可以在到达用户代码之前调用mmap,由于ASLR、库更新、预加载或其他原因,每次运行都可能有所不同。

    很可能第二个进程也应该进行非固定映射,没有请求地址来建立自己的有效基地址(包括ASLR和任何现有的mmap),以使其能够在尝试使用MMAP_FIXED之前对广告地址进行检查。

    因此,您需要一个反馈/共识机制。如果第二个进程无法在第一个广告地址处进行映射,请让其创建自己的映射并将该地址广告回第一个进程(在尝试之前应该释放自己的映射,以防它们重叠)。

    这种迭代还会使第一点在未来的ASLR更改面前变得不那么脆弱。

请注意,即使这种方法有效,在不区分共享地址空间和私有地址空间指针的情况下仍存在显著风险。您可以很容易地在共享结构中存储对非共享内存的指针,因为它们是相同类型的。
我强烈建议使用Boost.Interprocess分配器、offset_ptrs等,以明确区分它们。

为什么需要“然后在下一个1MB边界请求实际映射”? 我认为我可能误解了ASLR的工作原理。我假设操作系统会尽量避免给进程重叠的地址空间。事实上是否没有这样的要求? - Bruce Adams
如果您使用offset_ptr,则无论如何都不需要基地址相同。对于这种情况,我们可以假设共享指针始终保持受SharedAllocator提供的类型限制。一如既往地请注意程序员应该注意,永远不要让这样的结构采用由不同分配器(包括SharedAllocator的不同实例)分配的指针。 - Bruce Adams
我想它们只是部分独立的。许多地址转换在进程之间共享,例如 https://dev59.com/qavka4cB1Zd3GeqPqD-7#50257603 。我期望进程对自己的代码和数据段有唯一的映射,但对于共享代码和数据(包括 CoW 子进程和共享内存)则有共享映射。换句话说,如果 mmap 不重叠并且您使用 MAP_FIXED_NOREPLACE 请求,则可以故意共享块内存的映射。将条目添加到仅询问进程的页表中更安全、更容易实现。 - Bruce Adams
我并不立刻明白mmap(nullptr,...)如何让你发现基地址以及为什么这很重要。所以它是1MB区域中256个可能页面之一。这对我们有什么帮助呢?这只是为了防止第一个进程在第二个进程的mmap区域开始之前返回一个区域吗? - Bruce Adams
我们正在尝试找到两个进程都允许映射的地址。向上舍入到下一个1MB边界可能不足够,但它似乎是第二步的合理起点。 - Useless
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接