如何在 C++14 中将多字节值写入共享内存?

Posted

技术标签:

【中文标题】如何在 C++14 中将多字节值写入共享内存?【英文标题】:How to write multi-byte values to shared memory in C++14? 【发布时间】:2020-11-19 17:53:02 【问题描述】:

假设我有两个进程,它们都使用shm_openmmap 共享一个内存块,并且存在一个共享同步原语 - 比如说一个信号量 - 确保对内存的独占访问。 IE。没有竞争条件。

我的理解是,mmap 返回的指针仍必须标记为 volatile 以防止缓存读取。

现在,一个人如何写作,例如将std::uint64_t 放入内存中的任何对齐位置?

当然,我会简单地使用std::memcpy,但它不适用于指向易失性内存的指针。

第一次尝试

// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;

// Value to store, initialize "randomly" to prevent compiler
// optimization, for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(nullptr);

// Store byte-by-byte
unsigned char* src = reinterpret_cast<unsigned char*>(&value);
for(std::size_t i=0;i<sizeof(value);++i)
    ptr[i]=src[i];

Godbolt.

我坚信这个解决方案是正确的,但即使使用-O3,也有 8 个 1 字节传输。这确实不是最佳选择。

第二次尝试

既然我知道在我锁定内存时没有人会更改内存,那么 volatile 可能毕竟是不必要的?

// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;

// Value to store, initialize "randomly" to prevent compiler
// optimization for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(0xAA);
unsigned char* src = reinterpret_cast<unsigned char*>(&value);

//Obscure enough?
auto* real_ptr = reinterpret_cast<unsigned char*>(reinterpret_cast<std::uintptr_t>(ptr));

std::memcpy(real_ptr,src,sizeof(value));

Godbolt.

但这似乎不起作用,编译器看穿了演员表并且什么也不做。 Clang生成ud2指令,不知道为什么,我的代码中有UB吗?除了value初始化。

第三次尝试

这个来自this answer。但我认为它确实违反了严格的别名规则,不是吗?

// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;

// Value to store, initialize "randomly" to prevent compiler
// optimization for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(0xAA);
unsigned char* src = reinterpret_cast<unsigned char*>(&value);

volatile std::uint64_t* dest = reinterpret_cast<volatile std::uint64_t*>(ptr);
*dest=value;

Godbolt.

Gcc 实际上做了我想要的 - 一条简单的指令来复制 64 位值。但是如果是UB就没用了。

我可以解决它的一种方法是在那个地方真正创建std::uint64_t 对象。但是,显然placement new 也不适用于volatile 指针。

问题

那么,有没有比逐字节复制更好(安全)的方法? 我还想复制更大的原始字节块。这能比单个字节做得更好吗? 是否有可能强制memcpy 做正确的事? 我是否不必要地担心性能而应该只使用循环? 任何示例(主要是 C)根本不使用volatile,我也应该这样做吗? mmaped 指针是否已经被区别对待了?怎么样?

感谢您的任何建议。

编辑:

两个进程在同一个系统上运行。另外请假设这些值可以逐字节复制,而不是谈论存储指向某处的指针的复杂虚拟类。所有整数,没有浮点数就可以了。

【问题讨论】:

多字节是数字吗?请记住,排序对于多字节数字很重要。在互联网上搜索“Endianess”。 @ThomasMatthews 它是在同一个系统上的两个进程之间,我有点希望字节序是一样的。 顺便说一句,不能保证memcpy 将执行逐字节传输。它可以优化为使用寄存器进行复制(一次 4 个字节)或使用块传输指令(如果处理器支持)。为了保证传输单元的大小,您必须编写自己的代码。 (去过那里,用嵌入式系统做到了) @Quimby 你错了。如果你是正确的,你仍然会被搞砸,因为 CPU 和其他平台硬件也可以优化读取,所以阻止编译器这样做是不够的。 volatile 关键字没有为线程定义跨平台语义。 ***.com/questions/2484980/… 【参考方案1】:

我的理解是,从 mmap 返回的指针仍然必须标记为 volatile 以防止缓存读取。

