首先看看进程的地址空间是如何分布的
现代的应用程序都运行在一个内存空间里,在32的系统里,这个内存空间拥有4GB的寻址能力,Linux默认情况下将高地址的1GB空间分配给内核,用户使用剩下的3GB的内存空间称为“用户空间”,一般来讲,应用程序使用的内存空间有如下的“默认权限”:
- 栈:栈用于维护函数系统调用的上下文,离开了栈函数调用就没法实现。
- 堆:堆是用来容纳应用程序动态分配的内存区域,当程序使用malloc或new分配内存时,得到的内存来自堆里。
- 可执行文件映像:这里存储着可执行文件在内存里的映像,由装载器在装载时将可执行文件的内存读取或映射在这里。
- 保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称,例如,大多数操作系统里,极小的地址通常都是不允许访问的,如NULL。通常C语言将无效的指针赋值为0也是出于这个考虑。
栈
栈在程序运行中具有举足轻重的位置,栈保存了一个函数调用所需要的维护信息,这常常被称为堆栈帧或活动记录。堆栈帧一般包括如下几方面内容:
- 函数的返回地址和参数
- 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量
- 保存的上下文:包括在函数调用前后需要保持不变的寄存器。
在i386中,一个函数的活动记录用ebp和esp这两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也指向了当前函数的活动记录的顶部。而相对的,ebp寄存器指向了函数活动记录的一个固定位置,ebp寄存器又被称为帧指针.ebp所直接指向的数据是调用该函数前ebp的值,这样在函数返回的时候,ebp可以通过读取这个值恢复到调用前的值。一个i386下的函数总是这样调用的:
- 把所有或一部分参数压人栈中,如果没有其他参数入栈,那么使用某些特定的寄存器传值。
- 把当前指令的下一条指令的地址压入栈中。
- 跳转到函数体执行。
其中第二步和第三步由call指令一起执行。跳转到函数体之后即开始执行函数,而i386函数体的“标准开头”是这样的:
- push ebp: 把ebp压入栈中(简称old ebp)
- mov ebp,esp: ebp = esp (这时ebp 指向栈顶,而此时栈顶就是old ebp)。
- 【可选】sub esp ,XXX:在栈上分配XXX字节的临时空间
- 【可选】push XXX: 保存名为XXX的寄存器(可重复多个)。
把ebp压入栈中,是为了在函数返回是便于回复以前的ebp值(主函数调用函数,函数在接着调用其他函数)。
- 【可选】pop XXX:如有必要,恢复保存过得寄存器的值
- mov esp,ebp: 恢复ESP同时回收局部变量空间
- pop ebp:从栈中恢复保存的ebp的值
- ret: 从栈中取得返回地址,并跳转到该位置。
堆
光有栈对于面向过程的程序设计还远远不够,因为栈上的数据在函数返回的时候就会被释放掉,所以无法将数据传递到函数外部。而全局变量没有办法动态的产生,只能在编译的时候定义,有很多情况下缺乏表现力。在这种情况下,**堆(heap)**是唯一的选择。
堆是一块巨大的空间,常常占据整个虚拟空间的绝大部分。在这片空间里,程序可以请求一块连续内存,并自由的使用,这块内存在程序主动放弃之前一直保持有效。
Q:我在有些书里看到堆总是向上增长,是这样吗?
A:不是,有些较老的书里面针对当时的系统做出过这样的断言,这在当时可能是正确的。因为当时的系统多是类unix系统,它们使用类似brk的方法来分配空间,而brk的增长方向是向上的。但随着windows的出现,这个规律被打破了。在Windows里,大部分堆使用HeapCreat产生,而HeapCreat系列函数却完全不按照向上增长的这个规律。
Q:调用malloc会不会最后调用到API或系统?
A: 我们知道malloc申请内存是通过brk或者mmap系统调用实现的,一种可行的方法是,程序每次使用malloc或free都进行系统调用,但是这样做的性能开销是很差的,因为系统调用的性能开销是很大的,当程序对堆的操作比较频繁时,这样做的结果是会严重影响程序的性能的。比较好的做法是程序向操作系统申请一块适当大小的堆空间,然后由程序自己管理这块空间。所以这个取决于当前进程向操作系统批发的那些空间还够不够用,如果够用了,那么它可以直接在仓库里面取出来卖给用户;如果不够用了,它就只能通过系统调用或者API向操作系统再进一批货了。
Q:malloc申请的内存,进程结束以后还会不会存在?
A:不会存在。因为当进程结束以后,所有与操作系统相关的资源,包括进程的地址空间,物理内存,打开的文件,网络链接等都被操作系统关闭或回收,所以无论malloc申请了多少内存,进程结束以后都不存在了。
Q:malloc的空间是不是连续的?
A:在分析这个问题之前,我们首先要分清楚“空间”这个词所指的意思。如果“空间”是指虚拟空间的话,那么答案是连续的,即每一次malloc分配后返回的空间都可以看做是一块连续的地址;如果空间是指“物理空间”的话,则答案是不一定连续,因为一块连续的虚拟地址空间有可能是若干个不连续的物理页拼凑而成。