程序就是一堆代码组合起来实现某种功能,进程可以说是跑起来的程序。
程序经过编译之后会生成一个.exe可执行文件
#include<stdio.h>
#include<string.h>
int global = 1; //全局变量
int func(){ //函数1
int var1 = 1; //局部变量
printf("Func: var1 = %d\n",var1);
printf("global = %d\n",global);
return var1 + global; //返回值
}
int main(void){
int var1 = 1;
int var2 = 2;
int var3 = func();
printf("main: var3 = %d\n",var3);
}
<font size=2>程序中有两种变量(局部变量和全局变量),声明在函数体外部
的为全局变量,全局变量作用在整个程序的存活周期里,声明在函数里面的是局部
变量,局部变量的作用域是它存在的哪个函数里面,当函数return后,变量就会
被释放。</font>
重点是程序的运行过程:
当成需文件运行为进程的时候是需要内存的,但是进程从内存中获取的进程空间是什么样子的呢?
Text区域用来储存指令(instruction),说明每一步的操作。Global Data用于存放全局变量,栈(Stack)用于存放局部
变量,堆(heap)用于存放动态变量 (dynamic variable. 程序利用malloc系统调用,直接从内存中为dynamic variable
开辟空间)。Text和Global data在进程一开始的时候就确定了,并在整个进程中保持固定大小。
栈(Stack)以帧(stack frame)为单位。当程序调用函数的时候,比如main()函数中调用func()函数,stack会向下增长一帧。帧中存储该函数的参数和局部变量,以及该函数的返回地址(return address)。此时,计算机将控制权从main()转移到func(),func()函数处于激活(active)状态。位于栈最下方的帧,和全局变量一起,构成了当前的环境(context)。激活函数可以从环境中调用需要的变量。典型的编程语言都只允许你使用位于stack最下方的帧 ,而不允许你调用其它的帧 (这也符合stack结构“先进后出”的特征。但也有一些语言允许你调用栈的其它部分,相当于允许你在运行func()函数的时候调用main()中声明的局部变量,比如Pascal)。当函数又进一步调用另一个函数的时候,一个新的帧会继续增加到栈的下方,控制权转移到新的函数中。当激活函数返回的时候,会从栈中弹出(pop,读取并从栈中删除)该帧,并根据帧中记录的返回地址,将控制权交给返回地址所指向的指令(比如从func()函数中返回,继续执行main()中赋值给var3的操作)。
在进程运行的过程中,通过调用和返回函数,控制权不断在函数间转移。进程可以在调用函数的时候,原函数的帧中保存有在我们离开时的状态,并为新的函数开辟所需的帧空间。在调用函数返回时,该函数的帧所占据的空间随着帧的弹出而清空。进程再次回到原函数的帧中保存的状态,并根据返回地址所指向的指令继续执行。上面过程不断继续,栈不断增长或减小,直到main()返回的时候,栈完全清空,进程结束。
当程序中使用malloc的时候,堆(heap)会向上增长,其增长的部分就成为malloc从内存中分配的空间。malloc开辟的空间会一直存在,
直到我们用free系统调用来释放,或者进程结束。一个经典的错误是**内存泄漏**(memory leakage), 就是指我们没有释放不再使用
的堆空间,导致堆不断增长,而内存可用空间不断减少。
栈和堆的大小则会随着进程的运行增大或者变小。当栈和堆增长到两者相遇时候,也就是内存空间图中的蓝色区域(unused area)完全消失的时候,再无可用内存。进程会出现栈溢出(stack overflow)的错误,导致进程终止。在现代计算机中,内核一般会为进程分配足够多的蓝色区域,如果清理及时,栈溢出很容易避免。即便如此,内存负荷过大,依然可能出现栈溢出的情况。我们就需要增加物理内存了。