系统中的堆和栈

在数据结构中栈具有先进后出的(First in Last Out FIFO)的特性,而在计算机系统中,栈是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈中弹出。压栈的操作使得栈增大,而弹出操作使得栈变小。在一般的操作系统中。栈是向下增长的。在i386机器上,栈顶由称为esp的寄存器进行定位。压栈的操作使得栈顶的地址减小,弹出的操作使得栈顶的地址增大。

此处栈底的地址是0xbfffff,而esp寄存器标明了栈顶,地址为0xbffffff4.在栈上压入数据会导致esp减小,弹出数据使得esp增大。相反,直接减小esp的值也等效于在栈上开辟空间,直接增大esp的值等下鱼在栈上回收空间。

 在程序的运行中,栈保存了一个函数调用所需要的维护信息,通常称为堆栈帧(Stack Frame)或活动记录(Activate Record)堆栈帧一般包括如下几方面内容

:*函数的返回值和参数

*临时变量--包括函数的非静态局部变量以及编译器自动生成的其他临时变量

*保存的上下文--包括函数调用前后需要保持不变的寄存器

在i386中,一个函数的活动记录用ebp和esp这两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也就指向了当前函数的活动记录的顶部。而相对的,ebp寄存器执行了函数活动记录的一个固定位置,ebp寄存器又被称为帧指针。

在参数之后的数据(包括参数)既是当前函数的活动记录,ebp固定在图中所示的位置,不随这个函数的执行而变化,相反的esp始终指向栈顶,因此随着函数的执行,esp会不断变化。固定不变的ebp可以用来定位感受活动记录中的各个数据。在ebp之前首先是这个函数的返回值,他的地址是ebp-4,再往前是压入栈中的参数,他们的地址分别是ebp-8、ebp-12等,视参数数量和大小而定。ebp所直接指向的数据是调用该函数前ebp的值,这样函数在返回的时候,ebp可以通过读取这个值恢复到调用前的值。采用这样的结构原因在于函数调用如下(i386):

*把所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特地的寄存器传递

*把当前指令的下一条指令的地址压入栈中

*跳转到函数体执行

其中第2、3步是由指令call一起执行。跳转到函数体之后开始执行函数。而i386函数体的标准开头是这样的(特别情况下也有可能不一样)

*push ebp--把epb压入栈中(称old ebp)

*move 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--从栈中取得返回地址,并跳转到该位置

-----------------------------------------------------------------------------------------------------------------------------------堆---------------------------------------------------------------------------------------------------------------------------------

堆是一块巨大的内存空间,常常占据整个虚拟空间的绝大部分。在这片空间里,程序可以请求一块连续内存,并自由的使用,这块内存在程序主动放弃之前都会一直保持有效。

malloc实现的两种方法:1)把进程的内存管理交给操作系统内核去做,由于内核管理着进程的地址空间,当其提供一个系统调用,使程序可以通过这个系统调用申请内存。该方法理论可行,但是性能比较差(系统调用时性能开销很大,频繁调用的话会严重影响程序性能)。2)程序向操作系统申请一块适当大小的堆空间,然后程序自己管理这块空间,具体来说,管理着堆空间分配的往往是程序的运行库。

 在进程的地址空间中,除了可执行文件、共享库和栈之外。剩余的未分配的空间都可以用来作为堆空间。

在Linux系统下,其提供两种堆空间分配方式,即两个系统调用:一个是brk()系统调用,另一个是mmap()。brk()的C语言形式声明如下:

int brk(void * end_data_segment)

brk()的作用实际上是设置进程数据段的结束地址,即它可以扩大或者缩小数据段(Linux下数据段和BSS合并在一起统称为数据段)。如果我们将数据段的结束地址向高地址移动,那么扩大的那部分空间就可以被我们使用,最常见的做法就是将这个空间视为堆空间。

