内存映射文件和指向易失性对象的指针

时间:2017-08-18 10:00:56

标签: c++ c language-lawyer volatile mmap

我对C和C ++中volatile语义的理解是它将内存访问转换为(observable) side effects。每当读取或写入内存映射文件(或共享内存)时,我都希望指针是volatile限定的,以表明这实际上是I / O. (John Regehr在volatile)的语义上写了一篇非常好的article

此外,我希望使用像memcpy()这样的函数来访问共享内存是不正确的,因为签名表明易失性限定被丢弃,内存访问不被视为I / O.

在我看来,这是支持std::copy()的论据,其中挥发性限定符不会被丢弃,并且内存访问被正确地视为I / O.

但是,我使用指向易失性对象和std::copy()指针访问内存映射文件的经验是,它比仅使用memcpy()要慢几个数量级。我很想得出结论,或许clang和GCC对volatile的处理过于保守。是这样的吗?

如果我想遵循标准的字母并将其归还给我所依赖的语义,那么访问与volatile相关的共享内存有什么指导?

标准[intro.execution] §14的相关引用:

  

读取由volatile glvalue指定的对象,修改   对象,调用库I / O函数或调用函数   这些操作中的任何一个都是副作用,这些都是变化   在执行环境的状态。表达的评价   (或子表达式)通常包括值计算   (包括确定glvalue对象的身份   评估并获取先前分配给对象的值   prvalue评估)和副作用的启动。当打电话给   库I / O函数返回或通过volatile glvalue访问   被评估的副作用被认为是完整的,即使有些   呼叫所隐含的外部动作(例如I / O本身)或   易失性访问可能还没有完成。

3 个答案:

答案 0 :(得分:4)

  

我对C和C ++中volatile的语义的理解是它将内存访问转换为I / O

不,不这样做。所有volatile所做的就是从程序员向编译器传达一个特定的存储区域可以随时被改变,通过"其他东西"。

"别的"可能是很多不同的事情。例子:

  • 内存映射硬件寄存器
  • 与ISR共享的变量
  • 从回调函数更新的变量
  • 与其他线程或进程共享的变量
  • 通过DMA更新内存区域

由于标准(5.1.2.3)保证对易失性对象的访问(读/写)可能无法优化,volatile也可用于阻止某些编译器优化,这在与硬件相关的编程。

  

每当读取或写入内存映射文件(或共享内存)时,我都希望指针是volatile限定的

不一定,不。数据的性质并不重要,只是如何更新。

  

我希望使用像memcpy()这样的函数来访问共享内存是不正确的

总体而言,这取决于您对共享内存的定义"。这是你整个问题的一个问题,因为你一直在谈论"共享内存"这不是一个正式的,定义明确的术语。内存与另一个ISR /线程/进程共享?

是的,与另一个ISR /线程/进程共享的内存可能必须声明为volatile,具体取决于编译器。但这只是 因为volatile可以阻止编译器做出错误的假设并优化代码访问这样的"共享"变量是错误的方式。在旧的嵌入式系统编译器上特别容易发生的事情。在现代托管系统编译器上它不应该是必需的。

volatile不会导致记忆障碍行为。它(不一定)强制表达式以某种顺序执行。

volatile当然不保证任何形式的原子性。这就是将_Atomic类型限定符添加到C语言的原因。

回到复制问题 - 如果内存区域是"共享"在几个ISR /线程/进程之间,然后volatile根本没有帮助。相反,您需要一些同步方法,例如互斥,信号量或临界区。

  

在我看来,这是一个支持std :: copy()的论据,其中volatile限定符不会被丢弃,内存访问被正确地视为I / O.

不,这是错误的,因为已经提到的原因。

  

如果我想遵循标准的字母并将其归还给我所依赖的语义,那么访问有关volatile的共享内存有什么指导?

使用系统特定的API:s通过互斥/信号量/临界区来保护内存访问。

答案 1 :(得分:3)

我认为你是在思考这个问题。我认为mmap或等效(我将在这里使用POSIX术语)内存的任何原因都是不稳定的。

从编译器mmap的角度来看,返回一个被修改的对象,然后在msync期间将其赋予munmap_Exit或隐含的取消映射。这些功能需要被视为I / O,没有别的。

你几乎可以用mmap + mallocread替换munmap write + free,你可以获得大部分保证I / O何时以及如何完成。

请注意,这甚至不需要将数据反馈给munmap,只是更容易以这种方式进行演示。你可以让mmap返回一块内存,并将其内部保存在一个列表中,然后一个函数(让我们称之为msyncall)没有任何参数写出所有内存所有调用到先前返回的mmap。然后我们可以从中构建,说任何执行I / O的函数都有一个隐式msyncall。我们不需要那么远。从编译器的角度来看,libc是一个黑盒子,其中一些函数返回了一些内存,该内存必须在任何其他调用libc之前同步,因为编译器无法知道之前从libc返回的内存位仍被引用并在内部使用。

以上段落是如何在实践中发挥作用的,但我们如何从标准的角度来处理呢?我们先来看看类似的问题。对于线程,共享内存仅在某些very specific function calls处同步。这非常重要,因为现代CPU重新排序读取和写入,并且内存障碍是昂贵的,旧的CPU可能需要显式缓存刷新,然后其他人(其他线程,进程或I / O)可以看到写入的数据。 mmap的规范说:

  

当将mmap()与任何其他文件访问方法结合使用时,应用程序必须确保正确同步

但它没有指定如何完成同步。我在实践中知道同步几乎必须是msync,因为还有一些系统,其中读/写不使用与mmap相同的页面缓存。

答案 2 :(得分:0)

  

我对C和C ++中volatile的语义的理解就是它   将内存访问转换为I / O

你的理解是错误的。易失性对象是副作用volatile - 它的值可能会被编译期间编译器不可见的内容改变

所以volatile对象必须具有永久性(在其范围内)内存存储位置,必须在任何使用之前从中读取,并在每次更改后保存

参见示例: https://godbolt.org/g/1vm1pq

BTW IMO文章是垃圾 - 它假设程序员认为易失性意味着原子性和一致性,这不是事实。那篇文章应该有一个标题 - “为什么我对易变性的理解是错误的,为什么我仍然生活在神话世界中”