ISO/IEC 9899:2011 条款5——5.1.2 执行环境

5.1.2 执行环境


1、定义了两个执行环境:独立式(freestanding)以及宿主的(hosted)。在这两种情况下,当一个派定的C函数被执行环境调用时,程序启动。所有具有静态存储周期的对象应该在程序启动前被初始化(设置为它们的初始值)。这种初始化的方式以及时间点是未指定的。程序终止将控制返回给执行环境。


5.1.2.1 独立式环境

1、在一个独立式环境中(在此环境中下,C程序可以无需借助任何操作系统的帮助进行执行[译者注:比如通过虚拟机解释执行]),在程序启动时所调用的函数名和类型是由实现定义的。任一对一个独立式程序可用的库工具,而并非由条款4所要求的最小集,是由实现定义的。

2、在一个独立式环境中的程序终止的效果是由实现定义的。


5.1.2.2 宿主环境


1、一个宿主环境不需要被提供,但应该遵从以下规定,如果存在的话。


5.1.2.2.1 程序启动

1、在程序启动时所调用的函数被命名为main。实现不对此函数声明原型。它应该被定义为int返回类型,无形参:

int main(void) { /* ... */ }

或者带有两个形参(这里引用为argcargv,当然任意名字都可以被使用,因为它们在所声明的函数中是局部的):

int main(int argc, char *argv[]) { /* ... */ }

或等价物;[注:因而,int可以用一个被定义为int的typedef名字所代替,或者argv的类型可以被写为char ** argv,等等。]或者用其它某些实现自定义的方式。


2、如果main函数的形参被声明,那么它们应该遵守以下强制规定:

——argc的值应该是非负的。

——argv[argc]应该是一个空指针。

——如果argc的值大于零,那么argv[0]argv[argc-1]的数组成员应该包含指向字符串的指针,那么它们通过宿主环境在程序启动之前用实现自定义的值给出。其目的是在程序启动之前,在宿主环境的其它地方提供确定的程序信息。如果宿主环境不能同时提供含带大小写字母的字符串,那么实现应该确保能接收到小写字母的字符串。

——如果argc的值大于零,那么argv[0]所指向的字符串代表了程序名;如果程序名不能从宿主环境获得,那么argv[0][0]应该是空字符。如果argc的值大于1,那么argv[1]argv[argc-1]所指的字符串代表了程序参数

——形参argcargvargv数组所指向的字符串应该是可以被程序所修改的,并且在程序启动到程序结束之间,保留它们最后存储的值。


5.1.2.2.2 程序执行


1、在一个宿主环境下,一个程序可以使用所有在库条款(条款7)中所描述的的函数、宏、类型定义以及对象。


5.1.2.2.3 程序终止


1、如果main函数的返回类型与int兼容,那么从对main函数的初始调用的一个返回等价于用main函数所返回的值作为实参来调用exit函数;[注:对于符合6.2.4而言,在main中所声明的具有自动存储周期的对象的生命周期将在前者情况下结束,甚至在后者中它们也将不会拥有。]到达}来终止main函数,返回一个0值。如果返回类型与int不兼容,那么返回给宿主环境的终止状态是未指定的。


5.1.2.3 程序执行

1、在本国际标准中所描述的语义描述了一个抽象机器的行为,其中优化问题与其无关。

2、访问一个易变更的对象、修改一个对象、修改一个文件,或调用一个函数,做上述任一一种操作都是具有副作用的,[注:IEC 60559标准对二进制浮点算术需要某些用户可访问的状态标志以及控制标志。浮点操作隐式地设置状态标志;模式影响了浮点操作的结果值。支持这种浮点状态的实现要求把对浮点状态的改变看作为副作用——详见附录F。浮点环境库<fenv.h>提供了一个编程工具用于指示这些副作用什么时候要紧,而在其它情况下实现不用特别关心。]它们在执行环境的状态下改变。对一个表达式做通常的计算包括了对值的计算以及副作用的初始实施。对一左值表达式的值计算包括了确定所指派对象的标识。

