使用虚拟内存系统调用分配原子数组安全吗?
Is allocating arrays of atomics using virtual memory system calls safe?
我正在开发内存数据库,我的系统需要大量 std::atomic_int
对象,这些对象大致充当数据库记录的锁。现在我更愿意使用 VM 系统调用分配这些锁,例如类 Unix 系统上的 mmap
和 Win32/64 上的 VirtualAlloc
。这有几个原因,其中之一是不必显式初始化内存(即,由 VM 系统调用分配的内存保证由 OS 归零)。所以,我基本上想这样做:
#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
保证 return 内存与 VM 页面边界对齐,因此 std::atomic_int
的任何对齐要求都应该是很荣幸,并且 std::atomic_int
的构造函数不会初始化该值,因此不存在不调用构造函数的危险,因为长读写以合理的方式实现(例如,使用 GCC 的 __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
只是一个正常的移动,它保证只对字对齐是原子的访问。如果我对此是正确的,我是否可以在代码中添加任何内容以防止出现此类情况并可能在编译时检测到此类问题?
这是安全的,因为 mmap
为任何内置和 SIMD 类型分配适当对齐的内存。
确保您调用 std::uninitialized_default_construct_n
(或您自己的 C++17 之前的等价物)以满足 C++ 标准的要求,即必须调用构造函数,并且 std::destroy_n
在之后调用析构函数采用。这些调用编译成 0 条指令,因为 std::atomic<>
的默认构造函数和析构函数是 trivial (do nothing):
size_t numberOfLocks = ... some large number ...;
auto* locks = static_cast<std::atomic_int*>(mmap(0, numberOfLocks * sizeof(std::atomic_int), ...));
// initialize
std::uninitialized_default_construct_n(locks, numberOfLocks);
// ... use ...
// uninitialize
std::destroy_n(locks, numberOfLocks);
我正在开发内存数据库,我的系统需要大量 std::atomic_int
对象,这些对象大致充当数据库记录的锁。现在我更愿意使用 VM 系统调用分配这些锁,例如类 Unix 系统上的 mmap
和 Win32/64 上的 VirtualAlloc
。这有几个原因,其中之一是不必显式初始化内存(即,由 VM 系统调用分配的内存保证由 OS 归零)。所以,我基本上想这样做:
#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
保证 return 内存与 VM 页面边界对齐,因此 std::atomic_int
的任何对齐要求都应该是很荣幸,并且 std::atomic_int
的构造函数不会初始化该值,因此不存在不调用构造函数的危险,因为长读写以合理的方式实现(例如,使用 GCC 的 __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
只是一个正常的移动,它保证只对字对齐是原子的访问。如果我对此是正确的,我是否可以在代码中添加任何内容以防止出现此类情况并可能在编译时检测到此类问题?
这是安全的,因为 mmap
为任何内置和 SIMD 类型分配适当对齐的内存。
确保您调用 std::uninitialized_default_construct_n
(或您自己的 C++17 之前的等价物)以满足 C++ 标准的要求,即必须调用构造函数,并且 std::destroy_n
在之后调用析构函数采用。这些调用编译成 0 条指令,因为 std::atomic<>
的默认构造函数和析构函数是 trivial (do nothing):
size_t numberOfLocks = ... some large number ...;
auto* locks = static_cast<std::atomic_int*>(mmap(0, numberOfLocks * sizeof(std::atomic_int), ...));
// initialize
std::uninitialized_default_construct_n(locks, numberOfLocks);
// ... use ...
// uninitialize
std::destroy_n(locks, numberOfLocks);