浅析动态内存分配栈与堆

时间:2022-07-09 02:26:10
先举个例子:某用户需要一个将任意多个整数按大小排序的程序。(在计算机文件夹中,当文件很多时经常用到排序)
1。若不用动态分配内存,那就定义一个超大的数组吧!问题是,如果用户不需要那么大,不就浪费了?如果定义的数组还不够大,不就不能满足需求了?
2。如果用动态分配,就解决上述问题了。当你需要多大内存时,就给你多大——如果有的话——这就是动态分配的意义。

现在看上述问题的代码,我调试过的:
----------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
void main()
{
int n,*p,i,j,m;
printf("本程序可对任意个整数排序;\n");
printf("请输入整数的总个数: ");
scanf("%d",&n);
p=(int *)calloc(n,sizeof(int));
if(p==0) {
printf("分配失败!\n");
exit(1);
}
printf("请输入这些整数:\n");
for(i=0;i<n;i++)
scanf("%d",p+i);
for(i=1;i<n;i++)
{
for(j=0;j<n-i;j++)
if(*(p+j)>*(p+j+1))
{
m=*(p+j);
*(p+j)=*(p+j+1);
*(p+j+1)=m;
}
}
printf("将这些整数从小到大排列输出为:");
for(i=0;i<n;i++)
{
if(i%5==0) printf("\n");
printf(" d;",*(p+i));

}
printf("\n");
free(p);
}

----------------------------------------------------------------------
调用calloc函数时,calloc(n,sizeof(int))表示请求n个连续的、每个长度为整型的空间,若成功返回这些空间的首地址。(int *)表示将这个地址放在指针中。到此为止,就可以用指针来对分配到的空间操作了。注意,最后一定要用free函数释放申请到的空间,否则这部分空间会一直占着。

malloc、calloc、realloc的用法(以上述问题为例)及区别:
1。malloc(n*sizeof(int))
2。calloc(n,sizeof(int))
3。realloc(p,sizeof(int)*n)

 

=====================================================================

一、概述:

     动态内存分配,特别是开发者经常接触的Malloc/Free接口的实现,对许多开发者来说,是一个永远的话题,而且有时候也是一个比较迷惑的问题,本文根据自己的理解,尝试简单的探究一下在嵌入式系统中,两类典型系统中动态内存分配以及Malloc/Free的实现机制。

二、内存分配方式

      Malloc/Free主要实现的是动态内存分配,要理解它们的工作机制,就必须先了解操作系统内存分配的基本原理。

  在操作系统中,内存分配主要以下面三种方式存在:

   (1)静态存储区域分配。内存在程序编译的时候或者在操作系统初始化的时候就已经分配好,这块内存在程序的整个运行期间都存在,而且其大小不会改变,也不会被重新分配。例如全局变量,static变量等。

  (2)栈上的内存分配。栈是系统数据结构,对于进程/线程是唯一的,它的分配与释放由操作系统来维护,不需要开发者来管理。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元会被自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,不同的操作系统对栈都有一定的限制。

  (3) 堆上的内存分配,亦称动态内存分配。程序在运行的期间用malloc申请的内存,这部分内存由程序员自己负责管理,其生存期由开发者决定:在何时分配,分配多少,并在何时用free来释放该内存。这是唯一可以由开发者参与管理的内存。使用的好坏直接决定系统的性能和稳定。

三、动态内存分配概述

    首先,对于支持虚拟内存的操作系统,动态内存分配(包括内核加载,用户进程加载,动态库加载等等)都是建立在操作系统的虚拟内存分配之上的,虚拟内存分配主要包括:

    1、进程使用的内存地址是虚拟的(每个进程感觉自己拥有所有的内存资源),需要经过页表的映射才能最终指向系统实际的物理地址。

    2、主内存和磁盘采用页交换的方式加载进程和相关数据,而且数据何时加载到主内存,何时缓存到磁盘是OS调度的,对应用程序是透明的。

    3、虚拟存储器给用户程序提供了一个基于页面的内存大小,在32位系统中,用户可以页面大小为单位,分配到最大可以到4G(内核要使用1G或2G等内存地址)字节的虚拟内存。

    4、对于虚拟内存的分配,操作系统一般先分配出应用要求大小的虚拟内存,只有当应用实际使用时,才会调用相应的操作系统接口,为此应用程序分配大小以页面为单位的实际物理内存。

    5、不是所有计算机系统都有虚拟内存机制,一般在有MMU硬件支持的系统中才有虚拟内存的实现。许多嵌入式操作系统中是没有虚拟内存机制的,程序的动态分配实际是直接针对物理内存进行操作的。许多典型的实时嵌入式系统如Vxworks、Uc/OS 等就是这样。