3、顺序前(Sequenced before)是在两个计算之间由一个单线程所执行的一个非对称、传递的、成对的关系,在那些计算之间引发一个部分顺序。给定任意两个两个计算A和B,如果A顺在在B之前,那么对A的执行应该在对B的执行之前。(反过来说,如果A顺序在B之前,那么B顺序就在A之后。)如果A顺序不在B之前或之后,那么A和B之间是不分次序的。当A顺序要么亦或在B之前,亦或在B之后,但它没有指定是哪一个时,A和B的计算是不确定顺序的。[注:对于不分次序的计算的执行可以交错执行。未确定顺序的计算不能交错,但可以用任一次序执行。]在表达式A和B之间有顺序点出现,暗示了与A相关联的每个值计算和副作用顺序在与B相关联的每个值计算和副作用之前。(在附录C中给出了顺序点的概述。)

4、在抽象机中,所有表达式通过指定的语义进行计算。一个实际实现不需要计算一个表达式的一部分如果实现能推导出该值没有被使用,也没有副作用产生(包括通过一个函数调用或访问一个易变更对象所引起的副作用)。

5、当对抽象机的处理被一个信号的接收中断时,既非无锁的原子对象亦非volatile sig_atomic_t类型的对象的值是未指定的,浮点环境下的状态也一样。任一由这种既非无锁的对象亦非volatile sig_atomic_t类型的例程所修改的对象变为未确定的,当此例程存在时,浮点环境的状态也一样,如果它被此例程所修改,并且没有被恢复到其原始状态。

6、对一个顺从标准实现的最低要求是:

——对易变更对象的访问严格根据抽象机的规则进行计算。

——在程序终止时,根据抽象语义所产生的,所有写入文件的数据应该与程序执行的结果一样。

——可交互设备的输入与输出动态性应该在7.21.3中所指定的那样发生。这些要求的目的是非缓冲的或行缓冲的输出尽可能快地出现,以确保提示消息实际出现在一个程序等待输入之前。

这是程序可观察到的行为

7、一个可交互设备由什么组成是由实现定义的。

8、在抽象与实际语义之间的更严格的对应关系可以通过每个实现来定义。

9、例1  一个实现可以在抽象与实际语义之间定义一个一对一的对应关系:在每个顺序点,实际对象的值将符合由抽象语义所指定的。关键字volatile然后会是冗余的。

10、一个实现可以在每个翻译单元内执行各种优化,这样,仅当跨翻译单元边界做出函数调用时,实际语义才需要符合抽象语义。在这么一种实现中,在每个函数入口和函数返回时,函数调用者与被调函数处于不同的翻译单元,所有外部连接对象的值以及所有通过其中指针对象的值要符合抽象语义。此外,在每个这样的函数入口时,被调函数的形参值以及通过那里的指针可访问的所有对象要符合抽象语义。在这种类型的实现中,被中断服务例程所引用的对象——此中断服务例程通过signal函数而被激活——将需要显式的volatile存储的指定,以及其它实现定义的限制。

11、例2  在以下片断执行中:

char c1, c2;
/* ... */
c1 = c1 + c2;

“整型晋升”要求抽象机将每个变量值先进行晋升到int尺寸,然后对这两个int变量相加,再对求和结果做截断。最后所提供的两个char变量的加法可能不会有溢出,或是带有静默封装的溢出来产生正确的结果,实际执行只需要产生同样的结果,可能会忽略晋升。

12、例3  类似地,在以下片断中

float f1, f2;
double d;
/* ... */
f1 = f2 * d;

这个乘法可以使用单精度算术来执行,如果实现可以确定结果会是一样,就像似它使用双精度算术来执行一样(比如,如果ddouble类型的常量2.0来代替)。

13、例4  引入宽寄存器的实现必须注意遵从适当的语义。无论值是在一个寄存器中表示还是在存储器中表示,它们都是独立的。比如,一个寄存器的隐式溢出译者注:这里的寄存器溢出是指当前没有足够的寄存器来存放某个值,从而需要把该值存放到存储器中]不被允许来更改该值。同样,一个显式的存储加载要求取整到存储类型的精度。特别地,类型投射(cast)和赋值需要执行它们指定的转换。对于以下片断

