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

How to write multi-byte values to shared memory in C++14?

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

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

现在,一个人怎么写,例如a 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 指针。

问题

感谢您的任何建议。

编辑:

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

My understanding is that the pointer returned from mmap must still be marked as volatile to prevent cached reads.

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

例如,考虑 GCC documentation on volatile,它表示:

Accesses to non-volatile objects are not ordered with respect to volatile accesses. You cannot use a volatile object as a memory barrier to order a sequence of writes to non-volatile memory

如果您只想避免撕裂、缓存和重新排序 - 使用 <atomic> instead. For example, if you have an existing shared uint64_t (and it is correctly aligned), just access it via a std::atomic_ref<uint64_t>。您可以直接使用 acquire、release 或 CAS。

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


至于

Any examples(mostly C) do not use volatile at all, should I do that too? Is mmaped pointer treated differently already? How?

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

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

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

Assume that I have two processes that both share a memory block using shm_open and mmap and there exists a shared synchronization primitive - let's say a semaphore - that ensures exclusive access to the memory. I.e. no race conditions.

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

My understanding is that the pointer returned from mmap must still be marked as volatile to prevent cached reads.

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

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