5.1.2 执行环境
NOTE
这里的重点是:标准区分独立环境与宿主环境,并且通过“抽象机器”来描述程序执行语义。实现可以优化,但不能破坏标准要求的可观察行为。
5.1.2 执行环境
5.1.2.1 一般规定
标准定义了两种执行环境:独立环境(freestanding)和宿主环境(hosted)。在这两种环境中,程序启动都发生在执行环境调用某个指定的 C 函数时。
所有具有静态存储期的对象,都应在程序启动之前完成初始化(即被设为初始值)。至于这种初始化以何种方式、在何时完成,则未作规定。
程序终止时,控制权会返回给执行环境。
前向引用:对象的存储期(6.2.4)、初始化(6.7.11)。
5.1.2.2 独立环境
在独立环境中(即 C 程序的执行可以不依赖操作系统提供支持的环境),程序启动时被调用的函数,其名称和类型由实现定义。
除第 4 章所要求的最小库集合之外,独立程序可用的任何其他库设施,也都由实现定义。
程序在独立环境中终止时的效果,由实现定义。
5.1.2.3 宿主环境
5.1.2.3.1 一般规定
- 标准并不要求必须提供宿主环境;但如果提供了宿主环境,则它应符合以下规定。
5.1.2.3.2 程序启动
- 程序启动时被调用的函数名为
main。实现不会为该函数声明原型。它应定义为返回类型为int,并且:
int main(void) { /* ... */ }或者定义为带两个参数:
int main(int argc, char *argv[]) { /* ... */ }参数名虽然这里写作 argc 和 argv,但因为它们在函数内具有局部作用域,所以也可以使用其他名字。或者也可以采用与上述形式等价的写法。6)还可以采用其他实现定义的方式。
- 如果声明了这些参数,则
main函数的参数应满足以下约束:argc的值应为非负。argv[argc]应为空指针。- 如果
argc大于 0,则argv[0]到argv[argc - 1](含两端)都应是指向字符串的指针;这些字符串的值由宿主环境在程序启动之前以实现定义的方式提供。其意图,是把程序启动之前由宿主环境其他部分确定的信息提供给程序。如果宿主环境无法同时提供包含大写字母和小写字母的字符串,则实现应确保这些字符串以小写形式被接收。 - 如果
argc大于 0,则argv[0]所指向的字符串表示程序名;如果宿主环境无法提供程序名,则argv[0][0]应为空字符。如果argc大于 1,则argv[1]到argv[argc - 1]所指向的字符串表示程序参数。 - 参数
argc、argv以及argv数组所指向的字符串,都应允许程序修改,并且在程序启动到程序终止期间保持其最后一次存储的值。
5.1.2.3.3 程序执行
- 在宿主环境中,程序可以使用库条款(第 7 章)中描述的全部函数、宏、类型定义和对象。
5.1.2.3.4 程序终止
- 如果
main函数的返回类型与int兼容,那么从对main的初始调用中return,等价于以main的返回值为实参调用exit函数。7)执行到结束main的}时,则返回值为0。如果返回类型与int不兼容,则返回给宿主环境的终止状态未作规定。
脚注说明
6)因此,int 可以替换为一个以 int 为定义的 typedef 名字;argv 的类型也可以写成 char **argv;返回类型还可以写成 typeof(1),等等。
7)按照 6.2.4 的规定,在前一种情况下,main 中声明的自动存储期对象的生存期已经结束;而在后一种情况下,它们未必结束。
前向引用:术语定义(7.1.1)、exit 函数(7.24.4.4)。
5.1.2.4 程序语义
本文档中的语义描述,描述的是一台抽象机器的行为;在这台抽象机器中,优化问题不在考虑范围内。
通过
volatile限定类型左值访问对象,构成一次易失访问。对对象的易失访问、修改对象、修改文件,以及调用执行这些操作的函数,都属于副作用。8)副作用是执行环境状态的变化。表达式求值通常既包括值计算,也包括触发副作用。对于左值表达式,值计算还包括确定被指定对象的身份。“先序于”(sequenced before)是同一线程内各次求值之间的一种非对称、可传递、成对关系,它在这些求值之间诱导出一个偏序。对于任意两个求值
A和B,如果A先序于B,那么A的执行就应早于B。反过来,如果A先序于B,那么B就后序于A。如果A既不先序于B,也不后序于B,则二者是未定序的(unsequenced)。如果A和B中必有一个先于另一个,但标准不规定究竟是哪一个,则二者是不确定顺序的(indeterminately sequenced)。9)如果表达式A与B的求值之间存在一个序列点,则与A相关的全部值计算和副作用,都先序于与B相关的全部值计算和副作用。(附录 C 给出了序列点的总结。)在抽象机器中,所有表达式都按语义规定被求值。实际实现如果能够推导出某个表达式的一部分的值不会被使用,且不会产生所需的副作用(包括函数调用产生的副作用,以及通过
volatile访问对象产生的副作用),则不要求必须对这一部分求值。当抽象机器的处理因收到信号而中断时,那些既不是无锁原子对象、也不是
volatile sig_atomic_t类型的对象,其值未作规定;动态浮点环境的状态也未作规定。若信号处理程序修改了某个既非无锁原子对象也非volatile sig_atomic_t类型的对象,则在处理程序退出时,该对象的表示变为不确定表示;如果处理程序修改了动态浮点环境且未恢复原状,则其状态也同样变为不确定。对符合标准的实现,最低要求如下:
- 对对象的易失访问,必须严格按照抽象机器的规则求值。
- 程序终止时,写入文件的全部数据,都应与按照抽象语义执行该程序所得结果一致。
- 交互式设备的输入输出动态,应按 7.23.3 的规定发生。其意图是让无缓冲或行缓冲输出尽可能及时地出现,以保证提示信息先于程序等待输入而显示出来。
这就是程序的可观察行为。
什么构成交互式设备,由实现定义。
各个实现还可以定义比这更严格的“抽象语义与实际语义之间的对应关系”。
5.1.2.5 多线程执行与数据竞争
在宿主实现中,只要没有定义
__STDC_NO_THREADS__,程序就可以同时运行多个执行线程(thread)。每个线程的执行都按本文档其余部分的规定进行。整个程序的执行,由它全部线程的执行共同构成。10)在独立实现中,程序是否能够拥有多个执行线程,由实现定义。对于线程
T来说,它在某个特定时刻能看到某对象的值,可能是:- 该对象的初始值;
- 由
T自己写入的某个值; - 由其他线程写入的某个值;
至于究竟是哪一种,则由本小节剩余部分的规则决定。
注 1:在某些情况下,结果也可能是未定义行为。本节很大程度上是为支持具有显式、精细可见性约束的原子操作而设置的;但它也隐式支持一种适用于受限程序的更简单视角。
如果两个表达式求值中,一个修改了某个内存位置,而另一个读取或修改同一个内存位置,则这两个求值是冲突的。
库定义了原子操作(7.17)以及互斥锁上的操作(7.28.4),并将它们专门标识为同步操作。这些操作在使一个线程中的赋值对另一个线程可见时起特殊作用。
作用于一个或多个内存位置的同步操作,可以是以下之一:
- 获取操作(acquire operation)
- 释放操作(release operation)
- 同时是获取和释放的操作
- 消费操作(consume operation)
没有关联内存位置的同步操作称为栅栏(fence);它可以是获取栅栏、释放栅栏,或同时是获取和释放的栅栏。
此外,还有:
- 宽松原子操作(relaxed atomic operations),它们不是同步操作;
- 原子读-改-写操作(atomic read-modify-write operations),它们具有特殊性质。
注 2:例如,获取一个互斥锁的调用,会对构成该互斥锁的那些内存位置执行获取操作;对应地,释放同一个互斥锁的调用,会对这些相同位置执行释放操作。非正式地说,对
A执行释放操作,会迫使之前对其他内存位置产生的副作用,对后来在A上执行获取操作或消费操作的其他线程变得可见。宽松原子操作虽然与同步操作一样不会导致数据竞争,但不被计为同步操作。对某个特定原子对象
M的全部修改,会以某种特定的全序发生,这个次序称为M的修改顺序(modification order)。如果A和B都是对原子对象M的修改,并且A先于B发生(happens before),那么A必须在M的修改顺序中排在B前面。注 3:这说明修改顺序应尊重“先于发生”关系。
注 4:每个原子对象各自拥有独立的顺序;标准并不要求把所有对象的这些顺序合并成一个覆盖全部对象的单一全序。一般来说,这也是不可能的,因为不同线程可能会以不一致的顺序观察到不同对象上的修改。以原子对象
M上某个释放操作A为首的释放序列(release sequence),是M的修改顺序中一个极大的连续子序列,其第一个操作是A,并且其后的每个操作都满足以下二者之一:
- 由执行释放操作的同一线程执行;
- 是一次原子读-改-写操作。
某些库调用会与另一个线程执行的库调用发生同步。特别地,如果某个原子操作
A在对象M上执行释放操作,而原子操作B在M上执行获取操作,并且B读到了由A所引出的释放序列中某个副作用写入的值,那么A与B同步。注 5:除标准明确规定的情形外,读到一个更晚的值,并不必然保证本小节后文所说的那种可见性;否则有时会妨碍高效实现。
注 6:同步操作的规范会定义一个操作何时读到了另一个操作写入的值。对原子对象而言,这一定义是清晰的。对于某个给定互斥锁,其上的全部操作都发生在单一全序中;每次成功获取该互斥锁,都会“读到”最近一次释放该互斥锁所写入的值。如果满足以下任一条件,则求值
A会向求值B携带依赖(carry a dependency)11):
A的值被用作B的操作数,但以下情况除外:B是对宏kill_dependency的调用;A是&&或||运算符的左操作数;A是?:运算符的左操作数;A是逗号运算符的左操作数;
A向某个标量对象或位字段M写入,B从M中读到了由A写入的值,并且A先序于B;- 存在某次求值
X,使得A对X携带依赖,且X对B携带依赖。
脚注说明
10)整个执行通常可以看作全部线程执行的某种交织。不过,某些原子操作允许出现不符合简单交织模型的执行,本小节后文会描述这些情况。
11)“携带依赖”关系是“先序于”关系的一个子集,并且同样严格限定在线程内部。
- 如果满足以下任一条件,则求值
A依赖排序先于(dependency-ordered before)12)求值B:
A在某个原子对象M上执行释放操作,并且在另一个线程中,B在M上执行消费操作,同时读到了由以A为首的释放序列中某个副作用写入的值;- 存在某次求值
X,使得A依赖排序先于X,且X向B携带依赖。
- 如果满足以下任一条件,则求值
A线程间先于发生(inter-thread happens before)求值B:
A与B同步;A依赖排序先于B;- 存在某次求值
X,并满足下列之一:A与X同步,且X先序于B;A先序于X,且X线程间先于发生于B;A线程间先于发生于X,且X线程间先于发生于B。
注 7:“线程间先于发生”关系,描述的是
先序于、同步于和依赖排序先于这些关系的任意串接,但有两个例外。第一个例外是:不允许一个串接以“依赖排序先于”后接“先序于”结束;原因是,参与“依赖排序先于”关系的消费操作,只对它所携带依赖的操作提供排序保证。之所以只限制这种串接的结尾,是因为之后若再出现释放操作,就会为先前的消费操作提供所需排序。第二个例外是:不允许一个串接完全由“先序于”构成;原因一是为了使“线程间先于发生”关系能在传递闭包下保持成立,原因二是本小节后面定义的“先于发生”关系本身已经覆盖了纯“先序于”的情况。如果
A先序于B,或A线程间先于发生于B,则称求值A先于发生(happens before)求值B。实现应保证:任何程序执行都不会在“先于发生”关系中形成环。注 8:否则,这样的环只能通过消费操作产生。
对对象
M而言,相对于一次对M的值计算B,某个副作用A若满足下列条件,则A是M的一个可见副作用:
A先于发生于B;- 不存在另一个对
M的副作用X,使得A先于发生于X且X先于发生于B。
对于非原子标量对象
M,由求值B确定出来的值,应当是可见副作用A所存储的那个值。注 9:如果对于某个非原子对象,无法确定哪个副作用是可见的,那么这就是一次数据竞争,行为未定义。
注 10:这说明对普通对象的操作不会被“可见地”重排。没有数据竞争时,这一点通常无法被观察到;但它保证了按此定义的数据竞争,在适当限制原子操作使用方式后,与简单交织(顺序一致)执行模型中的数据竞争是一致的。对于原子对象
M,由求值B确定出来的值,应当来自某个对M的副作用A所写入的值,并且B不先于发生于A。注 11:某次求值能够从哪些副作用中取得值,还会受到本小节其余规则的约束,尤其是后面定义的一致性要求的约束。
如果某个修改原子对象
M的操作A先于发生于另一个修改M的操作B,那么在M的修改顺序中,A必须早于B。注 12:这种要求称为“写-写一致性”(write-write coherence)。
如果对原子对象
M的一次值计算A先于发生于另一次值计算B,并且A从M上的某个副作用X取得其值,那么B计算得到的值,要么就是X存储的值,要么就是另一个副作用Y存储的值,其中Y在M的修改顺序中位于X之后。注 13:这种要求称为“读-读一致性”(read-read coherence)。
如果对原子对象
M的一次值计算A先于发生于对M的某次操作B,那么A必须从M上某个副作用X取得其值,并且X在M的修改顺序中先于B。注 14:这种要求称为“读-写一致性”(read-write coherence)。
如果对原子对象
M的某个副作用X先于发生于对M的某次值计算B,那么B取得的值,要么来自X,要么来自另一个在M的修改顺序中位于X之后的副作用Y。注 15:这种要求称为“写-读一致性”(write-read coherence)。
注 16:这实际上禁止编译器把针对同一原子对象的操作任意重排,即使两次操作都是relaxed加载也不行。这样做等于是把大多数硬件提供的“缓存一致性”保证,落实到了 C 的原子操作之上。
注 17:一次对原子对象的加载最终观察到哪个值,取决于“先于发生”关系;而“先于发生”关系本身又取决于各次原子加载观察到了什么值。标准的意图是:存在一种“原子加载与其所观察修改之间的关联方式”,连同适当选择的修改顺序和前文定义出来的“先于发生”关系,一起满足此处施加的全部约束。如果程序执行中包含两个发生在不同线程中的冲突动作,且其中至少一个不是原子操作,并且二者谁也不先于发生于对方,那么该程序执行就包含一次数据竞争(data race)。任何这样的数据竞争都会导致未定义行为。
注 18:可以证明,如果一个程序正确地使用简单互斥锁和
memory_order_seq_cst操作来防止全部数据竞争,并且不使用其他同步操作,那么它的行为就仿佛其各线程执行的操作只是简单交织在一起,每次对象值计算得到的都是这种交织中最后一次写入的值。这通常称为“顺序一致性”(sequential consistency)。不过,这一结论只适用于无数据竞争程序;而无数据竞争程序无法观察到大多数“不改变单线程语义”的程序变换。事实上,大多数单线程程序变换依然被允许,因为任何会因这种变换而表现不同的程序,在变换之前通常就已经具有未定义行为。注 19:一般来说,编译器变换若向某个可能被共享的内存位置引入一条抽象机器原本不会执行到的赋值,就会被本文档排除,因为这种赋值可能覆盖另一个线程所作的赋值,而在抽象机器的执行中,本不会发生数据竞争。这也包括那种在实现数据成员赋值时,顺带覆盖位于不同内存位置的相邻成员的做法。对于可能别名的原子加载,若对其进行重排,也通常会被排除,因为这可能破坏一致性要求。
注 20:那些引入对可能共享内存位置的推测性读取的变换,也可能不保留本文档所定义的程序语义,因为它们可能引入数据竞争。不过,在面向某个对数据竞争具有明确定义语义的具体机器的优化编译器语境下,这类变换通常是有效的;而对于某种“不容忍竞争”或提供硬件竞争检测的假想机器来说,它们则会是无效的。
脚注说明
12)“依赖排序先于”关系与“同步于”关系类似,但使用的是 release/consume,而不是 release/acquire。
前向引用:表达式(6.5.1)、类型限定符(6.7.4)、语句(6.8)、浮点环境 <fenv.h>(7.6)、signal 函数(7.14)、文件(7.23.3)。