C++11中的atomic<T>能够与mmap一起使用吗?

35
我想在嵌入式Linux系统上运行的服务(daemon)中添加网络控制,以控制一些参数。每个参数都可以以非常自然的方式轮询,无需过程调用。共享内存似乎是使网络代码远离守护程序的好方法,并且限制对一组受控变量的共享访问。
由于我不希望部分写入导致未曾写入的值可见,因此我考虑使用std::atomic<bool>std::atomic<int>。但是,我担心std::atomic<T>可能只能与C++11线程一起使用,而不能与多个进程(甚至OS线程)一起使用。具体来说,如果实现使用位于共享内存块之外的任何数据结构,在多进程场景下,这将失败。
我确实看到了一些要求,表明std::atomic不会持有嵌入式锁对象或指向其他数据的指针:

原子整数特化和特化atomic<bool>应具有标准布局。它们必须分别具有平凡的默认构造函数和平凡的析构函数。它们必须支持聚合初始化语法。

原子类模板应有指针部分特化。这些特化应具有标准布局,平凡的默认构造函数和平凡的析构函数。它们必须支持聚合初始化语法。

对我来说,平凡的默认构造和析构似乎排除了相关的每个对象数据,无论是存储在对象内部、通过指针成员变量还是通过外部映射。
但是,我没有看到任何可以排除实现使用单个全局互斥锁/临界区(甚至全局集合)的规则,只要集合元素不与单个原子对象相关联--类似于缓存关联方案可用于减少虚假冲突。显然,如果一个实现使用全局互斥锁,那么从多个进程访问将失败,因为用户会拥有独立的互斥锁,并且不会真正地相互同步。 atomic<T>的实现是否允许执行与进程间共享内存不兼容的操作,或者是否有其他规则使其安全?
我刚刚注意到,平凡的默认构造将对象保留在未准备就绪的状态,并且需要调用atomic_init。标准还提到了锁的初始化。如果这些存储在对象内部(动态内存分配似乎不可能,因为析构函数仍然是平凡的),则它们将在进程之间共享。但是我仍然担心全局互斥锁的可能性。
无论如何,在共享区域中为每个变量保证单个调用atomic_init似乎很困难...所以我想我必须远离C++11原子类型。

作为补充,人们一直推荐使用共享内存的原子操作,尽管不清楚他们是否打算包括或排除std::atomic,或其他API是否保证工作。 - Ben Voigt
我的意思是从性能方面考虑。原子操作的整个目的就是要快,对吧?否则你还不如使用锁…… - user541686
@Mehrdad 速度与使用 atomic 的原因关系不大。使用 atomic 的目的是保证一致性。 - Andre Kostur
1
@Mehrdad 假设您正在使用某种进程间锁定机制,是的。但是,我怀疑OP希望使用std::atomic<T>的部分原因是它提供了一个很好的接口,您不需要记住获取和释放锁定。在那个符合规范的C++程序中,它将执行必要的操作使变量访问具有原子性。但由于标准没有涉及进程间问题,std::atomic使用的同步机制可能无法跨进程工作。 - Andre Kostur
@Mehrdad:任何编写可在多个平台上运行的代码的开发人员都会非常高兴地知道,有一种标准库类型可以映射到支持快速无锁比较和交换指令的平台,并在其他情况下使用锁。此外,明确使用锁编写代码会使代码变得更长,降低可读性。 - Ben Voigt
显示剩余3条评论
2个回答

26

我晚了两个月,但我目前遇到了完全相同的问题,并且我认为我已经找到了某种答案。简短的版本是它应该工作,但我不确定是否要依赖它。

