在Linux上我正在使用shmget
和shmat
来设置一个进程将写入的共享内存段,并且将读取一个或多个进程。正在共享的数据大小为几兆字节,并且在更新时完全重写;它从未部分更新。
我的共享内存段如下所示:
------------------------- | t0 | actual data | t1 | -------------------------
其中t 0 和t 1 是编写器开始更新时的副本(具有足够的精度,以确保连续更新具有不同的时间)。编写器首先写入t 1 ,然后复制数据,然后写入t 0 。另一方面,读者读取t 0 ,然后读取数据,然后t 1 。如果读者获得t 0 和t 1 的相同值,则认为数据一致且有效,如果不是,则再次尝试。
这个程序是否确保如果读者认为数据有效则实际上是?
我是否需要担心无序执行(OOE)?如果是这样,读者使用memcpy
获取整个共享内存段是否会克服读者端的OOE问题? (这假设memcpy
线性地执行它的复制并在地址空间中上升。这个假设是否有效?)
答案 0 :(得分:5)
现代硬件实际上不是顺序一致的。因此,如果您不在适当的位置执行内存屏障,则无法保证这样做。需要障碍因为架构实现了比顺序一致性更弱的共享内存一致性模型。因此,这与流水线操作或OoO无关,而是允许多个处理器并行地高效访问存储器系统。参见例如Shared memory consistency models: A tutorial。在单处理器上,您不需要障碍,因为所有代码都在该处理器上顺序执行。
此外,不需要有两个时间字段,序列计数可能是更好的选择,因为无需担心两个更新是否如此接近以至于它们获得相同的时间戳,并且更新计数器要快得多比得到当前的时间。而且,时钟不可能在时间上向后移动,这可能发生在例如当ntpd调整时钟漂移时。虽然可以通过使用clock_gettime(CLOCK_MONOTONIC,...)在Linux上克服最后一个问题。使用序列计数器而不是时间戳的另一个优点是您只需要一个序列计数器。写入器在写入数据之前和写入完成之后递增计数器。然后读者读取序列号,检查它是否均匀,如果是,则读取数据,最后再次读取序列号并与第一个序列号进行比较。如果序列号为奇数,则表示正在进行写入,无需读取数据。
Linux内核使用一个名为seqlocks的锁定原语来完成上述操作。如果你不害怕“GPL污染”,你可以谷歌实施;因此它是微不足道的,但诀窍是让障碍正确。
答案 1 :(得分:2)
Joe Duffy提供完全相同的算法并将其调用:"A scalable reader/writer scheme with optimistic retry"。
有效。
您需要两个序列号字段。
您需要以相反的顺序阅读和书写 您可能需要设置内存屏障,具体取决于memory ordering guarantees of the system。
具体来说,当读者和作者分别访问t 0 或t 1 进行读写时,需要读取获取和存储释放语义。
实现这一目标需要哪些指示,取决于架构。例如。在x86 / x64上,由于相对较强的保证one needs no machine specific barriers at all in this specific case*。
*还需要确保编译器/ JIT不会乱用加载和存储,例如通过使用volatile
(在Java和C#中具有与ISO C / C ++中不同的含义。但编译器可能会有所不同。例如,使用VC ++ 2005或更高版本的volatile可以安全地执行上述操作。请参阅{{ 3}}。也可以在x86 / x64上使用其他编译器。应该检查发出的汇编代码,并且必须确保访问t 0 和t 1 不被编译器消除或移动。)
作为旁注,如果您需要MFENCE
,"Microsoft Specific" section可能是更好的选择,lock or [TopOfStack],0
。 < / p>