内存分配、释放以及内存分配系统调用

来源:互联网 发布:玄空飞星软件下载 编辑:程序博客网 时间:2024/06/14 16:08

在做C/C++编程的时候,最棘手处理的大概就是内存操作了。

前一段时间查看资料得知内存管理的算法大致有两种:内存分配算法、内存页面置换算法,对这些算法虽然可能不需要实现,但是需要了解其中的概念原理,以备不时之需。

内存分配的算法主要有5种:

第一种是固定内存分配,也叫位图分配算法:

            固定内存分配,从其字面意思就可以看出来,分配的内存是固定的。比如1GB的内存可用于分配,每个分配的内存块的大小为1M,那么整个就有1024个内存块可用于分配,最多可分配1024次。然后每个分配单元对应于位图中的一位,0表示空闲,1表示占用。


第二种是使用链表分配内存:

          固定内存分配的方法虽然分配简单,易于管理内存,但是它造成了很多不必要的浪费,比如需要1KB的空间还是给它分配1M的空间,其他都没有使用,这样浪费率达到了99.9%。为了解决这个问题,需要一种新的方式,就像在学数据结构的时候,数组存储的时候太大浪费,太小无法满足,寻找一种新的存储方法就是使用链表进行存储。这里也借鉴是这种的思想,需要维护一个已分配内存段和空闲内存段的链表,每个节点存储了空闲标志区、进程指示标志区、起始地址、空间大小、指向下一个地址的指针。然后动态进行存储区间的分区与释放回收合并,以下是链表分配内存的四种方法:

        1、首次自适应算法。这个算法每次分配的时候均从低位地址开始查询然后分配地址,这个算法的好处是高位地址被保留了下来,为执行大的内存分配创造条件,其劣势是每次都从低位地址开始往后查找,低位地址不停被划分,留下很多难以利用的小空间。

        2、循环首次适应算法。这个算法是从上次分配的地址开始分配内存。这种方式分配内存,内存空间更加均匀,可能会缺失大的空闲分区。

        3、最佳适应算法。这个算法会对空闲的分区进行一个排序,从中找出最优的空间分配给程序。这个算法每次分配完内存后剩余空间一定是最小的,这样的话就留下了许多难以被利用的碎片,而且每次分配内存前都得进行重新排序,这样也带来了一定的开销。

        4、最差适应算法。该算法对空间按递减进行排列,形成空间链表,分配内存的时候从第一个空闲分区开始分配,如果第一分区不能满足,则不能满足分配,第一个分区载入程序后还剩下很多空间可以继续为后面的程序分配。这样分配内存的方式可以明显的减少内存碎片,但是保留大空间的可能性减小。


第三种是伙伴算法:

        链表的方法节省了很多空间,但是从链表分配内存的过程中可以看出,链表分配内存不可避免造成内存碎片。碎片是不可能消失的,我们能做的就是减少碎片,可以结合固定分配与链表分配,为此提出了伙伴算法,Linux内核管理中使用的就是这个算法。这个算法的基本思路就是将内存分成1,2,,4,8,16,32等等以2为幂指数的一系列的内存块,相同大小的内存块构成一个链表,然后再分配内存的时候,寻找大小最接近所要分配内存的2的幂的链表,从中找出一块空闲的区域分配给程序。释放内存的时候也是把相邻的空闲块合并为一块并插入上一级的链表中。

        为了说明这个算法的过程,我们这里做一个假设,设分割好的链表中每个是按照内存的页面来计算的,假设需要8个页面存储数据,到页面为8的链表中查找,如果有空余的块,则将此空余的分配给程序,如果没有空余的块则向上一级查找也就是16个页面的链表,看有没有空余的内存块,如果有则把一般用来存储数据,一半插入8的页面链表中,如果再16个页面链表中还是没有空闲的页面,则继续向上查找,在32个页面的链表中找到空闲的块,其中开始的8个用来存储数据,剩下24个,16个页面插入16个页面的链表中,剩下8个插入到8个页面的链表中,如果32个页面中还没找到则继续向上寻找,直到遍历完整个内存,如果已经没有空间了则返回错误信息。


第四种算法是基于内存池的伙伴算法:

        伙伴算法可以解决内存碎片的一些问题,但是伙伴算法在执行的过程中频繁地进行分配与合并。这在一定程度上会影响内存分配效率,也会出现一定的碎片。为此我们可以这样做,在使用完内存后先不进行释放,只是标记当前的内存已经不再使用了,这样的话我们就可以建立起一个内存池,当内存池里所有的块都不在使用的时候,然后对内存池中的块进行合并释放。


第五种算法是工作集算法:

      操作系统运行起来后,内存中分配的大小、配置比例关系都是相对固定的,变化不是很大。如果把这些数据记录下来,系统启动后预先分配好这些内存的话,可以极大地提高系统的启动速度,这些参数称之为工作集。这些工作集参数更多地是一种经验值,需要我们综合多方面的因素进行分析,反复比较才能获得很好的结果。


下面看看内存页面置换算法:

         实际的操作系统中,我们分配的内存也只是虚拟的内存,也就是给的变量的地址是逻辑上的地址,需要通过MMU映射到物理内存上去,这里的映射算法叫做内存页面置换算法。由于物理内存有限,一个进程所有的逻辑页面并不是全都会被映射到实际的物理页面上去,只是为进程分配一定数量的物理页面,既然物理页面不够用,那么就要进行页面的置换了,一段时间把需要进入物理内存的逻辑页面放入内存,不需要在内存中的页面置换出来。常用到的算法有:

第一种为最优页面置换算法(OPT):

         这只是一种理论上的算法,主要的思想是把将来使用次数最少的页面置换出去。