以下是我发现的:

  • C++11标准定义了一个新的内存模型,但它没有OS级别的“进程”概念,因此与任何多进程相关的内容都是非标准的。

  • 然而,标准的第29.4节“无锁属性”(或者至少我拥有的草案N3337)以这个注释结束:

    [注:无锁操作也应该是地址无关的。也就是说,通过两个不同地址对同一内存位置进行的原子操作将以原子方式通信。实现不应依赖于任何进程特定状态。该限制使得可以通过被映射到进程中超过一次的内存和由两个进程共享的内存进行通信。- 结束注释]

    听起来很有希望。:)

  • 该注释似乎来自N2427,该文更加明确:

    为了通过共享内存进行进程间通信,我们的意图是无锁操作也是地址无关的。 也就是说,通过两个不同地址对同一内存位置进行的原子操作将以原子方式通信。 实现不得依赖于任何进程特定状态。 虽然这样的定义超出了标准的范围,但我们的明确表态将使已经存在程序的类的可移植表达成为可能。

    因此,所有无锁操作似乎都应该在这种情况下起作用。

  • 现在,对于std::atomic<type>上的操作是原子的,但对于特定的type,它们可能或可能不是无锁的,这取决于平台的能力。可以通过调用x.is_lock_free()来检查任何变量x是否为无锁。

  • 那么我为什么写下不会依赖这个呢?我找不到任何有关gcc、llvm或其他人明确说明这一点的文档。


  • 1
    在任何“普通”的架构上,比如x86、ARM、PowerPC、MIPS等,gcc和llvm(以及其他正常的编译器)使用无地址的asm指令实现无锁原子操作,并且只关心物理地址。也就是说,即使两个独立的进程将相同的物理页映射到不同的虚拟地址上,它们也可以正常工作,而不需要做额外的处理。 - Peter Cordes
    2
    原子读取-修改-写入操作通过确保该核心是在RMW操作期间具有该行的唯一有效副本,从而阻止其他观察者在中间读取或写入相同的缓存行。所有现代系统都使用MESI的某个变体来进行缓存一致性,其中修改状态意味着没有其他核心可以拥有除无效状态之外的任何状态。在x86上,核心会获取“缓存锁定”(不响应共享缓存行的请求,直到RMW完成)。在大多数其他带有LL / SC的系统上,如果该行未保持M,则SC存储条件将中止“事务”。 - Peter Cordes
    1
    请参阅Can num++ be atomic for 'int num'?以获取有关其工作原理的更多详细信息。但关键是,多核系统已经维护一致的缓存,因此原子RMW建立在此基础之上。 - Peter Cordes

    2

    在C++11之前,标准没有规定多线程如何共享内存,因此我们编写了依赖于特定实现行为的多线程程序。标准仍未规定具有共享内存的进程 - 或者如果您愿意,部分共享内存的线程 - 如何交互。无论您最终做什么,都将依赖于特定实现的保证。

    话虽如此,我认为支持进程共享内存的实现将尝试使其线程同步机制(如原子操作)在进程同步方面可用。至少,我认为很难设计一个std::atomic专业化的无锁实现,该实现在跨进程时不正确地工作。


    并非所有使用共享内存的实现都支持在共享内存中使用 std::atomic<int>,但是你不太可能使用不支持它的实现。你已经通过使用进程共享内存来依赖非标准行为做出了决定,如果需要在该共享内存中使用 std::atomic<int>,那么这不会对你的可移植性产生实质性的限制。 - Casey
    好的观点...您是否知道有哪些实现记录了这样的额外保证(当然是非可移植的)?我特别关注在Linux/ARM环境下使用G++ 4.7.x,但任何关于这种保证的示例都将是很棒的。 - Ben Voigt
    4
    引用的这篇论文探讨了 Linux 内核中出现的空指针解引用问题,即使是在程序中使用空指针依然有可能导致程序错误或崩溃。作者形象地表述这个问题会像“恶魔从你的鼻子里飞出来”一样可怕。 - Yankes
    1
    @Yankes,我在引用的文件中找不到“demon”或“nasal”这些词的出现;你的论点是无效的。开玩笑,非常有趣的论文。 - Casey
    1
    @Yankes 那应该是“面向优化安全系统:分析未定义行为的影响”吧?URL在长期内不稳定 :( ... https://pdos.csail.mit.edu/papers/stack:sosp13.pdf - curiousguy
    显示剩余6条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接