码迷,mamicode.com
首页 > 其他好文 > 详细

【操作系统】堆和内存管理

时间:2015-07-24 09:15:38      阅读:151      评论:0      收藏:0      [点我收藏+]

标签:

什么是堆

  光有栈对于面向过程的程序设计还远远不够,因为栈上的数据在函数返回的时候就会被释放掉,所以无法将数据传递至函数外部。而全局变量没有办法动态地产生,只能在编译的时候定义,有很多情况下缺乏表现力。在这种情况下,堆是唯一的选择。

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

int main()
{
    char *p = (char*)malloc(1000);
    free (p)}

  上面的程序用malloc申请了1000个字节的空间后,程序可以自由地使用这1000个字节,直到程序用free函数释放它。

  进程的内存管理并没有交给操作系统内核管理,这样做性能较差,因为每次程序申请或者释放对空间都要进行系统调用。我们知道系统调用的性能开销是很大的,当程序对堆的操作比较频繁时,这样做的结果是会严重影响程序性能的。比较好的做法就是程序向操作系统申请一块适当大小的堆空间,然后由程序自己管理这块空间,而具体来讲,管理着堆空间分配往往是程序的运行库。

  运行库相当于向操作系统批发了一块较大的堆空间,然后“零售”给程序用。当全部“售完”或程序有大量的内存需求时,在根据实际需求向操作系统“进货”。当然运行库在向零售堆空间时,必须管理它批发来的堆空间,不能把同一块地址出售两次,导致地址的冲突。我们首先来了解运行库是怎么向操作系统批发内存的。我们以linux为例。

 

Linux进程堆管理

  进程地址空间中,除了可执行文件、共享库和栈之外,剩余的未分配的空间都可以被用来作为堆空间。Linux下的进程管理稍微有些复杂,因为它提供了两种堆分配方式,即两个系统调用:一个是brk()系统调用,另外一个是mmap()。brk()的C语言形式声明如下:

int brk(void* end_data_segment)

  brk()的作用实际上就是设置进程是数据段的结束地址,即它可以扩大或者缩小数据段(Linux下数据段和BSS合并在一起统称为数据段)。如果我们将数据段的结束地址向高地址移动,那么扩大的那部分空间就可以被我们使用,把这块空间拿来作为堆空间是最常见的做法之一。Giblic中海油一个函数叫做sbrk,它的功能与brk类似,只不过参数和返回值略有不同。sbrk以一个增量作为参数,即需要增加(负数为减少)的空间大小,返回值是增加(或减少)后数据段结束地址,这个函数实际上是对brk系统调用的包装,它通过brk()实现的。

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

void *mmap{void *start,  size_t length, int prot, int flags, int fd,off_t offset);

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

  glibc的malloc函数是这样处理用户空间请求的:对于小于128kb的请求来说,它会在现有的堆空间里面,按照堆分配算法为它分配一块空间并返回;对于大于128KB的请求来说,它会使用mmap()函数为它分配一块匿名空间,然后再这个匿名空间中为用户分配空间。当然我们直接使用mmap也可以轻而易举地实现malloc函数:

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

  由于mmap()函数与VirtualAlloc()类似,它们都是系统虚拟空间申请函数,它们申请的空间起始地址和大小都必须是系统页的大小的整数倍。

 

堆空间管理

  既然我们已经从操作系统“批发”一块内存用作堆,那么我们就要来思考如何管理这块大的内存。主要有三种方法,空闲链表和位图法以及对象池。

  

  空闲链表

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

  空闲链表是这样一种结构,在堆里的每一个空闲空间的开头(或结尾)有一个头,头结构里记录了上一个和下一个空闲块的地址,也就是说,所有的空闲块形成了一个链表。如下所示:

  技术分享

  在这样的结构下如何分配空间呢?首先在空闲链表查找足够容纳请求大小的一个空闲块,然后将这个块分为两部分,一部分为程序请求的空间,另一部分为剩余下来的空闲链表。下面将链表里对应原来空闲块的结构更新为新的剩下的空闲块,如果剩下的空闲块大小为0,则直接将这个结构从链表里删除。下图演示了用户请求一块和空闲块2恰好相等的内存空间后堆的状态。

  技术分享  

  位图

  位图的核心思想是将整个堆划分为大量的块,每个块的大小相同。当用户请求内存的时候,总是分配整数个块的空间给用户,第一个块我们称之为已分配区域的头,其余的称为已分配区域的主体。而我们可以使用一个整数数组来记录块的使用情况。由于每个块只有头/主体/空闲三种状态,因此仅仅需要两位即可表示一个块,因此称为位图。假设堆的大小为1MB,那么让一个块大小为128字节,那么总共就有1M/128=8k个块,可以用8k/(32/2)=512个int来存储。这有512个int的数组就是一个位图,其中每两位代表一个块。当用户请求300字节的内存时,堆分配给用户3个块,并将相应的位图的相应位置标记为头或躯体。

  下面是一个实例:

  技术分享

  这个堆分配了3片内存,分别有2/4/1个块,用虚线标出。其对应的位图将是:

  (HIGH) 11 00 00 10 10 10 11 00 00 00 00 00 00 00 10 11 (LOW)

  其中11表示H(头),10表示主体(Body),00表示空闲(Free)。

 

  对象池:

  以上介绍的堆管理方法是最为基本的两种,实际上在一些场合,被分配对象的大小是较为固定的几个值,这时候我们可以针对这样的特征设计一个更为高效的堆算法,称为对象池。

  对象池的思路很简单,如果每一次分配的空间大小都一样,那么就可以按照这个每次请求分配的大小作为一个单位,把整个堆空间划分为大量的小块,每次请求的时候只需要找到一个小块就可以了。

  对象池的管理方法可以采用空闲链表,也可以采用位图,与它们的区别仅仅在于它假定了每次请求的都是一个固定的大小,因此实现起来比较容易。由于每次总是只请求一个单位的内存,因此请求得到满足的速度非常快,无须查找一个足够大的空间。

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

  

参考资料:

  1. 《程序员的自我修养》--链接、装载与库

  2. 《现代操作系统》

  3. 《深入理解计算机系统》

  

【操作系统】堆和内存管理

标签:

原文地址:http://www.cnblogs.com/vincently/p/4671739.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!