【发布时间】:2019-05-06 00:03:20
【问题描述】:
我正在开发一个内存数据库,我的系统需要大量 std::atomic_int 对象,它们大致充当数据库记录的锁。现在我更愿意使用 VM 系统调用来分配这些锁,例如类 Unix 系统上的 mmap 和 Win32/64 上的 VirtualAlloc。这有几个原因,其中只有一个不需要显式初始化内存(即,由 VM 系统调用分配的内存保证由操作系统清零)。所以,我基本上想这样做:
#include <sys/mman.h>
#include <atomic>
// ...
size_t numberOfLocks = ... some large number ...;
std::atomic_int* locks = reinterpret_cast<std::atomic_int*>(mmap(0, numberOfLocks * sizeof(std::atomic_int), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0));
// ... use locks[i].load() or locks[i].store() as with any memory order as appropriate
我的主要问题是这段代码是否安全。我直观地期望代码可以在任何合理的平台上使用现代编译器运行:mmap 保证返回与 VM 页面边界对齐的内存,因此应该尊重 std::atomic_int 的任何对齐要求,并且 @987654330 的构造函数@ 不会初始化该值,因此只要以合理的方式(例如,使用 GCC 和 clang 的 __atomic_* 内置函数)实现长时间的读取和写入,就不存在不调用构造函数的危险。
但是,我可以想象,根据 C++ 标准,这段代码不一定是安全的——我这样想对吗?如果这是正确的,是否有任何检查,如果代码在目标平台上成功编译(即,如果std::atomic_int 的实现是我所期望的),那么一切都按我的预期工作?
与此相关,我希望以下代码(其中 std::atomic_int 未对齐属性)在 x86 上中断:
uint8_t* region = reinterpret_cast<uint8_t*>(mmap(...));
std::atomic_int* lock = reinterpret_cast<std::atomic_int*>(region + 1);
lock->store(42, std::memory_order_relaxed);
我认为这不应该工作的原因是因为在 x86 上合理实现 std::atomic_int::store 和 std::memory_order_relaxed 只是一个正常的举动,它保证仅对字对齐访问是原子的。如果我对此是正确的,是否可以在代码中添加任何内容以防止此类情况发生并可能在编译时检测到此类问题?
【问题讨论】:
-
您可以在 SO 上找到很多关于
mmap和 C++ 标准的问题。简而言之,它是 UB,因为从未调用过std::atomic_int的构造函数(由该程序调用),因此locks是一个悬空指针。其中一个问题是this,但作为欺骗目标并不是很有说服力。 -
正在朝着这个方向开展工作,您可以在the paper about
std::bless了解当前情况。 -
谢谢,这很有趣。我不能说我完全理解所有这些复杂性的原因,但我会继续阅读。但是,不管标准如何,期望它适用于大多数现代编译器(MSVC、GCC、clang)和平台(尤其是 x86_64 和 ARM)是否现实?
-
我希望它在实践中可以工作一段时间,但你永远无法确定优化器可以用 UB 做什么。首先考虑
std::laundering 指针,这可能会也可能不会避免不需要的代码生成。
标签: c++ atomic mmap virtualalloc