【问题标题】:Can shared memory be read and validated without mutexes?可以在没有互斥锁的情况下读取和验证共享内存吗?
【发布时间】:2010-03-31 17:16:40
【问题描述】:

在 Linux 上,我使用 shmgetshmat 设置一个共享内存段,一个进程将写入该共享内存段,一个或多个进程将读取该段。共享的数据大小为几兆字节,更新时会完全重写;它从未部分更新。

我的共享内存段布局如下:

------------------------- | t0 |实际数据 | t1 | -------------------------

其中 t0 和 t1 是写入器开始更新的时间的副本(具有足够的精度,以保证连续更新具有不同的时间)。写入器首先写入 t1,然后复制数据,然后写入 t0。另一方面,阅读器读取 t0,然后是数据,然后是 t1。如果读取器获得相同的 t0 和 t1 值,则认为数据一致且有效,否则,它会重试。

这个程序是否确保如果读者认为数据是有效的,那么它实际上是有效的?

我是否需要担心乱序执行 (OOE)?如果是这样,使用memcpy 获取整个共享内存段的读者会克服读者方面的OOE 问题吗? (这假设memcpy 执行它的复制线性并通过地址空间升序。这个假设是否有效?)

【问题讨论】:

  • 为什么要这样做?互斥锁相当便宜。此外,如果您担心开销,您可以提出一些基于原子类型的同步(就像您尝试使用 t0 和 t1 一样)。
  • 为什么?因为我很懒,试图让最简单的事情发挥作用。此外,我不希望作者等待锁。它还有其他的处理,它是我最初没有写的代码,我不打算修改它,让它等待读者读完。确保读者在阅读时获得一致的数据对我来说并不重要,只要他们知道数据是否一致即可。

标签: c++ linux concurrency locking shared-memory


【解决方案1】:

现代硬件实际上绝不是顺序一致的。因此,如果您不在适当的位置执行内存屏障,则不能保证这样工作。之所以需要屏障,是因为该架构实现了比顺序一致性更弱的共享内存一致性模型。这本身与流水线或 OoO 无关,而是允许多个处理器有效地并行访问内存系统。参见例如Shared memory consistency models: A tutorial。在单处理器上,您不需要屏障,因为所有代码都在该处理器上按顺序执行。

此外,不需要有两个时间字段,序列计数可能是更好的选择,因为无需担心两个更新是否如此接近以至于它们获得相同的时间戳,并且更新计数器要快得多而不是获取当前时间。此外,时钟不可能及时向后移动,这可能会发生,例如当 ntpd 调整时钟漂移时。虽然最后一个问题可以在 Linux 上通过使用 clock_gettime(CLOCK_MONOTONIC, ...) 来解决。使用序列计数器而不是时间戳的另一个优点是您只需要一个序列计数器。写入器在写入数据之前和写入完成之后都会增加计数器。然后读取器读取序列号,检查它是否是偶数,如果是,则读取数据,最后再次读取序列号并与第一个序列号进行比较。如果序号为奇数,则表示正在写入,无需读取数据。

Linux 内核使用了一个名为seqlocks 的锁定原语,它执行上述操作。如果你不怕“GPL污染”,可以google一下实现;因此,这是微不足道的,但诀窍是正确设置障碍。

【讨论】:

  • 假设我在读写之前和/或之后使用了适当的 [SLM]FENCE 汇编指令,我是否还需要 2 个序列计数 (seqc) 副本?如果我只有一个,编写器可以通过设置 seqc 开始,开始写入数据,然后操作系统切换进程,读取器启动,读取 seqc 和大部分数据(有些更新,有些没有),然后切换回完成的编写器,回到读完的读者。读取器两次获得相同的 seqc,但仍以无效数据结束。
  • 查看维基百科链接;写入程序在开始更新之前和更新完成之后递增 seqc。然后阅读器检查 seqc 是偶数还是奇数,除了比较阅读前后的 seqc。
  • @andras:确实,这就是为什么我写“......序列计数可能是更好的选择,因为无需担心两个更新是否如此接近以至于它们获得相同的时间戳。 ..”
  • @janneb:似乎我完全错过了“比...快”和“最后一个问题”之间的一句话——这当然使“最后一个问题”意味着完全不同的东西。对不起这是我的错。 :P
  • @janneb:在最广泛的桌面硬件 (x86/x64) 上,内存模型实际上足够强大,可以在没有明确围栏的情况下支持这一点。 OP 具有两个字段的方法的美妙之处在于您在 x86/x64 上不需要任何障碍。如果 编译器 不重新排序/优化内存访问,这将在 x86/x64 上运行 OOTB。参考文献:1.bluebytesoftware.com/blog/2009/06/05/… 2.blogs.msdn.com/kangsu/archive/2007/07/16/…
【解决方案2】:

Joe Duffy 给出完全相同的算法并将其称为:"A scalable reader/writer scheme with optimistic retry"

它有效。
您需要两个序列号字段。

您需要以相反的顺序读取和写入它们。
您可能需要设置内存屏障,具体取决于memory ordering guarantees of the system

具体来说,当读者和写者分别访问t0或t1进行读写时,你需要读取获取和存储释放语义。

实现这一点需要哪些指令,取决于架构。例如。在 x86/x64 上,因为有比较强的保证 one needs no machine specific barriers at all in this specific case*

* 仍然需要确保编译器/JIT 不会弄乱加载和存储,例如通过使用volatile(在 Java 和 C# 中的含义与在 ISO C/C++ 中的含义不同。但是,编译器可能会有所不同。例如,将 VC++ 2005 或更高版本与 volatile 一起使用是安全的。请参阅"Microsoft Specific" section。也可以在 x86/x64 上使用其他编译器完成。应该检查发出的汇编代码,并且必须确保不访问 t0 和 t1被编译器消除或移动。)

附带说明,如果您需要MFENCElock or [TopOfStack],0 可能是更好的选择,depending on your needs

【讨论】:

  • 您是否知道这种技术是否有名字?此外,由于现代流水线过于复杂的架构,需要内存屏障,对吗?如果这段代码运行在一个 8086 或一个非常简单的微控制器上,那么内存屏障不是不必要的吗?最后,如果数据复制是用 memcpy 完成的,并且函数调用是在时间戳读取/写入之间,那么 memcpy 中的 megs 数据的移动是否总是使时间戳操作顺序?
  • @Bribles:需要设置屏障,因为该架构实现了比顺序一致性更弱的共享内存一致性模型。这本身与流水线或 OoO 无关,而是允许多个处理器有效地并行访问内存系统。参见例如hpl.hp.com/techreports/Compaq-DEC/WRL-95-7.pdf 。在单处理器上,您不需要屏障,因为所有代码都在该处理器上按顺序执行。
  • @Bribles:时间戳的问题在于 1)如果性能很重要,获取当前时间比增加计数器慢 2)如果时间向后变化(例如 ntpd 向后移动时钟以补偿时钟漂移?)。好吧,在 Linux 2) 上,至少可以通过使用 clock_gettime(CLOCK_MONOTONIC, ...) 来修复。
  • @janneb:我什至没有考虑过在这里使用真实时钟值的可能性(即使 CLOCK_MONOTONIC 只会减慢速度而不提供任何附加值)。我认为误解源于此。 :S 你是对的,我首先应该在术语上更准确。 :P
  • 你不能说它不需要内存屏障,同时声称它只适用于 MSVC 或 Java volatile。因为这些系统使用带有易失性的内存屏障。所以是的,你确实需要内存屏障。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-07-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多