c - 在 C 中,如何确保内存加载只执行一次?

标签 c language-lawyer

我正在对两个进程进行编程,这些进程通过在共享内存段中相互发布消息来进行通信。尽管消息不是以原子方式访问的,但是通过使用存储释放和加载获取访问的共享原子对象来保护消息来实现同步。

我的问题是关于安全。进程不相互信任。收到消息后,进程不会假设消息格式正确;它首先将消息从共享内存复制到私有(private)内存,然后对这个私有(private)副本执行一些验证,如果有效,继续处理这个相同的私有(private)副本。制作此私有(private)副本至关重要,因为它可以防止其他进程在验证和使用之间修改消息的 TOC/TOU 攻击。

我的问题如下:标准是否保证聪明的 C 编译器永远不会决定它可以读取原始文件而不是副本?想象以下场景,其中消息是一个简单的整数:

int private = *pshared; // pshared points to the message in shared memory
...
if (is_valid(private)) {
  ...
  handle(private);
}

如果编译器用完寄存器,暂时需要溢出 private ,它是否可以决定,而不是将其溢出到堆栈中,而是可以简单地丢弃其值并从 *pshared 重新加载它?稍后,前提是别名分析确保此线程未更改 *pshared ?

我的猜测是这样的编译器优化不会保留源程序的语义,因此是非法的:pshared不指向可证明只能从该线程访问的对象(例如分配在堆栈上且地址未泄漏的对象),因此编译器不能排除某些其他线程可能并发修改 *pshared .相比之下,编译器可以消除冗余负载,因为可能的行为之一是在冗余负载之间没有其他线程运行,因此当前线程必须准备好处理这种特定行为。

任何人都可以确认或否定这种猜测并可能提供对标准相关部分的引用吗?

(顺便说一句:我假设消息类型没有陷阱表示,因此总是定义负载。)

更新

有几位发帖人评论了同步的必要性,我并不打算深入探讨,因为我相信我已经涵盖了这一点。但既然人们指出了这一点,我提供更多细节是公平的。

我正在两个互不信任的实体之间实现一个低级异步通信系统。我用进程运行测试,但最终会转移到虚拟机管理程序之上的虚拟机。我可以使用两个基本要素:共享内存和通知机制(通常,将 IRQ 注入(inject)另一个虚拟机)。

我已经实现了一个通用的循环缓冲区结构,通信实体可以用它来生成消息,然后发送上述通知,让彼此知道什么时候有东西要消费。每个实体都维护自己的私有(private)状态,用于跟踪它生产/消费的内容,并且在由消息槽和原子整数组成的共享内存中存在一个共享状态,用于跟踪保存待处理消息的区域的边界。该协议(protocol)明确地标识了哪些消息槽将在任何时候由哪个实体独占访问。当它需要产生一条消息时,一个实体将一条消息(非原子地)写入适当的槽,然后对适当的原子整数执行原子存储释放以将槽的所有权转移给另一个实体,然后等待直到内存写入已完成,然后发送通知以唤醒另一个实体。收到通知后,另一个实体应该对适当的原子整数执行原子加载获取,确定有多少待处理消息,然后使用它们。
*pshared的负载在我的代码片段中只是一个例子,说明消费一条微不足道的 ( int ) 消息是什么样的。在现实环境中,消息将是一个结构。消费一条消息不需要任何特定的原子性或同步性,因为根据协议(protocol)的规定,只有在消费实体与另一个实体同步并知道它拥有消息槽时才会发生。只要两个实体都遵循协议(protocol),一切都会完美无缺。

现在,我不希望实体必须相互信任。它们的实现必须对恶意实体具有鲁棒性,该实体会无视协议(protocol)并随时写入共享内存段。如果发生这种情况,恶意实体应该能够实现的唯一目标就是中断通信。想想一个典型的服务器,它必须准备好处理恶意客户端的格式错误的请求,而不会让这种不当行为导致缓冲区溢出或越界访问。

因此,虽然协议(protocol)依赖同步进行正常操作,但实体必须准备好随时更改共享内存的内容。我所需要的只是一种方法来确保,在实体制作消息的私有(private)副本后,它验证并使用相同的副本,并且不再访问原始副本。

我有一个使用 volatile 读取复制消息的实现,从而使编译器清楚共享内存没有普通内存语义。我相信这已经足够了;我想知道是否有必要。

最佳答案

您应该通过 volatile 通知编译器共享内存可以随时更改。修饰符。

volatile int *pshared;
...
int private = *pshared; // pshared points to the message in shared memory
...
if (is_valid(private)) {
  ...
  handle(private);
}

*pshared被声明为 volatile,编译器不能再假设 *psharedprivate保持相同的值。

根据您的编辑,现在很清楚,我们都知道共享内存上的 volatile 修饰符足以保证编译器将遵守对该共享内存的所有访问的时间性。

无论如何,C99 的 N1256 草案在 5.1.2.3 程序执行(强调我的)中有明确说明

2 Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression may produce side effects. At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.

5 The least requirements on a conforming implementation are:
— At sequence points, volatile objects are stable in the sense that previous accesses are complete and subsequent accesses have not yet occurred
— At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.



那让我们想想即使pshared不符合 volatile,private值必须是从 *pshared 加载的评价前is_valid ,而且作为抽象机没有理由在handle评价前改它,一致的实现不应该改变它。它最多可以删除对 handle 的调用。如果它不包含不太可能发生的副作用

无论如何,这只是学术讨论,因为我无法想象共享内存不需要 volatile 的真实用例。修饰符。如果您不使用它,编译器可以自由地相信前一个值仍然有效,因此在第二次访问时,您仍将获得第一个值。所以即使这个问题的答案是没有必要,你仍然必须使用volatile int *pshared; .

关于c - 在 C 中,如何确保内存加载只执行一次?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37699774/

相关文章:

c - '&&' 和 '||' 是在运算符优先级的同一行还是不同的行?

c - 如何打印一个汉字?

c - 是否需要保留填充位?

c++ - 一些 STL 容器的 std::allocator 不匹配

c++ - 为什么可推导类型列表中缺少指向函数类型的指针

c - 无线工具 : Converting network essid to char

c - C程序中的可变长度参数

c - 是否所有指针都保证通过 void * 正确往返?

c++ - 当两者都是 32 位宽时,在 C(或 C++)中使用 `unsigned long` 和 `unsigned int` 是否存在可观察到的差异?

c++ - 多个 CRTP 基类的对齐