double d1, d2;
float f;
d1 = f = expression;        // 译者注:expression类型到f可能会有取整操作;f到d1有类型转换操作
d2 = (float)expression;    // 译者注:对表达式进行类型转换

赋给d1d2的值要求被转换为float


14、例5  对浮点表达式的重新安排经常会被限制,因为精度以及范围的约束。实现通常不能应用加法或乘法的数学上的结合律,也不能应用分配率,因为舍入误差,甚至在没有上溢或下溢的情况,也不能。类似地,实现通常不能为了重新安排表达式来更换十进制常量。在以下片断中,由对于实数的数学上的规则所建议的重新安排经常不是有效的(见F.9)。

double x, y, z;
/* ... */
x = (x * y) *z;    // 不等价于x *= y * z;
z = (x - y) + y;   // 不等价于z = x;
z = x + (x * y;    // 不等价于z = x * (1.0 + y);
z = x / 5.0;       // 不等价于y = x * 0.2;

15、例6  为了描述表达式的组合行为,在以下片断中:

int a, b;
/* ... */
a = a + 32760 + b + 5;

此表达式语句行为与下列完全一样:

a = (((a + 32760) + b) + 5);

由于这些操作符的结合律与优先级。从而,(a + 32760)的求和结果接下来与b相加,然后结果再与5相加,最后的结果赋值给a。在一个机器上,上溢产生一个显式的陷阱并且在值可被一个int所表示的范围是[-32768, +32767],实现不能将表达式重写为:

a = ((a + b) + 32765);

由于,如果ab的值分别为-32454和-15,那么a + b 的和会产生一个陷阱,而原始表达式则不会;该表达式也不能被重写为:

a = ((a + 32765) + b);

或是

a = (a + (b + 32765));

由于ab的值可能分别是4和-8,或-17和12。然后,在一个溢出静默地生成某个值,正、负上溢取消的机器,那么上述表达式可以被实现以上述任意一种方式来写,因为会得到同样的结果。

16、例7  一个表达式的分组并不完全判定其求值。在以下片断中

#include <stdio.h>
int sum;
char *p;
/* ... */
sum = sum * 10 - '0' + (*p++ = getchar());

表达式语句被分组,就好比写成了这种

sum = (((sum * 10) - '0') + ((*(p++)) = (getchar())));

但实际p的递增可以在之前序列点与下一个序列点(;)之间的任何时刻发生,并且对getchar的调用可以在需要其返回值之前的任一点发生。


5.1.2.4 多线程执行与数据竞争

1、在一个宿主实现的环境下,一个程序可以具有多个执行线程(或线程)并发运行。每个线程的执行由本标准剩余部分所定义的那样进行。整个程序的执行由对其所有线程的执行所组成。[注:执行通常可以被视作为对所有线程的交错执行。然而,某些种类的原子操作允许执行,比如由以下所描述的一个简单交错的执行不一致。]在一个独立式实现下,一个程序是否可以具有多个线程执行是由实现定义的。

2、在一个特定点,对一个线程T可见的一个对象的值,由T所存储在此对象中的一个值,或是由另一个线程在此对象中所存储的值,是该对象的初始值,根据以下规则。

3、注1  在某些情况下,可能会有未定义行为。本章节的许多部分激发于对带有显式的以及详细可见性强制规定的原子操作的支持的愿望。然而,这对限制更多的程序也隐式支持了一个更简单的视图。

4、如果两个表达式的其中一个修改了一个存储位置,并且另一个读或修改同一个存储位置,那么这两个表达式计算冲突

5、库定义了一些原子操作(7.17)以及对互斥体的操作(7.26.4),它们被特别地定义为同步操作。这些操作在一个线程对另一个线程可见的情况下进行赋值中扮演了一个特殊的角色。对一个或多个存储位置上的一个同步操作要么是一个获得操作,要么是一个释放操作,亦或同时为一个获得和释放操作,亦或是一个消费操作。不含带一个相关联的存储位置的一个同步操作是一个栅栏(fence),并且可以要么是一个获得栅栏,一个释放栅栏,或是同时是一个获得栅栏和释放栅栏的操作。此外,还有松弛的原子操作,而它们并不是同步操作,以及原子的读-修改-写操作,它们具有特殊特性。

6、注2  比如,要获得一个互斥体的一个调用将在构成一个互斥体的位置执行一个获得操作。相应地,释放同一个互斥体的操作在相同的位置上执行一个释放操作。通俗地来说,在A上执行一个释放操作迫使先前在其它存储器位置上的副作用变得对其它线程可见,这些线程稍后将会在A上执行一个获得或消费操作。我们并不包含松弛的原子操作作为同步操作,尽管它们跟同步操作一样,不过它们对数据竞争没有多少贡献。

7、对一个特定原子对象M的所有修改以某种特定总的次序发生,这称为对M修改次序。如果A和B是作为对一个原子对象M的修改,并且A发生在B之前,那么在对M的修改次序中,A应该在B之前,这由以下进行定义。

8、注3  这陈述了修改次序必须考虑“发生在XX之前”这个关系。

9、注4  对于每个原子对象各自有一个独立的次序。对于所有对象,这些可以被结合到一单个总的次序,而不做要求。通常来说,这将会不太可能,由于不同的线程可能会以不一致的次序观察到不同变量的修改。

10、由对一个原子对象M的一个释放操作A所引发的一个释放序列,在对M的修改次序中是副作用的一个最大邻近的子序列,这里,第一个操作是A,并且每个子序列操作要么由执行释放的同一个线程执行,要么由一个原子的读-修改-写操作执行。

11、某些库调用与由另一个线程所执行的其它库调用同步。特别地,在对一个对象M执行一个释放操作的一个原子操作A与对M执行一个获得操作的一个原子操作B进行同步,并且B读一个值,在由A所引发的释放序列中的任一副作用之前写入了该值。

12、注5  除了在特定情况外,读后面的一个值并不需要确保如以下所描述的可见性。这么一个要求有时会妨碍高效实现。

13、注6  同步操作的规定定义了,一个线程什么时候去读由另一个线程所写的值。对于原子变量,该定义是清晰的。对一个给定互斥体上的所有操作以一个总的次序发生。每个互斥体通过最后的互斥体释放来获得“读已被写入的值”。

14、对A的一次计算会携带对B的一次计算的依赖[注:“携带一个依赖”关系是“发生在XX之前”关系的一个子集,并且与严格的线程内类似。],如果:

——A的值被用作为B的操作数,除非:

    ⚫︎  B是对kill_dependency宏的调用,

    ⚫︎  A是一个&&||操作符的左操作数,

    ⚫︎  A是一个?:操作符的左操作数,或

    ⚫︎  A是一个,操作符的左操作数;

——A对一个标量对象或位域M写,B从M读,并且A顺序在B之前,或

——对于某个计算X,A携带了对X的一个依赖,且X携带了对B的一个依赖。

17、注7  “线程间发生在XX之前”关系描述了“在XX之前顺序”、“与XX同步”、“在XX之前依赖次序”关系的随机结合,不过有两个例外。第一个例外是一个结合不允许以“在XX之前的依赖次序”后面跟“在XX之前顺序”结束。这个限制的理由是一个参与“在XX之前依赖次序”关系的消费操作仅以关于此消费操作实际携带一个依赖的操作提供次序。此限制仅应用于这么一个结合的结束的理由是任一后续释放操作将提供所需要的对前一次消费操作的次序。第二个例外是一个结合不允许由整个“在XX之前顺序”构成。这个限制的理由是(1)为了允许“线程间发生在XX之前”可以传递地关闭,并且(2)由以下定义的“在XX之前发生”关系提供了由整个“在XX之前顺序”所构成的关系。

18、如果A的顺序在B之前,或者A线程间发生在B之前,那么对A的计算发生在对B的计算之前。

19、一个可见的副作用A作用在一个对象M上,M是关于M的一个值计算B,满足条件:

——A发生在B之前,并且

——对M没有其它副作用X,使得A发生在X之前,而X又发生在B之前。

一个非原子标量对象M的值(由计算B来确定)应该是由可见的副作用A所存储的值。

20、注8  如果关于对一个非原子对象的副作用可见存在歧义,,那么就会有一个数据竞争,并且该行为是未定义的。

21、注9  这陈述了对普通变量的操作并不会被可见地重新安排次序。这实际上并不可探测到没有数据竞争,但是它却有必要来确保在这里所定义的数据竞争,以及带有对原子操作的使用上的约束,相应于在一个简单的交错(顺序上一致的)执行上的数据竞争。

22、对一个原子对象M上,关于M的一个值计算B,可见的副作用顺序是在M的修改次序中副作用的一个最大连续子顺序,其中第一个副作用对于B可见,并且对于每个后续副作用,没有B发生在它之前的情况。一个由计算B所确定的原子对象M的值,应该是由在关于B的M的可见顺序中的某个操作所存储的值。此外,如果对一个原子对象M的一个值计算发生在对M的一个值计算B之前,并且通过A所计算的值相应于由副作用X所存储的值,那么由B所计算的值应该要么等于由A所计算出的值,要么作为由副作用Y所存储的值,这里Y在对M的修改次序中发生在X之后。

23、注10  这有效地驳回了编译器对针对一单个对象的原子操作的重新安排,即使这两个操作都是“松弛”的加载。通过这么做,我们有效地使得“Cache一致性”确保由大部分对C可用的硬件原子操作。

24、注11  可见的次序依赖于“发生在XX之前”的关系,而这关系反过来又依赖于通过对原子操作的加载所观察到的值,而这方面就是我们在这里所要约束的。这目的是,一定存在对原子操作与它们所观察到的修改相关联,连带适当被选出的修改次序以及上述所描述的派生出来的“发生在XX之前”的关系,满足这里所利用的结果强制规定。

25、一个程序的执行包含了一个数据竞争,如果它在不同的线程中含有两个冲突的动作,其中至少有一个不是原子的,且两者都不在另一之前发生。任一这样的数据竞争导致未定义行为。

26、注12  可以展示出,正确使用简单互斥体和memory_order_seq_cst操作的程序,来防止所有数据竞争,并且不使用其它同步操作,行为就好像由它们所属的线程所执行的操作被简单地交错,随着对一个在那个交错中最后所存储值的对象的每个值计算。这正常来说称为“顺序一致性”。然而,这仅应用于无数据竞争的程序,并且无数据竞争的程序不能观察大部分程序变化,这些变化并不改变单线程程序的语义。实际上,大部分单线程程序变化继续被准许,由于任一行为不同的程序,作为结果一定包含了未定义行为。

27、注13  引入对一个潜在共享的存储器位置的赋值,该存储器位置的内容不会被抽象机所修改,的编译器变换通常会被本标准阻止,由于这样的一个赋值可能会覆盖由另一个线程的赋值,在这样的情况下情况下,一个抽象机执行可能不会遭遇一次数据竞争。这包括了数据成员赋值在各自独立的存储器位置中覆盖邻近的成员的实现。我们也一般在这么个情况下阻止原子加载的重新次序安排,原子操作可能会有别名,由于这可能违背了“可见的顺序”规则。

28、引入对一个投机共享的存储器位置的读的变换可能不保留由本标准所定义的程序语义,由于它们潜在地引入了一次数据竞争。然而,它们一般在优化编译器的上下文中是有效的,而该目标是含有对数据竞争良好定义的语义的一个特定机器。它们对于一个假定的机器可能是无效的,该机器不能容忍竞争也不能提供硬件竞争探测。

原文地址:https://www.cnblogs.com/zenny-chen/p/4213951.html