你的理解是错误的。不要使用volatile 来控制内存可见性——这不是它的用途。它要么过于昂贵,要么不够严格,或两者兼而有之。

以GCC documentation on volatile 为例,它表示:

对非易失性对象的访问没有相对于易失性访问的顺序。您不能使用易失性对象作为内存屏障来排序对非易失性内存的一系列写入

如果您只是想避免撕裂、缓存和重新排序,请改用&lt;atomic&gt;。例如,如果您有一个现有的共享uint64_t(并且它正确对齐),只需通过std::atomic_ref&lt;uint64_t&gt; 访问它。你可以直接使用acquire、release或CAS。

如果您需要正常同步,那么您现有的信号量就可以了。如下所示,它已经提供了任何必要的栅栏,并防止在等待/发布调用中重新排序。它不会阻止它们之间的重新排序或其他优化,但这通常没问题。


至于

任何示例(主要是 C)根本不使用 volatile,我也应该这样做吗? mmaped 指针是否已经被区别对待了?怎么样?

答案是无论使用什么同步都需要应用适当的栅栏。

POSIX lists these functions 作为“同步内存”,这意味着它们必须发出任何所需的内存栅栏,并防止不适当的编译器重新排序。 因此,例如,您的实现必须避免在 pthread_mutex_*lock()sem_wait()/sem_post() 调用之间移动内存访问,以便符合 POSIX,即使在其他情况下是合法的 C 或 C++。

当您使用 C++ 的内置线程或原子支持时,正确的语义是语言标准的一部分,而不是平台扩展(但共享内存不是)。

【讨论】:

感谢您的回答。我使用volatile 来避免优化 R/W 是不是不正确? C++ 编译器是否知道映射内存可以在没有看到任何写入的情况下更改?我猜那将是理想的。我考虑过使用&lt;atomic&gt;,但我没有找到任何 w.r.t.进程间通信而不是线程间通信。 线程间通信有效的进程间通信,除非您使用单线程协作多任务。 至于编辑,我使用sem_open 使用POSIX 信号量,我在文档中没有找到任何关于内存屏障的内容 一般情况下 - 除了内存映射 I/O 和与信号处理程序通信之外,将volatile 用于任何 很可能是错误的。所有these 函数都定义为“同步内存”。这意味着运行时的内存栅栏和编译器栅栏都可以防止在编译时重新排序。 @LouisGo 非常感谢。 @没用的谢谢,说清楚了很多。【参考方案2】:

假设我有两个进程,它们都使用 shm_open 和 mmap 共享一个内存块,并且存在一个共享同步原语 - 比如说一个信号量 - 确保对内存的独占访问。 IE。没有竞争条件。

您需要的不仅仅是对内存的独占访问。您需要同步内存。我见过的每一个信号量都已经做到了。如果你没有,那是错误的同步原语。换一个。

我的理解是,从 mmap 返回的指针仍然必须标记为 volatile 以防止缓存读取。

volatile 不会阻止缓存读取,但几乎所有的信号量、互斥锁和其他同步原语都会像阻止缓存读取和写入一样起作用。否则,它们几乎无法使用。

您使用什么信号量?如果它不同步内存,那就是错误的工具。

【讨论】:

我使用sem_open - 一个 POSIX 信号量,这很好,对吧?因此,如果每个访问都受信号量保护,我可以忽略 volatile 关键字并使用 memcpy? 没错。在this page 中搜索sem_waitsem_post。您将看到“以下函数相对于其他线程同步内存:”,然后是包含这两个函数的列表。

以上是关于如何在 C++14 中将多字节值写入共享内存?的主要内容,如果未能解决你的问题,请参考以下文章

如何在多写入器情况下对文件支持的共享内存中的大页进行故障排除

在 Python 多处理中将 Pool.map 与共享内存数组结合起来

如何在 Ada 中使用 Linux 将任意字符串写入并读取到共享内存?

如何将字符串数组附加到共享内存? C

从共享内存中保存 4 字节 pP 深度图像

python multiprocessing - 在进程之间共享类字典,随后从进程写入反映到共享内存