第二种为最近未使用页面置换算法(NUR):

        为页面设置一个访问位,当某个页面被访问时,访问位设置为“1”,否则设置为“0”,当需要置换一页的时候,从被置为“0”的页面中选择出一个页面进行置换,操作系统会周期性的对方位为清零。

第三种为最久未使用算法(LRU):

        当需要置换某个页面的时候,选择离当时时间最近的一段时间内最久没有使用过的页面置换。该算法假设某个页面被访问了,这个页面可能还需要被访问,或者某个页面很长一段时间都没有被访问,那么在最近的一段时间内也不会被访问。

第四种算法为先进先出(FIFO):

         和队列一个样,先进入内存的页面最先被置换出去。我们需要在页表中记录进入的次序,将各个已经分配的页面按照分配的时间的先后链接起来,组成FIFO队列。这种算法实现起来快,但是遇到常用页面经常会碰到缺页的错误。

第五种算法为第二次页面置换算法(SCR):

        为了避免FIFO可能把经常使用的页面给换出去,选择置换页面的时候,需要先检查它的访问位,如果是0,就立刻置换掉这个页面;如果访问位是1,则给它第二次机会选择下一个FIFO页面。当一个页面第二次被置换时,这期间如果被访问过了,则访问位置1,继续淘汰下一个,如果这期间没有被访问,则访问位就清零,如果一个页面经常使用则访问位就一直为1,不会被置换出去。

第六种算法为时钟页面置换算法:

        这个也是对FIFO算法的改进,我们把所有的页面保存在类似于钟表面的环形链表中,指针指向最先进入队列的页面。当需要置换页面的时候,首先检查这个页面,如果访问位是0,则立刻置换掉这个页面,指针指向下一个页面,如果是访问位是1,则清零再移动到下一个位置,重复这个过程直到找到访问位为零的页面位置。为何叫时钟页面,从下面的图中可以直观地看出来,页面加一个指针就是很像一个钟表。



以上就是内存管理算法。下面看下内存分配的一些函数。

首先看下C语言中几个内存分配的函数:

首先是用的最多的malloc函数,其函数的原型如下:

void *malloc(size_t size);

调用这个函数会size字节的内存空间,如果内存可以满足这个需求就返回指向分配的内存空间的首地址。

void *calloc(size_t num_elements,size_t element_size);
calloc也malloc的区别就是需要指定元素的数量,以及每个元素的字节数,而且在内存空间申请之后,初始化这段内存空间为0。

void *realloc(void *ptr,size_t new_size);
这个函数用来修改一块已经申请好的内存的大小,如果扩展内存则新扩展的部分添加到原来内存块儿的后面,如果缩小内存区域,则该内存块儿的后面部分被删除掉。如果函数的第一个参数为NULL,则这个函数和malloc的作用一样。

上面提到的几个函数分配内存的时候都是在堆上进行分配的,使用完内存需要手动进行释放回收。利用下面的函数:

void  free(void *ptr);
传值给free函数的是NULL时候,不做任何处理,传值malloc、calloc、realloc函数的返回值,则释放这些空间。

以上分配完内存需要释放的操作都是在堆上面进行的,下面看一个函数,也是分配内存,不过是在栈上进行的,所以它使用完的内存不需要进行手动释放,会自动释放:

void *_cdecl alloca(size_t size);
这个函数的调用方式与malloc相同,当时与malloc不同的是它在栈上进行内存的分配,使用后无需为释放空间而烦恼。但是有些机器不能增加栈的长度,不支持alloca函数。

上面这些内存分配函数是通过调用底层的系统调用实现的,这些函数主要有以下几种:

两个关于数据段函数:

int brk(void *end_data_segment);void *sbrk(ptrdiff_t increament);
第一个brk依据参数end_data_segment所指的数值设成进程数据段结束的地址。成功返回0,失败为-1.第二个sbrk增加程序的数据段的空间,增加的大小由increasment而定,成功返回的值是指向新的数据段末尾的指针,失败返回-1,这个函数其实是对brk()系统调用的封装,通过brk()实现。

还有两个关于内存映射的函数,一个是内存映射,一个解除内存映射:

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);int munmap(void *start, size_t length);
这里我们看下mmap函数,*start用于指定需要申请空间的起始地址,length为申请的长度,如果起始的地址设为0,则Linux系统会自动挑选合适的起始地址,prot用来设置空间的读写权限位,设置为可读、可写、可执行,flag用来设置映射的类型(文件映射、匿名空间等),fd与offset用来设置文件映射时的文件句柄以及文件偏移。

了解了这些系统调用,我们可以利用这几个系统调用来实现malloc函数,为了说明进程的分配状态还是看下进程的结构:


进行内存分配的时候,默认的情况下,待分配的内存大小如果小于128KB(这个阈值可通过M_MMAP_THRESHOLD设置),则利用brk系统调用进行分配,我们要做的就是在堆区域往上扩展,如图中所示分配100k与80k的空间时,堆区指针heap_data往高位地址移动,如果分配的内存大小大于128KB,则直接从栈与堆之间的空闲区域分配一块区间给程序,如图中分配280K的空间时,这里利用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;}
释放内存的时候,如果是通过brk()系统调用申请的内存,如图中所示,要依次先释放80K的空间,然后再释放100K的空间,如果先释放100K的空间,则实际上并未进行释放,只有80K空间释放了才会释放,这期间如果有需要100K的空间,则系统会立刻使用这块区域。当堆顶连续空闲空间大于128K(通过M_TRIM_THRESHOLD调整,默认值为128K)时,就会调用brk()系统调用进行堆区指针的调整,释放出内存。







0 0
原创粉丝点击