(2020.11.07 Sat)
进程空间
应用程序位于整个架构的顶层,应用程序的进程会获得一块独立的内存空间,称为进程空间。C语言中变量的相关操作实际上就作用于进程空间。
函数调用
下面一段函数为例。
#include<stdio.h>
float PI = 3.14;
float power(float x, int n) {
//pass
}
float calculate_area(float t) {
float square;
result = power(square,2);
//pass
}
void main(void) {
float r = 1.2;
area = calculate_area(r);
//other operation
}
该函数中,main函数调用了一次calculate_area,c_a函数调用了一次power函数。被调用的函数是下级函数,下级函数运行时,上级函数只是暂停,比如调用c_a函数时,main函数暂停。等到被调函数返回,上级函数才恢复运行。C语言中,main函数总是最早被调用的,其位于最高级。后面被调用的函数位于下方,但下级函数先执行。除非下级函数运行结束,否则上级函数都处于暂停状态。即后来的函数都先获得执行。
跳转
这里我们研究一下经过编译后的指令式程序如何在底层实现C语言的函数调用。
程序代码在进程空间中的存储
在进程空间中,有一块名为程序段(TEXT)的区域。进程启动后,先把程序文件加载到进程空间的程序段中。程序文件是编译后的指令式程序,每个指令占据进程空间的一个存储单元,并可以通过内存地址来定位。计算机按照指令顺序,依次执行每条指令。
函数中包含了需要依次执行的多个指令。函数指令存储在一块连续的区域中,包含多条指令。函数可以通过内存地址来确定位置,函数的内存地址是函数第一条指令的内存地址。为实现程序复用,每个函数在程序段中只会存一次。
上面提到的main函数,运行时遇到了calculate_area函数指令,则不能继续执行面函数自身的下一句指令,需要跳转到calculate_area函数指令所在的位置。c_a函数运行完成后,也必须跳转回上级函数离开的位置。在跳转语句汇总,只需要说明指令的内存地址,就可以让进程在这个内存地址的位置进行执行。
程序开始前,程序加载如程序段,每个函数就有了确定的内存地址。当函数调用时,进城只需要跳转到函数指令所在的位置就可以了。但是被调用函数返回时就比较复杂,因为该函数可能被调用多次,因此其电泳的返回地址也是不同的。关键点:函数调用的某些信息是可变的。
为了记录函数调用中的可变信息,进程开辟了另一块名为栈(stack)的内存空间。
栈与情境切换
栈的组织方式和函数调用类似。函数调用发生时,上级函数暂停,下级函数开始工作。因此函数调用的逻辑顺序有个特点,总时最下级的函数处于激活状态。
main函数运行时,内存中就会有一个对应main函数的内存单元出现,用来记录main函数的可变信息,比如main函数返回时,应该跳转的地址。这个帧就是整个栈的起点。伺候,每次有新的函数调用发生时,栈就会向下增加一个帧,对应这一次的函数调用。在创建这个帧时,进程就会记下离开上级函数前的地址,也就是新函数调用的返回地址,所有的帧就组成了一个栈。借助栈的存储能力,函数返回就不再是一个问题了。
再比如前面的main函数。当帧最下方的函数完成时,栈会弹出最下方的帧,取出其中的返回地址,并删除帧的内存空间。进程跳转到返回地址继续运行,原本暂停了的上级函数继续执行。与此同时,暴露在栈最下方的帧恰好对应了恢复激活状态的上级函数。随着函数的调用和返回,栈也不断变化,增加一帧或减少一。等到main函数也返回时,栈最高级的帧也被删除,整个程序运行结束。
栈的变化过程和函数调用的变化过程很类似:总时最下方、对应当前的帧处于活跃状态。
本地变量local variable
所谓本地变量,即函数内部的变量,一个本地变量只能在函数内部声明。
栈除了保存返回地址,还能存储本地变量,和函数的参数。栈中存储的其他数据也跟着函数调用诞生和消失。当函数被调用时,该函数的本地变量才在对应的帧中出现,调用结束,帧被清空,其中存储的本地变量也会被清空。
本地变量随着函数调用诞生,又随着函数返回消失。如果函数调用了下级函数,上级函数的本地变量依然保持在帧中。C语言只允许函数调用当前帧中的内容。因此,激活函数只能操作当前帧中的内容,没法读取或写入上级帧的本地变量。本地变量也完全封闭到了函数内部。定义本地变量的函数内部,称作本地变量的作用域。
函数的参数,用于存储函数的输入。参数也存活于帧中,因此参数的作用域和本地变量完全相同。
全局变量global variable和存放动态变量的堆heap
进程空间中的全局数据(global data)部分用于存放全局变量(global variable)。
堆(heap)用于存放动态变量(dynamic variable)。动态变量可以被所有函数看到。与全局变量不同的,动态变量不能一直确定,可以在进程中产生和消失;但动态变量的作用域同样是全局。进程创建动态变量时,堆的区域就会增长,占据更多的内存空间。堆增长的部分就是动态变量的空间。
堆和栈是相互独立的区域,堆的空间不随着函数调用自动增长或清空。在堆的支持下,动态变量的作用域同样是全局。任何一个函数内部都可以通过动态变量的地址来访问动态变量中的数据。通过malloc系统调用来在堆上创建动态变量。这个系统调用返回的是动态变量的内存地址。函数之间可通过参数或返回值来交换该地址,从而跨函数的共享数据。本地变量无法实现上述功能。
不再需要一个动态变量时,可以通过free系统调用来释放动态变量占据的内存空间。C语言中常见的错误是内存泄露(memory leakage),就是指没释放不再使用的动态变量,导致进程空间的可用内存不足。
Reference
1 Vamei, 周梓昕著,树莓派开始玩转Linux,中国工信出版集团,电子工业出版社
网友评论