mmap()的作业和window系统下的VituralAlloc很相似,它的作用就是向操作系统申请一段虚拟地址空间,当然这块虚拟地址空间可以映射到某个文件(这也是这个系统调用的最初作用),当它不将地址空间映射到某个文件时,我们又将这块空间称为匿名空间(Anonymous),匿名空间就可以拿来作为堆空间。其声明如下

void * mmap(

void *start,

size_t length,

int prot,

int flags,

int fd.

off_t offset);

mmap的前两个参数分别用于指定需要申请的空间的起始地址和长度,如果起始地址设置为0,那么Linux系统会自动挑选合适的其实地址。prot/flags这两个参数分别用于设置申请空间的权限(可读、可写、可执行)以及映射类型(文件映射、匿名空间等),最后两个参数是用于文件映射时指定文件描述符和文件偏移的。

对于malloc函数,处理用户空间请求时:对于128kb的请求来说,其会在现有的堆空间里面,按照堆分配算法为其分配一块空间并返回:对于大于128kb的请求来说,它会使用mmap()函数为它分配一块匿名空间,然后在这个匿名空间中为用户分配空间。当然也可以直接使用mmap()函数来实现malloc函数

void *malloc(size_t nbytes){
void * res=mmap(0,nbytes,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONMOUS,0,0);
if(ret==MAP_FAILED)
 {return 0;}
return ret;
 
}

在malloc中申请的内存,在进程结束会便不会存在。因为当进程结束后,所有与进程相关的资源,包括进程的地址空间、物理内存、打开的文件、网络链接等都被操作系统关闭或者收回、所以无论malloc申请了多少内存,进程结束后都不会存在

malloc申请的空间:如果是指虚拟空间的话,其为连续的,即每次malloc分配后返回的空间都可以看作是一块连续的地址;如果空间是指“物理空间”则并不一定联系,对于一块连续的虚拟空间来说,其可能是由若干个不连续的物理页拼凑而成。

堆分配算法:

1)空闲链表--其方法是把堆中各个空闲的块按照链表的方式连接起来,当用户请求一块空间时,可以遍历整个列表,直到找到合适大小的块并且将它拆分。当用户释放空间时将它合并到空闲链表中。

2)位图(Bitmap)--核心思想是将整个堆划分为大量的块(block),每个块大小相同。当用户请求内存的时候,总是分配整数个块的空间给用户,第一个块我们称为已分配区域的头(Head),其余的称为已分配区域的主体(Body).而我们可以使用一个整数数组来记录块的使用情况,由于每个块只有头/主体/空闲三种状态,因而仅仅需要两位即可表示一个块,因此称为位图。位图的实现具有以下优点:(1)速度快--由于整个堆的空闲信息存储在一个数组内,因此访问该数组时cache容易命中(2)稳定性好--为了避免用户越界读写破坏数据,我们只需简单的备份一下位图即可。而且即使部分数据被破坏,也不会导致整个堆无法工作(3)块不需要额外信息,易于管理;其缺点有:(1)分配时容易产生碎片(2)如果堆很大,或者设定的块很小(可以减少碎片),那么位图会很大,可能失去cache命中率高的优势,并且浪费一定的空间。此种情形出现时可以使用多级的位图。

(3)对象池--其思路为:如果每一次分配的空间大小都一样,那么可以按照这个每次请求分配的大小作为一个单位,把整个堆空间划分为大量的小块,每次请求的时候只需要找到一个小块就可以了。对象池的管理方法可以采用空闲链表,也可以采用位图,与他们的区别仅仅在于它假定每次请求的都是一个固定的大小,因此实现起来很容易,由于每次总是只请求一个单位的内存,因此请求得到满足的速度非常快,无需查找足够大的空间。

  在现实应用中,堆分配算法往往是采取多种算法复合而成的。比如对与glibc来说,它对小于64字节的空间申请是采用类似于对象池的方法;而对于大于512字节的空间申请采用的是最佳适配算法,对于大于64字节而小于512字节的,会根据情况采用上述方法中的折衷策略;对于大于128kb的申请,它会使用mmap机制直接向操作系统申请空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值