【问题标题】:Is allocating arrays of atomics using virtual memory system calls safe?使用虚拟内存系统调用分配原子数组是否安全?
【发布时间】: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::storestd::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


【解决方案1】:

这是安全的,因为mmap 为任何内置和 SIMD 类型分配适当对齐的内存。

确保调用 std::uninitialized_default_construct_n(或您自己的 C++17 之前的等效项)以满足 C++ 标准的要求,即必须调用构造函数,并确保调用 std::destroy_n 在使用后调用析构函数。这些调用编译成0条指令,因为std::atomic&lt;&gt;的默认构造函数和析构函数是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);

【讨论】:

  • 这难道不是说明mmap 的全部意义吗?我虽然你想读取你映射的文件的内存,而不是简单地通过将元素放置到其中来覆盖它。据我所知,mmap 要么是未初始化的读取,要么是丢弃数据,所以它要么是 UB,要么是无用的。
  • @nwp 这里mmap 用于匿名内存分配,而不是映射文件。 mmap 总是将内存清零,这样以前使用内存的其他进程就不会泄漏任何信息 - 基本安全功能。
  • 谢谢!出于好奇:如果我不使用 std::uninitialized_default_construct 或等效项,会出现什么问题?我知道这样做不会造成性能损失——我只是想了解如果我不调用它,我会违反 C++ 标准中的哪些规则。
  • @Boris 如果你不调用构造函数和析构函数,我想不出在实践中会出现什么问题,因为它们是微不足道的。
猜你喜欢
  • 2011-09-25
  • 1970-01-01
  • 2017-12-13
  • 2023-04-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-08
  • 1970-01-01
相关资源
最近更新 更多