四、动态内存分配的实现

       由于频繁的进行动态内存分配会造成内存碎片的产生,影响系统性能,所以在不同的系统中,对于动态内存管理,开发了许多不同的算法(具体的算法实现不想在这里做详细的介绍,有兴趣的读者可以参考Glib C 的源代码和附录中的资料)。不同的操作系统有不同的实现方式,为了程序的可移植性,一般在开发语言的库中都提供了统一接口。对于C语言,在标准C库和Glib 中,都实现了以malloc/free为接口的动态内存分配功能。也就是说,malloc/free库函索包装了不同操作系统对动态内存管理的不同实现,为开发者提供了一个统一的开发环境。对于我们前面提到的一些嵌入式操作系统,因为实时系统的特殊要求(实时性要求和开发者订制嵌入式系统),可能没有提供相应的接口。一般C 库中的malloc/free函数会实现应用层面的内存管理算法,在系统真正需要内存时,才通过操作系统的API(系统调用)来获取实际的物理内存,当然,你也可以使用第三方的内存管理器,或者通过自己改写malloc/free函数来实现应用层面的内存管理。

4.1、动态内存管理的一般机制:

   动态内存管理机制会随操作系统和系统架构的不同而不同,一些操作系统利用malloc等分配器,在支持虚拟内存的操作系统中就利用这种方式来实现进程动态内存管理的。而另外一些系统利用预先分配的内存区间来进行动态内存管理,该方式主要应用于不支持虚拟内存机制的嵌入式操作系统中。对于进程动态内存管理机制的具体实现,许多系统提供了统一的接口,并由malloc/free函数来具体实现。下面以嵌入式Linux和Uc/OS 为例,简单解析一下动态内存管理的具体实现。

4.2、Linux下动态内存分配的实现:

     在Linux下,glibc 的malloc提供了下面两种动态内存管理的方法:堆内存分配和mmap的内存分配,此两种分配方法都是通过相应的Linux 系统调用来进行动态内存管理的。具体使用哪一种方式分配,根据glibc的实现,主要取决于所需分配内存的大小。一般情况中,应用层面的内存从进程堆中分配,当进程堆大小不够时,可以通过系统调用brk来改变堆的大小,但是在以下情况,一般由mmap系统调用来实现应用层面的内存分配:A、应用需要分配大于1M的内存,B、在没有连续的内存空间能满足应用所需大小的内存时。

  (1)、调用brk实现进程里堆内存分配

    在glibc中,当进程所需要的内存较小时,该内存会从进程的堆中分配,但是堆分配出来的内存空间,系统一般不会回收,只有当进程的堆大小到达最大限额时或者没有足够连续大小的空间来为进程继续分配所需内存时,才会回收不用的堆内存。在这种方式下,glibc会为进程堆维护一些固定大小的内存池以减少内存脆片。

  (2)、使用mmap的内存分配

    在glibc中,一般在比较大的内存分配时使用mmap系统调用,它以页为单位来分配内存的(在Linux中,一般一页大小定义为4K),这不可避免会带来内存浪费,但是当进程调用free释放所分配的内存时,glibc会立即调用unmmap,把所分配的内存空间释放回系统。

注意:这里我们讨论的都是虚拟内存的分配(即应用层面上的内存分配),主要由glibc来实现,它与内核中实际物理内存的分配是不同的层面,进程所分配到的虚拟内存可能没有对应的物理内存。如果所分配的虚拟内存没有对应的物理内存时,操作系统会利用缺页机制来为进程分配实际的物理内存。

4.3、Uc/OS 下内存分配的实现:

    在一般的实时嵌入式系统中,由于实时性的要求,很少使用虚拟内存机制。所有的内存都需要开发人员参与分配,他们直接操作物理内存,所分配的内存不能超过系统的物理内存,于系统的堆栈的管理,都由开发者显式进行。

    在Uc/OS 中,主要利用内存分区来管理系统内存,系统中一般有多个分区,每个分区相当于一些固定大小内存块的内存池,应用可以从这些内存池中分配内存,当内存使用完成后,也需要把该内存释放回对应的内存池中。Uc/OS 的内存管理可以分为以下过程:

(1)、创建内存分区:在使用内存之前,开发者必须首先调用OSMemCreare()函数来创建相应的内存分区,在创建内存分区成功后,就会在系统中存在一个以开发者指定内存大小,指定内存块数目的内存池。在此过程中,开发者需要明确的知道系统的内存分布,并指明内存池的基址。

