C语言 volatile语义的形式化理解

lnlaulya  于 2023-11-17  发布在  其他
关注(0)|答案(3)|浏览(100)

5.1.2.3定义了以下内容:
在抽象机器中,所有表达式都按照语义的规定进行求值。如果一个实际的实现可以推断出表达式的值没有被使用,并且没有产生任何必要的副作用(包括调用函数或通过对对象的volatile访问所引起的副作用),那么它就不需要对表达式的一部分求值。
这个定义对我来说有点模糊,我特别关心 Sequenced before 的定义,以及它是如何(如果)与volatile绑定的。
请以下列函数为例:

void foo(volatile int* a, int *b, volatile int *c){
    *a = 1;
    *b = 2;
    *c = 3;
}

字符串
基本上我有两个问题:

  1. *a = 1;的顺序是否在*c = 3之前?
  2. *a = 1;的顺序是否在*b = 2;之前?
    若有,原因为何?
    5.1.2.3处定义为:
    Sequenced before是由单个线程执行的求值之间的非对称、可传递、成对关系,它会在这些求值之间产生偏序
vsmadaxz

vsmadaxz1#

在定义C17 5.1.2.3 §3的“之前排序”的同一部分中进一步向下:
在表达式A和B的求值之间存在序列点意味着与A相关联的每个值计算和副作用在与B相关联的每个值计算和副作用之前排序。
这是非常清楚的,没有任何解释的余地。
在你的代码中,每一个;都标记了一个序列点(完整表达式的结束),所以它毫无疑问是从上到下排序的,这与volatile无关。
另一个问题是优化编译器是否被允许对表达式进行重新排序。C语言中“抽象机器”的概念是模糊的,通常是没有帮助的,但它说的是:
在抽象机器中,所有的表达式都按照语义进行求值。如果一个实际的实现可以推断出表达式的值没有被使用,并且没有产生任何必要的副作用(包括调用函数或访问volatile对象引起的副作用),那么它就不需要对表达式的一部分求值。
/--/
volatile对象的访问严格按照抽象机的规则进行评估。
“语义”和“抽象机器的规则”指的是前面提到的“排序在前”部分。
除了禁止对volatile对象访问的指令重新排序之外,这不能很好地以任何其他方式解释。
然而,*b=2;表达式不是volatile限定的。这里是它变得模糊的地方。人们可以将上面引用的部分读为“不允许跨volatile访问的指令重新排序”。也就是说-volatile访问必须充当内存屏障-我认为这是正确的解释。
但实际上,在多个内核中实现并行执行预取和/或流水线的各种CPU制造商在设计过程中并不一定会深入研究C标准。因此,硬件可能会规定如何重新排序,然后编译器供应商可以做很多事情来修复它。这样的硬件和编译器可能是C语言的不一致实现。
我们还可能注意到,volatile的行为在即将到来的C23中略有变化。volatileaccess 现在被视为volatileobject access,这意味着像*(volatile int*)0x1234这样的东西严格来说并不遵循与volatile int* ptr = (volatile int*)0x1234; ... *ptr相同的规则。这是一个缺陷,有一个缺陷报告,它已在C23中得到修复。

iyr7buue

iyr7buue2#

volatile-qualified access的语义是实现定义的。许多商业编译器(如MSVC)被设计为避免在volatile write中对任何内存操作进行排序,并将volatile read之前重新排序的操作限制为那些将它们与先前的读取合并的操作,因此序列如下:

in_buff_ptr = my_buff;
in_buff_count = 4;
do {} while(in_buff_count);
doSomething(in_buff);

字符串
如果任何可能访问缓冲区的东西在访问与运行上述代码的核心高速缓存一致的核心内的普通存储器时都会这样做,那么它将是可靠的(注意,在大多数嵌入式系统上,所有核心都是高速缓存一致的-如果没有别的,因为该特性适用于所有单核系统)。
该标准将允许volatile访问通过以某种方式排队并由执行代码控制之外的东西处理而得到处理的可能性,这种情况通常适用于I/O设备。代码将被要求表现为好像操作按指定的顺序排队,但该标准将完全不知道之后会发生什么,包括动作是否以指定的顺序发生。在一些现实世界的系统上,当执行一系列事件时,这是完全合理的,例如:

IOPINS = x; // IOPINS is volatile and when written sets the state of some I/O pins
y = IOPINS; // Reading IOPINS reports the clock-synchronized state of those pins


读操作可能会产生恰好在写操作发生之前的引脚状态。如果系统试图直接读取I/O引脚的瞬时状态,则如果引脚状态恰好在其被读取时发生变化,则可能会发生糟糕的事情。如果代码在错误的时刻执行诸如“ADD R 0,IOPINS”之类的操作,而R 0保持255并且引脚上的值从0变为1,寄存器0中的物理位可能会处于所谓的“亚稳态”状态。为了避免这种情况,一些系统通过所谓的“双同步器”传递端口位,这可以避免这个问题,但在引脚实际改变状态和观察到它们改变状态之间增加了延迟。
如果想要对这样的I/O设备执行可靠的读-修改-写操作,阅读和理解它们的硬件手册是很重要的。编程人员通常无法知道哪些类型的I/O设备可以通过volatile-qualified操作访问,以及处理它们需要什么特殊的预防措施。编译器所能做的最好的事情通常是避免在跨volatile限定的访问,并将硬件问题留给程序员。

hgc7kmma

hgc7kmma3#

在抽象机内部,标准用于指定语义,*a = 1;*b = 2;之前,后者在*c = 3;之前。
在抽象机之外,C标准除了规定程序的可观察行为必须与抽象机相同之外,对*b = 2;没有任何规定。该标准根本不要求实现*b = 2;*b = 2;在抽象机之外甚至没有任何意义; C标准甚至不要求*b在任何意义上存在,更不用说它在*a = 1;*c = 3;之后。

相关问题