📅  最后修改于: 2021-01-18 05:28:44             🧑  作者: Mango
程序作为源代码仅仅是文本(代码,语句等)的集合,并且要使其生动起来,就需要在目标计算机上执行操作。程序需要内存资源才能执行指令。程序包含过程名称,标识符等,它们需要在运行时与实际内存位置进行映射。
在运行时,我们是指正在执行的程序。运行时环境是目标计算机的状态,可以包括软件库,环境变量等,以向系统中运行的进程提供服务。
运行时支持系统是一个程序包,主要由可执行程序本身生成,并有助于流程与运行时环境之间的流程通信。它在执行程序时负责内存的分配和取消分配。
程序是一系列指令,这些指令组合成许多过程。过程中的指令按顺序执行。过程具有开始和结束定界符,其中的所有内容都称为过程的主体。过程标识符和其中的有限指令序列构成过程的主体。
过程的执行称为其激活。激活记录包含调用过程所需的所有必要信息。激活记录可能包含以下单位(取决于所使用的源语言)。
Temporaries | Stores temporary and intermediate values of an expression. |
Local Data | Stores local data of the called procedure. |
Machine Status | Stores machine status such as Registers, Program Counter etc., before the procedure is called. |
Control Link | Stores the address of activation record of the caller procedure. |
Access Link | Stores the information of data which is outside the local scope. |
Actual Parameters | Stores actual parameters, i.e., parameters which are used to send input to the called procedure. |
Return Value | Stores return values. |
每当执行过程时,其激活记录都存储在堆栈中,也称为控制堆栈。当一个过程调用另一个过程时,调用者的执行将被挂起,直到被调用过程完成执行为止。此时,被调用过程的激活记录存储在堆栈中。
我们假设程序控制以顺序方式流动,并且在调用过程时,其控制权将转移到被调用过程。执行被调用过程时,它将控制权返回给调用者。这种类型的控制流程使以树的形式(称为激活树)的形式表示一系列激活更为容易。
为了理解这个概念,我们以一段代码为例:
. . .
printf(“Enter Your Name: “);
scanf(“%s”, username);
show_data(username);
printf(“Press any key to continue…”);
. . .
int show_data(char *user)
{
printf(“Your name is %s”, username);
return 0;
}
. . .
下面是给定代码的激活树。
现在我们了解到,过程是以深度优先的方式执行的,因此堆栈分配是过程激活的最佳存储形式。
运行时环境管理以下实体的运行时内存需求:
代码:被称为程序的文本部分,该部分在运行时不会更改。它的内存要求在编译时是已知的。
过程:它们的文本部分是静态的,但是以随机方式调用。因此,堆栈存储用于管理过程调用和激活。
变量:变量只有在运行时才知道,除非它们是全局或常量。堆内存分配方案用于在运行时管理变量的内存分配和取消分配。
在这种分配方案中,编译数据绑定到内存中的固定位置,并且在程序执行时它不会更改。由于内存需求和存储位置是事先已知的,因此不需要用于内存分配和取消分配的运行时支持包。
过程调用及其激活通过堆栈内存分配进行管理。它以后进先出(LIFO)方法工作,此分配策略对于递归过程调用非常有用。
仅在运行时才分配和取消分配过程本地变量。堆分配用于为变量动态分配内存,并在不再需要变量时将其收回。
除了静态分配的内存区域之外,堆栈和堆内存都可以动态且意外地增长和收缩。因此,它们不能在系统中提供固定数量的内存。
如上图所示,代码的文本部分分配有固定的内存量。堆栈和堆内存的分配是分配给程序的总内存的最大值。两者都缩小并且彼此对抗。
程序之间的通信介质称为参数传递。来自调用过程的变量的值通过某种机制转移到被调用过程。在继续之前,首先要了解一些与程序中的值有关的基本术语。
表达式的值称为其r值。如果单个变量中包含的值出现在赋值运算符的右侧,那么它也将成为r值。 r值可以始终分配给其他变量。
存储表达式的内存(地址)位置称为该表达式的I值。它总是出现在赋值运算符的左侧。
例如:
day = 1;
week = day * 7;
month = 1;
year = month * 12;
从此示例中,我们了解到常数值(例如1、7、12)和变量(例如日,周,月和年)都具有r值。只有变量具有l值,因为它们也代表分配给它们的内存位置。
例如:
7 = x + y;
是一个L值错误,因为常数7不代表任何存储位置。
采用调用程序过程传递的信息的变量称为形式参数。这些变量在被调用函数的定义中声明。
将其值或地址传递给被调用过程的变量称为实际参数。这些变量在函数调用中作为参数指定。
例:
fun_one()
{
int actual_parameter = 10;
call fun_two(int actual_parameter);
}
fun_two(int formal_parameter)
{
print formal_parameter;
}
形式参数保存实际参数的信息,具体取决于所使用的参数传递技术。它可以是值或地址。
在按值传递机制中,调用过程将传递实际参数的r值,然后编译器将其放入被调用过程的激活记录中。然后,形式参数保存调用过程传递的值。如果形式参数保留的值发生更改,则对实际参数应该没有影响。
在按引用传递机制中,实际参数的l值被复制到被调用过程的激活记录中。这样,被调用的过程现在具有实际参数的地址(存储位置),而形式参数则指向相同的存储位置。因此,如果更改了形式参数所指向的值,则应该看到对实际参数的影响,因为它们也应指向相同的值。
该参数传递机制的工作原理与“传递引用”相似,不同之处在于在调用过程结束时对实际参数进行更改。在调用函数时,实际参数的值将被复制到被调用过程的激活记录中。形式参数(如果被操纵)对实际参数没有实时影响(因为传递了l值),但是当被调用过程结束时,形式参数的l值将被复制到实际参数的l值。
例:
int y;
calling_procedure()
{
y = 10;
copy_restore(y); //l-value of y is passed
printf y; //prints 99
}
copy_restore(int x)
{
x = 99; // y still has value 10 (unaffected)
y = 0; // y is now 0
}
该函数结束时,形式参数x的l值将复制到实际参数y。即使在过程结束之前更改了y的值,也将x的l值复制到y的l值,使其行为类似于按引用调用。
诸如Algol之类的语言提供了一种新型的参数传递机制,其作用类似于C语言中的预处理器。在按名称传递机制中,被调用过程的名称由其实际主体替换。传递名称以文本方式替换过程调用中的参数表达式,以代替过程主体中的相应参数,以便它现在可以处理实际参数,就像传递引用一样。