【问题标题】:memory mapped files and pointers to volatile objects内存映射文件和指向易失性对象的指针
【发布时间】:2018-01-26 23:28:33
【问题描述】:

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

此外,我希望使用像 memcpy() 这样的函数来访问共享内存是不正确的,因为签名表明 volatile 限定已被抛弃,并且内存访问不被视为 I/O。

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

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

如果我想遵循标准的字母并将其恢复为我所依赖的语义,关于访问共享内存有什么指导 volatile


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

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

【问题讨论】:

  • 在处理 volatile 时,可能需要在 memcpy 前后添加内存屏障。
  • 请澄清您的意思是“易失性指针”还是“指向易失性数据的非易失性指针”?
  • 您同时标记了 C 和 C++,但谈论的是“标准”。
  • @Arvid:你混淆了线程、进程、内存访问和信念。
  • @knivil 我只想通过 mmap 映射的内存访问磁盘上的文件。而已。我不相信“内存屏障”是 C++ 的概念(有 launder 和 std::experimental::barrier)。我标记了“语言律师”,以表明我的主要兴趣不是一些当代编译器如何在一些流行的架构上生成代码,而是如何以 C++(或 C)抽象正确捕获和执行的方式表达意图机器。对于通过 mmap() 读取和写入文件的目的,多次加载或存储并不是真正的问题(给定一个 std::copy)

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


【解决方案1】:

我对 C 和 C++ 中 volatile 语义的理解是 将内存访问变成 I/O

你的理解是错误的。 Volatile 对象具有副作用 volatile - 在编译期间它的值可能会被编译器不可见的东西改变

因此 volatile 对象必须有一个永久的(当然在其范围内)内存存储位置,必须在任何使用前从中读取,并在每次更改后保存

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

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

【讨论】:

  • 内存访问不是 I/O,没有什么共同点。 I/O 寄存器可以在内存中映射一个地址,然后可以通过内存写入和读取来访问它。但是 I/O != 一般的内存
  • 同意,将 'volatile' 误解为类似于 'atomic' 或 'synchonised' 的东西很流行。它唯一的'请不要在寄存器中优化'
  • @PeterJ 我不知道那篇文章的哪一部分给你的印象是他建议 volatile 可以用于线程同步。 Bullet 8 致力于与这个神话作斗争
  • 因对链接文章的不公平和误导性批评而被否决。许多人确实仍然相信volatile 可以用于线程同步。该文章的作者说这是无效的,因此他的陈述既有效又相关。我不同意其中的一些措辞,但你所做的是纯粹的辱骂,没有任何价值。
  • 这个答案应该有标题“为什么我对链接的文章的理解是错误的,为什么我没有首先阅读它”。
【解决方案2】:

我对 C 和 C++ 中 volatile 语义的理解是,它将内存访问变成了 I/O

不,它不会那样做。 volatile 所做的只是从程序员与编译器进行通信,即可以通过“其他方式”随时更改某个内存区域。

“其他东西”可能是很多不同的东西。例子:

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

由于标准 (5.1.2.3) 保证对 volatile 对象的访问(读/写)可能不会被优化掉,volatile 也可用于阻止某些编译器优化,这在硬件中最有用 -相关的编程。

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

不一定,不。数据的性质无关紧要,重要的是如何更新。

我认为使用 memcpy() 之类的函数来访问共享内存是不正确的

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

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

volatile 不会导致内存屏障行为。它不会(必然)强制表达式按特定顺序执行。

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

回到复制问题 - 如果内存区域在多个 ISR/线程/进程之间“共享”,那么 volatile 将毫无帮助。相反,您需要一些同步方式,例如互斥锁、信号量或临界区。

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

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

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

使用系统特定的 API:s 来保护内存访问,通过 mutex/semaphore/critical section。

【讨论】:

  • 您说“不,它不会那样做”,然后是 I/O 的各种描述(即程序的副作用)。不允许优化的访问会产生副作用。我的问题与同步任何东西无关。
  • 也许我的 mmap() 模型不正确,但从程序的角度来看,它看起来像是可以在程序本身不写入的情况下更改的内存,对吧?
  • @Arvid I/O 一词在计算机中具有非常特殊的含义,即进出计算机的事物。在我的示例中,只有硬件寄存器和 DMA 可以是 I/O 的一种形式。虽然 C 标准正式定义的副作用只是“执行环境的变化”,但这是一个非常模糊的术语。
  • 如果我将值复制到没有 volatile 限定符的内存映射范围中,是什么阻止编译器将其优化为死存储?一个答案可能是将内存返回给 munmap(),但这意味着对底层文件对象的更新不会被映射反映,它就是这样。
  • 我明白了。我承认我使用术语 I/O 是松散的。我的意思是可观察到的副作用。基本上,C++ 承诺做的事情
【解决方案3】:

我认为你想多了。我看不出有任何理由让mmap 或等效(我将在此处使用 POSIX 术语)内存变得易失。

从编译器的角度来看,mmap 返回一个经过修改的对象,然后将其提供给msyncmunmap_Exit 期间隐含的取消映射。这些函数需要被视为 I/O,仅此而已。

您几乎可以将mmap 替换为malloc+read 并将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 相同的页面缓存。

【讨论】:

  • 有趣。这是一个很好的观点,从程序的角度来看,执行 I/O 的是 mmap()munmap(),并且由于 mmap 返回的内存被反馈给 munmap,因此存在对“写”操作。但是,内存映射文件也会即时更新,这有时是所需的属性。在这种情况下,内存访问本身就是副作用。不是吗?即当另一个进程更新文件的内容时
  • @Arvid 更新了答案,因为我无法在字符数限制内回答。
猜你喜欢
  • 1970-01-01
  • 2015-04-23
  • 2014-01-26
  • 1970-01-01
  • 2011-09-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多