(2)、申请内存:当系统内存分区创建好了后,系统就可以从相应的内存分区中获取内存了。在Uc/OS 中,主要利用OSMemGet()来申请内存,应用程序会根据所需要内存的大小,从开发者指定的内存池中申请内存。

(3)、释放内存:因为内存是系统的紧缺资源,当应用不再需要使用所申请的内存时,应该及时释放该内存。在Uc/OS 中,主要利用OSMemPut()来释放不再需要的内存,在此过程中,开发者应该保证把该内存释放回原内存的分区。

    在一些实时嵌入式系统中,系统也会提供一些比较复杂的内存管理机制,并为应用提供类malloc/free接口供开发者使用,如Vxworks等。不管怎么样,在这些系统中,都得由开发者显式的参与内存的管理,而且应用程序只能使用实际物理内存大小的内存。


五、小结:

     动态内存管理是开发者唯一能够参与管理的内存分配机制,这给开发者灵活使用系统主存提供了一个手段,但是由于内存的分配和释放都得依靠开发者显式进行,很容易出现内存泄露的问题。另外,不好的动态内存管理算法对系统的性能影响有着也不可忽视影响。

在本文简单解析了两种动态内存管理实现,它们互有忧缺点:对于以虚拟内存机制和分页机制为基础的动态内存管理(如Linux等),由于请求分页机制的存在,不能满足系统实时性方面的要求,但是它能为应用提供最多能到4G的内存空间,而且由于应用虚拟内存机制,可以为进程空间提供保护,一个进程的崩溃不会影响其他的进程。对于基于非虚拟内存管理机制的系统,由于可以直接操作物理内存,提高了系统实时性,在这样的系统中,开发者的参与度比基于虚拟内存机制的要高。其缺点是没有进程间的保护机制,一个进程或任务的错误很容易导致整个系统的崩溃。

参考:

1、Glibc 源代码

2、嵌入式实时操作系统Uc/OS-II  

3、经典收藏之 - C++内存管理详解:http://www.oneedu.cn/xxyd/jzjs/aspnet/200703/14963.html

4、Understanding Linux kernel

5、The Virtual-Memory Manager in Windows NT:

http://msdn2.microsoft.com/en-us/library/ms810616.aspx

============================================================================

堆是硬件实现的,栈是一种数据结构,但是很多情况已经不区分它们了 

栈是由cpu实现的一种数据结构,其特点是后进先出。用于程序局部变量、程序返回地址。调用子程序、声明局部变量时,实际上就是在告诉编译程序使用这种数据结构。程序链接(link.exe)时,必须告诉链接程序为进程保留多大的栈空间。进程被加载时,操作系统就为该进程分配或者说保留这么大的内存空间作为栈来使用。 
  代码空间、栈空间、全局变量空间都是保留的,没有这些空间进程会加载失败。这个时候操作系统会提示系统内存太少无法加载进程。 
  堆空间相对代码空间、栈空间、全局变量空间,它是没娘的孩子。因为系统除去这些保留的空间,剩下的都是*空间,它的头上插了根草标,表示这些空间没有使用,谁都可以*的使用。 
  堆空间唯一有用的时候,就是你的程序在执行mallocreallocnew等操作时,堆空间就像站街女等待你的挑选。不过,操作系统的眼光好一点,它会通过malloc等函数帮你选好合适的站街女,选好的站街女就是这几个函数的返回值。 
  站街女用完了之后,就用freedelete等操作撵走。 

    再通俗一点来说,栈是已经预先保留的内存空间,必须按照后进先出的方法使用。 
  而堆是,进程加载之后,系统多余的空间。 

  如果程序加载时,操作系统提示内存不够导致加载失败,则必须去电脑城买一条更大的内存。 
  如果程序运行过程中,提示栈溢出,则必须告诉程序员,重新链接程序。链接程序时指定大一点的栈空间。 
  如果程序运行过程中,提示内存不够,这意味着堆空间不够。也必须去电脑城买一条更大的内存。 
  如果不想买内存,或许可以这样:关闭一些已经运行的程序。比如防火墙、杀毒软件等,游戏、realplaywinamp就别关了。电脑不娱乐,还有什么用呀^_^ 
  
补充一点:之所以要告诉链接程序需要保留多大的栈空间,是因为,无论是编译程序还是链接程序,都不知道程序运行需要多少栈空间,其实连程序员自己也不知道,只不过是根据经验指定。不过实际写程序时,好像大家并没有做这个操作,是因为开发平台有一个默认值已经设置好了。 
而代码空间和全局变量空间不一样,程序编译好了就知道需要多大的代码空间和全局变量空间。所以链接程序可以自己决定。