内存分配_分配器内存 - CSDN
  • C语言中内存分配

    2013-09-03 19:17:42
    在任何程序设计环境及语言中,内存管理都十分重要。在目前的计算机系统或嵌入式系统中,内存资源仍然是有限的。...第2节主要介绍C语言中内存分配及释放函数、函数的功能,以及如何调用这些函数申请

    在任何程序设计环境及语言中,内存管理都十分重要。在目前的计算机系统或嵌入式系统中,内存资源仍然是有限的。因此在程序设计中,有效地管理内存资源是程序员首先考虑的问题。

    第1节主要介绍内存管理基本概念,重点介绍C程序中内存的分配,以及C语言编译后的可执行程序的存储结构和运行结构,同时还介绍了堆空间和栈空间的用途及区别。

    第2节主要介绍C语言中内存分配及释放函数、函数的功能,以及如何调用这些函数申请/释放内存空间及其注意事项。

    3.1 内存管理基本概念

    3.1.1 C程序内存分配

    1.C程序结构

    下面列出C语言可执行程序的基本情况(Linux 2.6环境/GCC4.0)。

    [root@localhost Ctest]# ls test -l     //test为一个可执行程序
    -rwxr-xr-x  1 root root 4868 Mar 26 08:10 test
    [root@localhost Ctest]# file test //此文件基本情况
    test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
    for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped
    [root@localhost Ctest]# size test  //此二进制可执行文件结构情况
    //代码区静态数据/全局初始化数据区 未初始化数据区 十进制总和 十六进制总和 文件名
    text   data         bss    dec    hex filename
    906    284          4    1194    4aa test

    可以看出,此可执行程序在存储时(没有调入到内存)分为代码区(text)、数据区(data)和未初始化数据区(bss)3个部分。

    (1)代码区(text segment)。存放CPU执行的机器指令(machine instructions)。通常,代码区是可共享的(即另外的执行程序可以调用它),因为对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

    (2)全局初始化数据区/静态数据区(initialized data segment/data segment)。该区包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。例如,一个不在任何函数内的声明(全局数据):

    int   maxcount = 99;

    使得变量maxcount根据其初始值被存储到初始化数据区中。

    static mincount=100;

    这声明了一个静态数据,如果是在任何函数体外声明,则表示其为一个全局静态变量,如果在函数体内(局部),则表示其为一个局部静态变量。另外,如果在函数名前加上static,则表示此函数只能在当前文件中被调用。

    (3)未初始化数据区。亦称BSS区(uninitialized data segment),存入的是全局未初始化变量。BSS这个叫法是根据一个早期的汇编运算符而来,这个汇编运算符标志着一个块的开始。BSS区的数据在程序开始执行之前被内核初始化为0或者空指针(NULL)。例如一个不在任何函数内的声明:

    long  sum[1000];
    

    将变量sum存储到未初始化数据区。

    图3-1所示为可执行代码存储时结构和运行时结构的对照图。一个正在运行着的C编译程序占用的内存分为代码区、初始化数据区、未初始化数据区、堆区和栈区5个部分。

    (点击查看大图)图3-1 C程序的内存布局

    (1)代码区(text segment)。代码区指令根据程序设计流程依次执行,对于顺序指令,则只会执行一次(每个进程),如果反复,则需要使用跳转指令,如果进行递归,则需要借助栈来实现。

    代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的数值,如5),将直接包含在代码中;如果是局部数据,将在栈区分配空间,然后引用该数据地址;如果是BSS区和数据区,在代码中同样将引用该数据地址。

    (2)全局初始化数据区/静态数据区(Data Segment)。只初始化一次。

    (3)未初始化数据区(BSS)。在运行时改变其值。

    (4)栈区(stack)。由编译器自动分配释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。每当一个函数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,这就是C实现函数递归调用的方法。每执行一次递归函数调用,一个新的栈框架就会被使用,这样这个新实例栈里的变量就不会和该函数的另一个实例栈里面的变量混淆。

    (5)堆区(heap)。用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收。

    之所以分成这么多个区域,主要基于以下考虑:

    一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次,当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此单独开辟空间以方便访问和节约空间。

    临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。

    全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。

    堆区由用户自由分配,以便管理。

    下面通过一段简单的代码来查看C程序执行时的内存分配情况。相关数据在运行时的位置如注释所述。

    //main.cpp 
    int a = 0;    //a在全局已初始化数据区
    char *p1;    //p1在BSS区(未初始化全局变量)
    main()
    {
    int b;    //b在栈区
    char s[] = "abc"; //s为数组变量,存储在栈区,
    //"abc"为字符串常量,存储在已初始化数据区
    char *p1,p2;  //p1、p2在栈区
    char *p3 = "123456"; //123456\0在已初始化数据区,p3在栈区
    static int c =0;  //C为全局(静态)数据,存在于已初始化数据区
    //另外,静态数据会自动初始化
    p1 = (char *)malloc(10);//分配得来的10个字节的区域在堆区
    p2 = (char *)malloc(20);//分配得来的20个字节的区域在堆区
    free(p1);
    free(p2);
    }

    2.内存分配方式

    在C语言中,对象可以使用静态或动态的方式分配内存空间。

    静态分配:编译器在处理程序源代码时分配。

    动态分配:程序在执行时调用malloc库函数申请分配。

    静态内存分配是在程序执行之前进行的因而效率比较高,而动态内存分配则可以灵活的处理未知数目的。

    静态与动态内存分配的主要区别如下:

    静态对象是有名字的变量,可以直接对其进行操作;动态对象是没有名字的变量,需要通过指针间接地对它进行操作。

    静态对象的分配与释放由编译器自动处理;动态对象的分配与释放必须由程序员显式地管理,它通过malloc()和free两个函数(C++中为new和delete运算符)来完成。

    以下是采用静态分配方式的例子。

    int a=100;

    此行代码指示编译器分配足够的存储区以存放一个整型值,该存储区与名字a相关联,并用数值100初始化该存储区。

    以下是采用动态分配方式的例子。

    p1 = (char *)malloc(10*sizeof(int));//分配得来得10*4字节的区域在堆区

    此行代码分配了10个int类型的对象,然后返回对象在内存中的地址,接着这个地址被用来初始化指针对象p1,对于动态分配的内存唯一的访问方式是通过指针间接地访问,其释放方法为:

    free(p1);
    

    3.1.2 栈和堆的区别

    前面已经介绍过,栈是由编译器在需要时分配的,不需要时自动清除的变量存储区。里面的变量通常是局部变量、函数参数等。堆是由malloc()函数(C++语言为new运算符)分配的内存块,内存释放由程序员手动控制,在C语言为free函数完成(C++中为delete)。栈和堆的主要区别有以下几点:

    (1)管理方式不同。

    栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。

    (2)空间大小不同。

    栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。

    堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的,不会存在一个内存块从栈中间弹出的情况。

    (3)是否产生碎片。

    对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。

    (4)增长方向不同。

    堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。

    (5)分配方式不同。

    堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手工实现。

    (6)分配效率不同。

    栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。堆则是C函数库提供的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的空间,如果没有足够大的空间(可能是由于内存碎片太多),就有需要操作系统来重新整理内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率比栈要低得多。

    3.1.3 Linux数据类型大小

    在Linux操作系统下使用GCC进行编程,目前一般的处理器为32位字宽,下面是/usr/include/limit.h文件对Linux下数据类型的限制及存储字节大小的说明。

    /* We don't have #include_next.   Define ANSI  for standard 32-bit words.  */
    /* These assume 8-bit 'char's, 16-bit 'short int's,   and 32-bit 'int's and 'long int's.  */

    1.char数据类型

    char类型数据所占内存空间为8位。其中有符号字符型变量取值范围为?128~127,无符号型字符变量取值范围为0~255。其限制如下:

    /* Number of bits in a 'char'. */
    #  define CHAR_BIT 8          //所占字节数
    /* Minimum and maximum values a 'signed char' can hold.  */  //有符号字符型范围
    #  define SCHAR_MIN (-128)
    #  define SCHAR_MAX 127
    /* Maximum value an 'unsigned char' can hold.  (Minimum is 0.)  */ //无符号字符型范围
    #  define UCHAR_MAX 255
    /* Minimum and maximum values a 'char' can hold.  */
    #  ifdef __CHAR_UNSIGNED__
    #   define CHAR_MIN 0
    #   define CHAR_MAX UCHAR_MAX
    #  else
    #   define CHAR_MIN SCHAR_MIN
    #   define CHAR_MAX SCHAR_MAX
    #  endif

    2.short int数据类型

    short int类型数据所占内存空间为16位。其中有符号短整型变量取值范围为?32768~32767,无符号短整型变量取值范围为0~65535。其限制如下:

    /* Minimum and maximum values a 'signed short int' can hold.  */ // 有符号短整型范围
    #  define SHRT_MIN (-32768)
    #  define SHRT_MAX 32767
    /* Maximum value an 'unsigned short int' can hold.  (Minimum is 0.)  */// 无符号短整型范围
    #  define USHRT_MAX 65535

    3.int数据类型

    int类型数据所占内存空间为32位。其中有符号整型变量取值范围为?2147483648~2147483647,无符号型整型变量取值范围为0~4294967295U。其限制如下:

    /* Minimum and maximum values a 'signed int' can hold.  */  //整形范围
    #  define INT_MIN (-INT_MAX - 1)     
    #  define INT_MAX 2147483647
    /* Maximum value an 'unsigned int' can hold.  (Minimum is 0.)  */ //无符号整形范围
    #  define UINT_MAX 4294967295U

    4.long int数据类型

    随着宏__WORDSIZE值的改变,long int数据类型的大小也会发生改变。如果__WORDSIZE的值为32,则long int和int类型一样,占有32位。在Linux GCC4.0-i386版本中,默认情况下__WORDSIZE的值为32。其定义如下:

    //come from /usr/include/bits/wordsize.h
    #define __WORDSIZE 32

    在64位机器上,如果__WORDSIZE的值为64, long int类型数据所占内存空间为64位。其中有长整型变量取值范围为-9223372036854775808L~3372036854775807L,无符号长整型变量取值范围为0~18446744073709551615UL。其限制如下:

    /* Minimum and maximum values a 'signed long int' can hold.  */ //有符号长整形范围
    #  if __WORDSIZE == 64
    #   define LONG_MAX 9223372036854775807L
    #  else
    #   define LONG_MAX 2147483647L
    #  endif
    #  define LONG_MIN (-LONG_MAX - 1L)
    /* Maximum value an 'unsigned long int' can hold.  (Minimum is 0.)  *///无符号长整形范围
    #  if __WORDSIZE == 64
    #   define ULONG_MAX 18446744073709551615UL
    #  else
    #   define ULONG_MAX 4294967295UL
    #  endif

    5.long long int数据类型

    在C99中,还定义了long long int数据类型。其数据类型限制如下:

    #  ifdef __USE_ISOC99
    /* Minimum and maximum values a 'signed long long int' can hold.  *///无符号长长整形范围
    #   define LLONG_MAX 9223372036854775807LL
    #   define LLONG_MIN (-LLONG_MAX - 1LL)
    /* Maximum value an 'unsigned long long int' can hold.  (Minimum is 0.)  *///有符号长长整形范围
    #   define ULLONG_MAX 18446744073709551615ULL
    #  endif /* ISO C99 */

    3.1.4 数据存储区域实例

    此程序显示了数据存储区域实例,在此程序中,使用了etext、edata和end3个外部全局变量,这是与用户进程相关的虚拟地址。

    在程序源代码中列出了各数据的存储位置,同时在程序运行时显示了各数据的运行位置,图3-2所示为程序运行过程中各变量的存储位置。

    图3-2 函数运行时各数据位置

    主函数源代码如下:

    [root@localhost linux_app]# cat mem_add.c
    #include
    #include
    #include
    #include
    extern void afunc(void);
    extern etext,edata,end;
    int bss_var;                                //未初始化全局数据存储在BSS区
    int data_var=42;                            //初始化全局数据存储在数据区
    #define SHW_ADR(ID,I) printf("the %8s\t is at adr:%8x\n",ID,&I); //打印地址宏
    int main(int argc,char *argv[])
    {
    char *p,*b,*nb;
    printf("Adr etext:%8x\t Adr edata %8x\t Adr end %8x\t\n",&etext,&edata,&end);
    printf("\ntext Location:\n");
    SHW_ADR("main",main);              //查看代码段main函数位置
    SHW_ADR("afunc",afunc);           //查看代码段afunc函数位置
    printf("\nbss Location:\n");
    SHW_ADR("bss_var",bss_var);      /查看BSS段变量位置
    printf("\ndata location:\n");
    SHW_ADR("data_var",data_var);     /查看数据段变量
    printf("\nStack Locations:\n"); 
    afunc();
    p=(char *)alloca(32);              //从栈中分配空间
    if(p!=NULL)
    {
    SHW_ADR("start",p);
    SHW_ADR("end",p+31);
    }
    b=(char *)malloc(32*sizeof(char));   //从堆中分配空间
    nb=(char *)malloc(16*sizeof(char));  //从堆中分配空间
    printf("\nHeap Locations:\n");
    printf("the Heap start: %p\n",b);   //堆起始位置
    printf("the Heap end:%p\n",(nb+16*sizeof(char)));//堆结束位置
    printf("\nb and nb in Stack\n");
    SHW_ADR("b",b);       //显示栈中数据b的位置
    SHW_ADR("nb",nb);       //显示栈中数据nb的位置
    free(b);         //释放申请的空间,以避免内存泄漏
    free(nb);         //释放申请的空间,以避免内存泄漏
    }

    子函数源代码如下:

    void afunc(void)
    {
    static int long level=0;          //静态数据存储在数据段中
    int      stack_var;                 //局部变量,存储在栈区
    if(++level==5)
    {
    return;
    }
    printf("stack_var is at:%p\n",&stack_var);
    //      SHW_ADR("stack_var in stack section",stack_var);
    //      SHW_ADR("Level in data section",level);
    afunc();
    }

    函数运行结果如下:

    [root@localhost linux_app]# gcc -o mem_add mem_add.c //编译
    [root@localhost linux_app]# ./mem_add     //运行结果
    Adr etext: 8048702       Adr edata  8049950      Adr end  804995c
    text Location:
    the     main     is at adr: 8048418
    the    afunc     is at adr: 8048611
    bss Location:
    the  bss_var     is at adr: 8049958
    data location:
    the data_var     is at adr: 804994c
    Stack Locations:
    the stack_var in stack section    is at adr:bfbf6c44
    the Level in data section         is at adr: 8049954
    the stack_var in stack section    is at adr:bfbf6c24
    the Level in data section         is at adr: 8049954
    the stack_var in stack section    is at adr:bfbf6c04
    the Level in data section         is at adr: 8049954
    the stack_var in stack section    is at adr:bfbf6be4
    the Level in data section         is at adr: 8049954
    the    start     is at adr:bfbf6c74
    the      end     is at adr:bfbf6cf0
    Heap Locations:
    the Heap start: 0x8453008
    the Heap end:0x8453040
    b and nb in Stack
    the        b     is at adr:bfbf6c70
    the       nb     is at adr:bfbf6c6c

    如果运行环境不一样,运行程序的地址与此将有差异,但是,各区域之间的相对关系不会发生变化。可以通过readelf命令来查看可执行文件的详细内容。

    [root@localhost yangzongde]# readelf -a memadd

    3.2 内存管理函数

    3.2.1 malloc/free函数

    Malloc()函数用来在堆中申请内存空间,free()函数释放原先申请的内存空间。Malloc()函数是在内存的动态存储区中分配一个长度为size字节的连续空间。其参数是一个无符号整型数,返回一个指向所分配的连续存储域的起始地址的指针。当函数未能成功分配存储空间时(如内存不足)则返回一个NULL指针。

    由于内存区域总是有限的,不能无限制地分配下去,而且程序应尽量节省资源,所以当分配的内存区域不用时,则要释放它,以便其他的变量或程序使用。

    这两个函数的库头文件为:

    #include

    函数定义如下:

    void *malloc(size_t size)   //返回类型为空指针类型
    void free(void *ptr)

    例如:

    int *p1,*p2;
    p1=(int *)malloc(10*sizeof(int));
    p2=p1;
    ……
    free(p2) ;      /*或者free(p1)*/
    p1=NULL;       /*或者p2=NULL */

    malloc()函数返回值赋给p1,又把p1的值赋给p2,所以此时p1,p2都可作为free函数的参数。使用free()函数时,需要特别注意下面几点:

    (1)调用free()释放内存后,不能再去访问被释放的内存空间。内存被释放后,很有可能该指针仍然指向该内存单元,但这块内存已经不再属于原来的应用程序,此时的指针为悬挂指针(可以赋值为NULL)。

    (2)不能两次释放相同的指针。因为释放内存空间后,该空间就交给了内存分配子程序,再次释放内存空间会导致错误。也不能用free来释放非malloc()、calloc()和realloc()函数创建的指针空间,在编程时,也不要将指针进行自加操作,使其指向动态分配的内存空间中间的某个位置,然后直接释放,这样也有可能引起错误。

    (3)在进行C语言程序开发中,malloc/free是配套使用的,即不需要的内存空间都需要释放回收。

    下面是使用这两个函数的一个例子。

    [root@localhost yangzongde]# cat malloc_example.c 
    #include               //printf()    //(1)头文件信息
    #include              //malloc()    //(2)
    int main(int argc,char* argv[],char* envp[])   //(3)
    {
    int count;
    int* array;
    if((array=(int *)malloc(10*sizeof(int)))==NULL)  //(4)分配空间
    {
    printf("malloc memory unsuccessful");
    exit(1);
    }
    for (count=0;count<10;count++)      //(5) 赋值
    {
    *array=count;
    array++;
    }
    for(count=9;count>=0;count--)                  //(6)赋值
    {
    array--;
    printf("%4d",*array);
    }
    printf("\n");
    free(array);        //(7)释放空间
    array=NULL;       //(8)将指针置为空,避免不安全访问
    exit (0);
    }
    [root@localhost yangzongde]# gcc -o malloc_example malloc_example.c  //编译
    [root@localhost yangzongde]# ./malloc_example       //运行
    9   8   7   6   5   4   3   2   1   0

    在以上程序中,(1)句中包含stdio.h头文件,从而在后面可以调用printf()函数。(2)句中包含stdlib.h头文件,其是malloc()函数的头文件。(3)句为函数的入口位置,此处采用Linux下编程标准,返回值为int型,argc为参数个数, argv[]为参数,envp[]存放的是所有环境变量。(4)句动态分配了10个整型存储区域,此语句可以分为以下几步。

    ① 分配10个整型的连续存储空间,并返回一个指向其起始地址的整型指针。

    ② 把此整型指针地址赋给array。

    ③ 检测返回值是否为NULL。

    (5)、(6)句为数组赋值并打印输出,以免内存泄漏。(7)句调用free()函数释放内存空间。(8)句将一个NULL指针传递给array,虽然在很多情况下可以不用此句,但这样处理可以避免此指针成为野指针。

    在C++中,使用new和delete运算符来实现内存的分配和释放,使用new/delete运算符实现内存管理比使用malloc/free函数更有优越性。new/delete运算符定义如下:

    static void* operator new(size_t sz);     //new运算符
    static void  operator delete(void* p);      //delete运算符

    下面是一段C++程序代码:

    void UseNewDelete(void)
    {
    Obj  *a = new Obj;           //申请动态内存并且初始化
    //…
    delete a;                   //清除并且释放内存
    }

    下面详细介绍C++中new/delete运算符的使用方法。

    class A
    {
    public:
    A()  {   cout<<"A is here!"<<ENDL;&NBSP;&NBSP; 构造函数 ~A() {   cout<<"A is dead!"<<ENDL;&NBSP;&NBSP; }  ="" 析构函数 private:
    int i;
    };
    A* pA=new A;     //调用new运算符申请空间
    delete pA;      //删除pA

    其中,语句new A完成了以下两个功能:

    (1)调用运算符new,在自由存储区分配一个sizeof(A)大小的内存空间。

    (2)调用构造函数A(),在这块内存空间上初始化对象。

    当然,delete pA完成相反的两件事:

    (1)调用析构函数~A(),销毁对象。

    (2)调用运算符delete,释放内存。

    由此可以看出,运算符new和delete提供了动态分配和释放存储区的功能。它们的作用相当于C语言的malloc()和free()函数,但是性能更为优越。使用new比使用malloc()有以下几个优点:

    (1)new自动计算要分配给对象的内存空间大小,不使用sizeof运算符,简单,而且可以避免错误。

    (2)自动地返回正确的指针类型,不用进行强制类型转换。

    (3)用构造函数给分配的对象进行初始化。

    但是,使用malloc函数和new分配内存的时候,本身并没有对这块内存空间做清零等任何动作。因此,申请内存空间后,其返回的新分配的内存是没有零填充的,程序员需要使用memset()函数来初始化内存。

    3.2.2 realloc--更改已经配置的内存空间

    realloc()函数用来从堆上分配内存,当需要扩大一块内存空间时,realloc()试图直接从堆上当前内存段后面的字节中获得更多的内存空间,如果能够满足,则返回原指针;如果当前内存段后面的空闲字节不够,那么就使用堆上第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,而将原来的数据块释放掉。如果内存不足,重新申请空间失败,则返回NULL。此函数定义如下:

    void *realloc(void *ptr,size_t size)

    参数ptr为先前由malloc、calloc和realloc所返回的内存指针,而参数size为新配置的内存大小。其库头文件为:

    #include<stdlib.h>

    当调用realloc()函数重新分配内存时,如果申请失败,将返回NULL,此时原来指针仍然有效,因此在程序编写时需要进行判断,如果调用成功,realloc()函数会重新分配一块新内存,并将原来的数据拷贝到新位置,返回新内存的指针,而释放掉原来指针(realloc()函数的参数指针)指向的空间,原来的指针变为不可用(即不需要再释放,也不能再释放),因此,一般不使用以下语句:

    ptr=realloc(ptr,new_amount)

    如果内存减少,malloc仅仅改变索引信息,但并不代表被减少的部分还可以访问,这一部分内存将交给系统内存分配子程序。

    下面是一个使用relloc函数的实例。

    [root@localhost yangzongde]# cat realloc_example.c 
    #include <stdio.h>
    #include <stdlib.h>
    int main (int argc,char* argv[],char* envp[])   //(1)主函数
    {
    int input;
    int n;
    int *numbers1;
    int *numbers2;
    numbers1=NULL;
         if((numbers2=(int *)malloc(5*sizeof(int)))==NULL) //(2)numbers2指针申请空间
    {
    printf("malloc memory unsuccessful");
    //free(numbers2);
    numbers2=NULL;
    exit(1);
    }
    for (n=0;n<5;n++)       //(3)初始化
    {
    *(numbers2+n)=n;
    printf("numbers2's data: %d\n",*(numbers2+n));
    }
         printf("Enter an integer value you want to remalloc ( enter 0 to stop)\n");//(4)新申请空间大小 
    scanf ("%d",&input);

    numbers1=(int *)realloc(numbers2,(input+5)*sizeof(int));  //(5)重新申请空间
    if (numbers1==NULL)
    {
    printf("Error (re)allocating memory");
    exit (1);
    }
         for(n=0;n<5;n++)       //(6)这5个数是从numbers2拷贝而来
    {
    printf("the numbers1s's data copy from numbers2: %d\n",*(numbers1+n));
    }
         for(n=0;n<input;n++)       //(7)新数据初始化
    {
    *(numbers1+5+n)=n*2;
    printf ("nummber1's new data: %d\n",*(numbers1+5+n)); // numbers1++;
    }
    printf("\n");
    free(numbers1);       //(8)释放numbers1
    numbers1=NULL;
    // free(numbers2);     //(9)不能再释放numbers2
    return 0;
    }
    [root@localhost yangzongde]# gcc -o realloc_example realloc_example.c
    [root@localhost yangzongde]# ./realloc_example
    numbers2's data: 0
    numbers2's data: 1
    numbers2's data: 2
    numbers2's data: 3
    numbers2's data: 4
    Enter an integer value you want to remalloc ( enter 0 to stop) //重新申请空间

    the numbers1s's data copy from numbers2: 0
    the numbers1s's data copy from numbers2: 1
    the numbers1s's data copy from numbers2: 2
    the numbers1s's data copy from numbers2: 3
    the numbers1s's data copy from numbers2: 4
    nummber1's new data: 0
    nummber1's new data: 2
    nummber1's new data: 4
    nummber1's new data: 6
    nummber1's new data: 8

    此程序是一个简单的重新申请内存空间的实例,(1)为函数入口,前面已经介绍过。(2)从堆空间中申请5个int空间,将返回地址赋给numbers2,如果返回值为NULL,将返回错误信息,释放numbers2并退出。(3)为新申请的空间初始化。(4)输入需要增加的内存数量。(5)调用realloc()函数重新申请内存空间,重新申请内存空间大小为原有空间大小加上用户输入的内存空间数。如果申请失败,将返回NULL,此时numbers2仍然有效。如果申请成功,将重新分配一块大小合适的空间,并将新空间首地址赋给numbers1,同时将numbers2所指向的5个空间的数据复制到新的内存空间中,释放掉原来numbers2所指向的内存空间。(6)打印从numbers2所指向的原空间拷贝的数据,(7)句对新增加的空间进行初始化。(8)句释放number1所指向的新申请空间。(9)为注释掉的代码,提示读者此时对原空间再次释放,因为第(5)已经完成了这一操作。

    3.2.3 其他内存管理函数calloc和alloca

    1.calloc函数

    calloc是malloc函数的简单包装,它的主要优点是把动态分配的内存进行初始化,全部清零。其操作及语法类似malloc()函数。

    ptr=(struct data *)calloc (count,sizeof(strunt data)) //申请并初始化空间

    下面是这个函数的实现描述:

    void *calloc(size_t nmemb,size_t size)
    {
    void *p;
    size_t total;
    total=nmemb *size;
    p=malloc(total);         //申请空间
    if(p!=NULL)
    memset(p,'\0',total);      //将其实始化为\0
    return p;
    }

    2.alloca函数

    alloca()函数用来在栈中分配size个字节的内存空间,因此函数返回时会自动释放掉空间。alloca函数定义及库头文件如下:

    /* Allocate a block that will be freed when the calling function exits.  */
    extern void *alloca (size_t __size) __THROW;   //从栈中申请空间

    返回值:若分配成功返回指针,失败则返回NULL。

    它与malloc()函数的区别主要在于:

    alloca是向栈申请内存,无需释放,malloc申请的内存位于堆中,最终需要函数free来释放。

    malloc函数并没有初始化申请的内存空间,因此调用malloc()函数之后,还需调用函数memset初始化这部分内存空间;alloca则将初始化这部分内存空间为0。



     


    展开全文
  • c 程序内存分配管理

    2018-09-23 15:44:33
    给变量分配内存空间可分为静态内存分配和动态内存分配。 静态内存分配属于编译时给变量分配的空间,动态分配属于在程序运行时给变量分配的空间 静态分配属于栈分配,动态分配属于堆分配 运行效率上,静态内存比...

    首先澄清几个概念,

    给变量分配内存空间可分为静态内存分配和动态内存分配。

    静态内存分配属于编译时给变量分配的空间,动态分配属于在程序运行时给变量分配的空间

    静态分配属于栈分配,动态分配属于堆分配

    运行效率上,静态内存比动态内存要快

    int a[10] 属于静态分配

    int a[n] 或 int *a; a = (int*)malloc(sizeof(int)*n) 属于动态分配

    一般情况下采用malloc()函数进行动态空间分配,并且用free()函数来释放空间

     

    以下为转载内容

    https://www.cnblogs.com/tuhooo/p/7221136.html

    深入理解C语言内存管理

    之前在学Java的时候对于Java虚拟机中的内存分布有一定的了解,但是最近在看一些C,发现居然自己对于C语言的内存分配了解的太少。

    问题不能拖,我这就来学习一下吧,争取一次搞定。 在任何程序设计环境及语言中,内存管理都十分重要。

    内存管理的基本概念

    分析C语言内存的分布先从Linux下可执行的C程序入手。现在有一个简单的C源程序hello.c

    复制代码

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 int var1 = 1;
    4 
    5 int main(void) {
    6   int var2 = 2;
    7   printf("hello, world!\n");
    8   exit(0);
    9 }

    复制代码

    经过gcc hello.c进行编译之后得到了名为a.out的可执行文件

    [tuhooo@localhost leet_code]$ ls -al a.out
    -rwxrwxr-x. 1 tuhooo tuhooo 8592 Jul 22 20:40 a.out

    ls命令是查看文件的元数据信息

    [tuhooo@localhost leet_code]$ file a.out
    a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=23c58f2cad39d8b15b91f0cc8129055833372afe, not stripped

    file命令用来识别文件类型,也可用来辨别一些文件的编码格式。

    它是通过查看文件的头部信息来获取文件类型,而不是像Windows通过扩展名来确定文件类型的。

    [tuhooo@localhost leet_code]$ size a.out

    text  data bss dec hex filename
    (代码区静态数据) (全局初始化静态数据) (未初始化数据区)  (十进制总和) (十六制总和) (文件名)
    1301 560 8 1869 74d a.out

    显示一个目标文件或者链接库文件中的目标文件的各个段的大小,当没有输入文件名时,默认为a.out。

    size:支持的目标: elf32-i386 a.out-i386-linux efi-app-ia32 elf32-little elf32-big srec symbolsrec tekhex binary ihex trad-core。

    那啥,可执行文件在存储(也就是还没有载入到内存中)的时候,分为:代码区数据区未初始化数据区3个部分。

    进一步解读

    (1)代码区(text segment)。存放CPU执行的机器指令(machine instructions)。通常,代码区是可共享的(即另外的执行程序可以调用它),因为对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

    (2)全局初始化数据区/静态数据区(initialized data segment/data segment)。该区包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。例如,一个不在任何函数内的声明(全局数据):

    1 int maxcount = 99;

    使得变量maxcount根据其初始值被存储到初始化数据区中。

    1 static mincount = 100; 

    这声明了一个静态数据,如果是在任何函数体外声明,则表示其为一个全局静态变量,如果在函数体内(局部),则表示其为一个局部静态变量。另外,如果在函数名前加上static,则表示此函数只能在当前文件中被调用。

    (3)未初始化数据区。亦称BSS区(uninitialized data segment),存入的是全局未初始化变量。BSS这个叫法是根据一个早期的汇编运算符而来,这个汇编运算符标志着一个块的开始。BSS区的数据在程序开始执行之前被内核初始化为0或者空指针(NULL)。例如一个不在任何函数内的声明:

    1 long sum[1000];

    将变量sum存储到未初始化数据区。

    下图所示为可执行代码存储时结构和运行时结构的对照图。一个正在运行着的C编译程序占用的内存分为代码区、初始化数据区、未初始化数据区、堆区和栈区5个部分。

     

    再来看一张图,多个一个命令行参数区:

    (1)代码区(text segment)。代码区指令根据程序设计流程依次执行,对于顺序指令,则只会执行一次(每个进程),如果反复,则需要使用跳转指令,如果进行递归,则需要借助栈来实现。代码段: 代码段(code segment/text segment )通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的数值,如5),将直接包含在代码中;如果是局部数据,将在栈区分配空间,然后引用该数据地址;如果是BSS区和数据区,在代码中同样将引用该数据地址。另外,代码段还规划了局部数据所申请的内存空间信息。

    (2)全局初始化数据区/静态数据区(Data Segment)。只初始化一次。数据段: 数据段(data segment )通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。data段中的静态数据区存放的是程序中已初始化的全局变量、静态变量和常量。

    (3)未初始化数据区(BSS)。在运行时改变其值。BSS 段: BSS 段(bss segment )通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS 是英文Block Started by Symbol 的简称。BSS 段属于静态内存分配,即程序一开始就将其清零了。一般在初始化时BSS段部分将会清零。

    (4)栈区(stack)。由编译器自动分配释放,存放函数的参数值、局部变量的值等。存放函数的参数值、局部变量的值,以及在进行任务切换时存放当前任务的上下文内容。其操作方式类似于数据结构中的栈。每当一个函数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,这就是C实现函数递归调用的方法。每执行一次递归函数调用,一个新的栈框架就会被使用,这样这个新实例栈里的变量就不会和该函数的另一个实例栈里面的变量混淆。栈(stack) :栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧"{}"中定义的变量(但不包括static 声明的变量,static 意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/ 恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

    (5)堆区(heap)。用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收。堆(heap): 堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并将在内存中为这些段分配空间。栈段亦由操作系统分配和管理,而不需要程序员显示地管理;堆段由程序员自己管理,即显式地申请和释放空间。

    另外,可执行程序在运行时具有相应的程序属性。在有操作系统支持时,这些属性页由操作系统管理和维护。

    C语言程序编译完成之后,已初始化的全局变量保存在DATA段中,未初始化的全局变量保存在BSS段中。TEXT和DATA段都在可执行文件中,由系统从可执行文件中加载;而BSS段不在可执行文件中,由系统初始化。BSS段只保存没有值的变量,所以事实上它并不需要保存这些变量的映像。运行时所需要的BSS段大小记录在目标文件中,但是BSS段并不占据目标文件的任何空间。

    以上两图来自于《C语言专家编程》。

    在操作系统中,一个进程就是处于执行期的程序(当然包括系统资源),实际上正在执行的程序代码的活标本。那么进程的逻辑地址空间是如何划分的呢?

     

    左边的是UNIX/LINUX系统的执行文件,右边是对应进程逻辑地址空间的划分情况。

    首先是堆栈区(stack),堆栈是由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。栈的申请是由系统自动分配,如在函数内部申请一个局部变量 int h,同时判别所申请空间是否小于栈的剩余空间,如若小于的话,在堆栈中为其开辟空间,为程序提供内存,否则将报异常提示栈溢出。  

    其次是堆(heap),堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。堆的申请是由程序员自己来操作的,在C中使用malloc函数,而C++中使用new运算符,但是堆的申请过程比较复杂:当系统收到程序的申请时,会遍历记录空闲内存地址的链表,以求寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲 结点链表中删除,并将该结点的空间分配给程序,此处应该注意的是有些情况下,新申请的内存块的首地址记录本次分配的内存块大小,这样在delete尤其是 delete[]时就能正确的释放内存空间。

    接着是全局数据区(静态区) (static),全局变量和静态变量的存储是放在一块的初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 另外文字常量区,常量字符串就是放在这里,程序结束后有系统释放

    最后是程序代码区,放着函数体的二进制代码。

    为什么要这么分配内存?

    (1)一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次,当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此单独开辟空间以方便访问和节约空间。

    (2)临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。

    (3)全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。

    (4)堆区由用户自由分配,以便管理。

    举例说明内存分布情况

    复制代码

     1 /* memory_allocate.c用于演示内存分布情况 */
     2 
     3 int a = 0;                      /* a在全局已初始化数据区 */
     4 char *p1;                       /* p1在BSS区(未初始化全局变量) */
     5 
     6 int main(void) {
     7   int b;                        /* b在栈区 */
     8   char s[] = "abc";             /* s为数组变量, 存储在栈区 */
     9   /* "abc"为字符串常量, 存储在已初始化数据区 */
    10   char *p1, p2;                 /* p1、p2在栈区 */
    11   char *p3 = "123456";          /* "123456\0"已初始化在数据区, p3在栈区 */
    12   static int c = 0;             /* c为全局(静态)数据, 存在于已初始化数据区 */
    13   /* 另外, 静态数据会自动初始化 */
    14   p1 = (char *)malloc(10);      /* 分配的10个字节的区域存在于堆区 */
    15   p2 = (char *)malloc(20);      /* 分配得来的20个字节的区域存在于堆区 */
    16   
    17   free(p1);
    18   free(p2);
    19 }

    复制代码

    内存的分配方式

    在C语言中,对象可以使用静态或动态的方式分配内存空间。

    静态分配:编译器在处理程序源代码时分配。

    动态分配:程序在执行时调用malloc库函数申请分配。

    静态内存分配是在程序执行之前进行的因而效率比较高,而动态内存分配则可以灵活的处理未知数目的。

    静态与动态内存分配的主要区别如下:

    静态对象是有名字的变量,可以直接对其进行操作;动态对象是没有名字的一段地址,需要通过指针间接地对它进行操作。

    静态对象的分配与释放由编译器自动处理;动态对象的分配与释放必须由程序员显式地管理,它通过malloc()和free两个函数来完成。

    以下是采用静态分配方式的例子。

    1 int a = 100;

    此行代码指示编译器分配足够的存储区以存放一个整型值,该存储区与名字a相关联,并用数值100初始化该存储区。

    以下是采用动态分配方式的例子:

    1 p1 = (char *)malloc(10*sizeof(int));

    此行代码分配了10个int类型的对象,然后返回对象在内存中的地址,接着这个地址被用来初始化指针对象p1,对于动态分配的内存唯一的访问方式是通过指针间接地访问,其释放方法为:

    1 free(p1);

    栈和堆的区别

    前面已经介绍过,栈是由编译器在需要时分配的,不需要时自动清除的变量存储区。里面的变量通常是局部变量、函数参数等。堆是由malloc()函数分配的内存块,内存释放由程序员手动控制,在C语言为free函数完成。栈和堆的主要区别有以下几点:

    (1)管理方式不同。

    栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M。当然,这个值可以修改。碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问 题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在它弹出之前,在它上面的后进的栈内容已经被弹出,详细的可以参考数据结构。生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的, 比如局部变量的分配。动态分配由malloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址, EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果。

    (2)空间大小不同。

    栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。

    堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的,不会存在一个内存块从栈中间弹出的情况。

    在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

    (3)是否产生碎片。

    对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。

    (4)增长方向不同。

    堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。

    (5)分配方式不同。

    堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手工实现。

    STACK: 由系统自动分配。例如,声明在函数中一个局部变量 int b;系统自动在栈中为b开辟空间。HEAP:需要程序员自己申请,并指明大小,在C中malloc函数。指向堆中分配内存的指针则可能是存放在栈中的。

    (6)分配效率不同。

    栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。堆则是C函数库提供的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的空间,如果没有足够大的空间(可能是由于内存碎片太多),就有需要操作系统来重新整理内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率比栈要低得多。

    栈由系统自动分配,速度较快。但程序员是无法控制的。

    堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

    (7)申请后系统的响应

    栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

    堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

    (8)堆和栈中的存储内容

    栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。栈中的内存是在程序编译完成以后就可以确定的,不论占用空间大小,还是每个变量的类型。

    堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

    (9)存取效率的比较

    1 char s1[] = "a";
    2 char *s2 = "b";

    a是在运行时刻赋值的;而b是在编译时就确定的但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。

    (10)防止越界发生

    无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的

    数据存储区域实例

    此程序显示了数据存储区域实例,在此程序中,使用了etext、edata和end3个外部全局变量,这是与用户进程相关的虚拟地址。在程序源代码中列出了各数据的存储位置,同时在程序运行时显示了各数据的运行位置,下图所示为程序运行过程中各变量的存储位置。

    mem_add.c

    复制代码

     1 /* mem_add.c演示了C语言中地址的分布情况 */
     2 
     3 #include <stdio.h>
     4 #include <stdlib.h>
     5 
     6 extern void afunc(void);
     7 extern etext, edata, end;
     8 
     9 int bss_var;                    /* 未初始化全局数据存储在BSS区 */
    10 int data_var = 42;              /* 初始化全局数据区域存储在数据区 */
    11 #define SHW_ADDR(ID, I) printf("the %8s\t is at addr:%8x\n", ID, &I); /* 打印地址 */
    12 
    13 int main(int argc, char *argv[]) {
    14 
    15   char *p, *b, *nb;
    16   printf("Addr etext: %8x\t Addr edata %8x\t Addr end %8x\t\n", &etext, &edata, &end);
    17 
    18   printf("\ntext Location:\n");
    19   SHW_ADDR("main", main);       /* 查看代码段main函数位置 */
    20   SHW_ADDR("afunc", afunc);     /* 查看代码段afunc函数位置 */
    21   printf("\nbss Location:\n");
    22   SHW_ADDR("bss_var", bss_var); /* 查看BSS段变量的位置 */
    23   printf("\ndata Location:\n");
    24   SHW_ADDR("data_var", data_var); /* 查看数据段变量的位置 */
    25   printf("\nStack Locations:\n");
    26 
    27   afunc();
    28   p = (char *)alloca(32);       /* 从栈中分配空间 */
    29   if(p != NULL) {
    30     SHW_ADDR("start", p);
    31     SHW_ADDR("end", p+31);
    32   }
    33 
    34   b = (char *)malloc(32*sizeof(char)); /* 从堆中分配空间 */
    35   nb = (char *)malloc(16*sizeof(char)); /* 从堆中分配空间 */
    36   printf("\nHeap Locations:\n");
    37   printf("the Heap start: %p\n", b); /* 堆的起始位置 */
    38   printf("the Heap end: %p\n", (nb+16*sizeof(char))); /* 堆的结束位置 */
    39   printf("\nb and nb in Stack\n");
    40 
    41   SHW_ADDR("b", b);             /* 显示栈中数据b的位置 */
    42   
    43   SHW_ADDR("nb", nb);           /* 显示栈中数据nb的位置 */
    44   
    45   free(b);                      /* 释放申请的空间 */
    46   free(nb);                     /* 释放申请的空间 */
    47 }

    复制代码

     afunc.c

    复制代码

     1 /* afunc.c */
     2 #include <stdio.h>
     3 #define SHW_ADDR(ID, I) printf("the %s\t is at addr:%p\n", ID, &I); /* 打印地址 */
     4 void afunc(void) {
     5   static int long level = 0;    /* 静态数据存储在数据段中 */
     6   int stack_var;                /* 局部变量存储在栈区 */
     7   
     8   if(++level == 5)
     9     return;
    10   
    11   printf("stack_var%d is at: %p\n", level, &stack_var);
    12   SHW_ADDR("stack_var in stack section", stack_var);
    13   SHW_ADDR("level in data section", level);
    14   
    15   afunc();
    16 }

    复制代码

     gcc mem_add.c afunc.c进行编译然后执行输出的可执行的文件,可得到如下结果(本机有效):

    然后可以根据地址的大小来进行一个排序,并可视化:

    如果运行环境不一样,运行程序的地址与此将有差异,但是,各区域之间的相对关系不会发生变化。可以通过readelf命令来查看可执行文件的详细内容。

    readelf -a a.out

    其他知识点

    来看一个问题,下面代码的输出结果是啥?

    第一个文件code1.c

    复制代码

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 
     4 char* toStr() {
     5   char *s = "abcdefghijk";
     6   return s;
     7 }
     8 
     9 int main(void) {
    10   printf("%s\n", toStr());
    11 }

    复制代码

     第二个文件code2.c

    复制代码

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 
     4 char* toStr() {
     5   char s[] = "abcdefghijk";
     6   return s;
     7 }
     8 
     9 int main(void) {
    10   printf("%s\n", toStr());
    11 }

    复制代码

     其实我在用gcc编译第二的时候已经有warning了:

    第一个可以正常输出,而第二个要么乱码,要么是空的。

    两段代码都很简单,输出一段字符,类型不同,一个是char*字符串,一个是char[]数据。

    结果:第一个正确输出,第二个输出乱码。

    原因:在于局部变量的作用域和内存分配的问题,第一char*是指向一个常量,作用域为函数内部,被分配在程序的常量区,直到整个程序结束才被销毁,所以在程序结束前常量还是存在的。而第二个是数组存放的,作用域为函数内部,被分配在栈中,就会在函数调用结束后被释放掉,这时你再调用,肯定就错误了。

    我发现了一个新的问题,如果你把这两个文件合成一个的话,第二个其实可以打印出正确的字符的,代码如下:

    复制代码

     1 /* toStr.c演示内存分配问题哦 */
     2 
     3 #include <stdio.h>
     4 #include <stdlib.h>
     5 
     6 char* toStr1() {
     7   char *s = "abcdefghijklmn";
     8   return s;
     9 }
    10 
    11 char* toStr2() {
    12   char s[] = "abcdefghijklmn";
    13   return s;
    14 }
    15 
    16 void printStr() {
    17   int a[] = {1,2,3,4,5,6,7};
    18 }
    19 
    20 int main(void) {
    21   printf("调用toStr1()返回的结果: %s\n",toStr1());
    22   printf("调用toStr2()返回的结果: %s\n",toStr2());
    23   // printStr();
    24   exit(0);
    25 
    26 }

    复制代码

     

    不知道为啥,第二个还是可以正常打印的。但是只打印第二个,或者先打印第二个,然后在打印第一个的话,不输出乱码,倒是输出空串。

    顾名思义,局部变量就是在一个有限的范围内的变量,作用域是有限的,对于程序来说,在一个函数体内部声明的普通变量都是局部变量,局部变量会在栈上申请空间,函数结束后,申请的空间会自动释放。而全局变量是在函数体外申请的,会被存放在全局(静态区)上,知道程序结束后才会被结束,这样它的作用域就是整个程序。静态变量和全局变量的存储方式相同,在函数体内声明为static就可以使此变量像全局变量一样使用,不用担心函数结束而被释放。

    • 栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
    • 堆区(heap)—一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
    • 全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态                                  变量在相邻的另一块区域。  程序结束后由系统释放。
    • 常量区—常量字符串就是放在这里的,直到程序结束后由系统释放。上面的问题就在这里!!!
    • 代码区—存放函数体的二进制代码。

    一般编译器和操作系统实现来说,对于虚拟地址空间的最低(从0开始的几K)的一段空间是未被映射的,也就是说它在进程空间中,但没有赋予物理地址,不能被访问。这也就是对空指针的访问会导致crash的原因,因为空指针的地址是0。至于为什么预留的不是一个字节而是几K,是因为内存是分页的,至少要一页;另外几k的空间还可以用来捕捉使用空指针的情况。

    char *a 与char a[] 的区别

    char *d = "hello" 中的a是指向第一个字符‘a'的一个指针;char s[20] = "hello" 中数组名a也是执行数组第一个字符'h'的指针。现执行下列操作:strcat(d, s)。把字符串加到指针所指的字串上去,出现段错误,本质原因:*d="0123456789"存放在常量区,是无法修的。而数组是存放在栈中,是可以修改的。两者区别如下:

    读写能力:char *a = "abcd"此时"abcd"存放在常量区。通过指针只可以访问字符串常量,而不可以改变它。而char a[20] = "abcd"; 此时 "abcd"存放在栈。可以通过指针去访问和修改数组内容。

    赋值时刻:char *a = "abcd"是在编译时就确定了(因为为常量)。而char a[20] = "abcd"; 在运行时确定

    存取效率:char *a = "abcd"; 存于静态存储区。在栈上的数组比指针所指向字符串快。因此慢,而char a[20] = "abcd"存于栈上,快。
    另外注意:char a[] = "01234",虽然没有指明字符串的长度,但是此时系统已经开好了,就是大小为6-----'0' '1' '2' '3' '4' '5' '\0',(注意strlen(a)是不计'\0')

    展开全文
  • 原文地址:...一、数据结构中的栈和堆 虽说我们经常把堆栈放在一起称呼,但是不可否认的是,堆栈实际上是两种数据结构:堆和栈。 堆和栈都是一种数据项按序...

    原文地址:https://blog.csdn.net/u013007900/article/details/79338653
    参考文章:http://www.cnblogs.com/hanyonglu/archive/2011/04/12/2014212.html
    一、数据结构中的栈和堆
    虽说我们经常把堆栈放在一起称呼,但是不可否认的是,堆栈实际上是两种数据结构:堆和栈。

    堆和栈都是一种数据项按序排列的数据结构。

    栈:就像装数据的桶或箱子,它是一种具有后进先出性质的数据结构。

    堆:一种经过排序的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。

    二、内存分配中的栈和堆
    注意:一般情况下,当我们说“堆栈”的时候,其实说的是“栈”!

    一般情况下程序存放在Rom或Flash中,运行时需要拷到内存中执行,内存会分别存储不同的信息。内存中的栈区处于相对较高的地址以地址的增长方向为上的话,栈地址是向下增长的。

    栈中分配局部变量空间,堆区是向上增长的用于分配程序员申请的内存空间。另外还有静态区是分配静态变量,全局变量空间的;只读区是分配常量和程序代码空间的;以及其他一些分区。

    程序的内存分配:
    一个由C/C++编译的程序占用的内存分为以下几个部分:
    1、栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
    2、堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
    3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
    4、文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放。
    5、程序代码区—存放函数体的二进制代码。

    实例讲解

    int  a=0;   全局初始化区    
    
    char *p1;   全局未初始化区    
    int  main()    
    {    
      int  b; //栈    
      char  s[]="abc"; //栈    
      char  *p2; //栈    
      char  *p3="123456"; //123456/0在常量区,p3在栈上。    
    
      static int c =0;//全局(静态)初始化区    
      p1 =  (char  *)malloc(10);  //分配得来得10和20字节的区域就在堆区
      p2  = (char  *)malloc(20);       
      strcpy(p3,"123456"); //123456/0放在常量区,编译器可能会将它与p3所指向的"123456"  优化成一个地方。    
    } 
    

    下面详细讲解一下内存分配的几个区:

    栈:就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。

    堆:就是那些由 new 分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个 new 就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。堆可以动态地扩展和收缩。

    自由存储区,就是那些由 malloc 等分配的内存块,他和堆是十分相似的,不过它是用 free 来结束自己的生命的。

    全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的 C 语言中,全局变量又分为初始化的和未初始化的(初始化的全局变量和静态变量在一块区域,未初始化的全局变量与静态变量在相邻的另一块区域,同时未被初始化的对象存储区可以通过 void* 来访问和操纵,程序结束后由系统自行释放),在 C++ 里面没有这个区分了,他们共同占用同一块内存区。

    常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)

    void f() { int* p=new int[5]; }
    1
      这条短短的一句话就包含了堆与栈,看到 new,我们首先就应该想到,我们分配了一块堆内存,那么指针 p 呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针 p。在程序会先确定在堆中分配内存的大小,然后调用 operator new 分配内存,然后返回这块内存的首地址,放入栈中。

    那么该怎么去释放呢?

    使用 delete []p,这是为了告诉编译器:我删除的是一个数组,VC6 就会根据相应的 Cookie 信息去进行释放内存的工作。

    堆和栈究竟有什么区别?
    主要的区别由以下几点:

    1、管理方式不同;

    2、空间大小不同;

    3、能否产生碎片不同;

    4、生长方向不同;

    5、分配方式不同;

    6、分配效率不同;

    管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

    空间大小:一般来讲在 32 位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:打开工程,依次操作菜单如下:Project->Setting->Link,在 Category 中选中 Output,然后在 Reserve 中设定堆栈的最大值和 commit。注意:reserve 最小值为 4Byte;commit 是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。

    碎片问题:对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。

    生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

    分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

    分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是 C/C++ 函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

    从这里我们可以看到,堆和栈相比,由于大量 new/delete 的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP 和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

    虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

    无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候 debug 可是相当困难的 :)

    堆和栈还有几点不同:

    1、申请后系统的响应
    栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
    堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。
    另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

    2、申请大小的限制
    栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
    堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

    3、申请效率的比较:
    栈由系统自动分配,速度较快。但程序员是无法控制的。
    堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
    另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

    4、堆和栈中的存储内容
    栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
    当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
    堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

    5、堆和栈上的内存操作越界

    1>堆内存越界主要是操作的内存超过了calloc/malloc/new等在堆上分配内存函数所分配的大小,后果导致下次calloc/malloc/new的失败,malloc失败发生_int_malloc错误(引起abort)大多是这种情况引起的;

    2>栈内存越界的情况大多出现在对数组的操作上,数组下标超过了数组定义的长度,后果导致覆盖其他变量。

    小结一下
    堆和栈的区别可以用如下的比喻来看出:
    使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
    使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

    展开全文
  • 计算机体系结构和内存层次 操作系统中内存的最小访问单位是 字节 ,也就是8bit。 通常我们所说的计算机系统是32位的总线,所谓的32位总线就是说一次读写可以从内存当中读或者写32位(也就是4字节)。 ...

    计算机体系结构和内存层次

    操作系统中内存的最小访问单位是 字节 ,也就是8bit。

    通常我们所说的计算机系统是32位的总线,所谓的32位总线就是说一次读写可以从内存当中读或者写32位(也就是4字节)。

    因为一次读写是32位,所以需要地址对齐,访问的时候不能从任意地方开始。

    在CPU中可以看到高速缓存,由于指令执行和访问数据都需要从内存里读数据,如果此时有大量数据要读写而且会重复利用的话,那么在CPU中加高速缓存会使读写进行得更快。

    有了这样一个大致了解,可以来看内存层次结构。

    首先CPU在读写指令和数据的时候,如果缓存里已经有相应的内容,那么CPU直接从缓存中拿到数据,这时候速度是最快的。(写程序的时候完全感受不到L1缓存和L2缓存的存在,因为这部分是硬件控制的,不能显示使用它们。)

    如果缓存不命中,那么就必须去内存中读。如果内存还是没找到,那就去外存中读。(访问速度又有很大差别)

    虽然计算机硬件一直在飞速发展,内存容量也在不断增长,但是仍然不可能将所有用户进程和系统所需要的全部程序和数据放入主存中,所以操作系统就要对内存进行合理地划分和有效地动态分配。操作系统需要做到四个方面:抽象、保护、共享和虚拟化。

    如图,四个进程在操作系统的划分和管理下,彼此之间在逻辑上既需要相互独立,又可以相互通信。

    操作系统中采用的内存管理方式有:

    • 重定位(relocation)
    • 分段(segmentation)
    • 分页(paging)
    • 虚拟存储(virtual memory)

    目前多数系统(如 Linux)采用按需页式虚拟存储。

    地址空间和地址生成

    物理地址空间——硬件支持的地址空间,起始地址从0直到MAXsys。这个编号在存储单元角度来讲是唯一的,但是这种唯一对于我们写程序来说是不大容易的,因为我们在写程序的时候对于到底要使用哪个地址可能是不知道的。

    逻辑地址空间——在CPU运行的进程看到的地址,起始地址从0知道MAXprog。对应的是可执行文件的那段空间。

    对于我们用高级语言编写的程序,经过如下过程生成它的逻辑地址:

    此时的逻辑地址生成时机的不同会有不同的限制。

    • 编译时:已经假设起始地址已知,如果起始地址改变,必须重新编译。例如以前的手机,程序是写死的,不能自己安装应用,只能用手机里编译好的。
    • 加载时:如编译时起始位置未知,编译器需生成可重定位的代码。例如现在的智能手机,可以下载很多APP。
    • 执行时:执行时代码可移动,但需地址转换(映射)硬件支持。这种情况出现在使用虚拟存储的系统,

    逻辑地址生成后,接着就是生成物理地址,过程如下:

    因为内存有着诸多限制,所以也有着对应的检查机制。检查过程如图:

    连续地址分配

    生成地址后接着分配地址,连续内存分配是给进程分配一块不小于指定大小的连续的物理内存区域。地址分配从两个角度考虑:如何去找你要用的空间分区;如何处理不能利用的小的空闲分区。

    这里不能利用的小的空闲分区我们统称为 内存碎片 ,内存碎片分为三类:

    • 内存碎片:有的还可以用,有的无论如何都用不起来了。
    • 外部碎片:分配单元之间的未被使用内存
    • 内部碎片:分配单元内部的未被使用内存(你只占500字节,但是不得不分配512字节)

    这里我们就不得不有所取舍地选择不同的分配策略,常用的有三种:最先匹配(First-fit)、最佳匹配(Best-fit)、最差匹配(Worst-fit)。

    最先匹配(First Fit Allocation)策略

    原理 & 实现:

    • 空闲分区列表按地址顺序排序
    • 分配过程时,搜索一个合适的分区
    • 释放分区时,检查是否可与临近的空闲分区合并

    优点:

    • 简单(这也算优点的话)
    • 在高地址空间有大块的空闲分区

    缺点:

    • 外部碎片
    • 分配大块时较慢

    最佳匹配(Best Fit Allocation)策略

    原理 & 实现:

    • 空闲分区列表按照大小排序
    • 分配时,查找一个合适的分区
    • 释放时,查找并且合并临近的空闲分区(如果找到)

    优点:

    • 大部分分配的尺寸较小时,效果很好(可避免大的空闲分区被拆分)
    • 可减小外部碎片的大小

    缺点:

    • 外部碎片
    • 释放分区缓慢
    • 容易产生很多无用的小碎片

    最差匹配(Worst Fit Allocation)策略

    原理 & 实现:

    • 空闲分区列表按由大到小排序
    • 分配时,选最大的分区
    • 释放时,检查是否可与临近的空闲分区合并,进行可能的合并,并调整空闲分区列表顺序

    优点:

    • 中等大小的分配较多时,效果最好
    • 避免出现太多的小碎片

    缺点:

    • 释放分区较慢
    • 外部碎片
    • 容易破坏大的空闲分区,因此后续难以分配大的分区

    碎片整理

    通过调整进程占用的分区位置来减少或避免分区碎片

    1. 紧凑(compaction) 通过移动分配给进程的内存分区,以合并外部碎片。紧凑的条件是:所有的应用程序可动态重定位。

    2. 分区对换(Swapping in/out) 通过抢占并回收处于等待状态进程的分区,以增大可用内存空间。

    伙伴系统(Buddy System)

    伙伴系统即,整个可分配的分区大小2^U,需要的分区大小为2^(U-1) < s ≤ 2^U 时,把整个块分配给该进程。

    伙伴系统的实现参见: Buddy memory allocation



      内存是计算机中最重要的资源之一,通常情况下,物理内存无法容纳下所有的进程。虽然物理内存的增长现在达到了N个GB,但比物理内存增长还快的是程序,所以无论物理内存如何增长,都赶不上程序增长的速度,所以操作系统如何有效的管理内存便显得尤为重要。本文讲述操作系统对于内存的管理的过去和现在,以及一些页替换的算法的介绍。

     

    对于进程的简单介绍

        在开始之前,首先从操作系统的角度简单介绍一下进程。进程是占有资源的最小单位,这个资源当然包括内存。在现代操作系统中,每个进程所能访问的内存是互相独立的(一些交换区除外)。而进程中的线程所以共享进程所分配的内存空间。

        在操作系统的角度来看,进程=程序+数据+PCB(进程控制块)。这个概念略微有点抽象,我通过一个类比来说吧:比如,你正在厨房做饭,你一边看着菜谱一边按照菜谱将原料做成菜,就在这时,你儿子进来告诉你他擦破了腿,此时你停下手中的工作,将菜谱反扣过来,然后找来急救书按照书中的内容给你儿子贴上创口贴,贴完后你继续回去打开菜谱,然后继续做饭。在这个过程中,你就好比CPU,菜谱就好比程序,而做菜的原料就好比数据。你按照程序指令加工数据,而急救工作好比一个更高优先级的进程,中断了你当前做饭的工作,然后你将菜谱反扣过来(保护现场),转而去处理高优先级的进程,处理完毕后你继续从刚才的页读菜谱(恢复现场),然后继续执行做菜这个进程。

        在简单介绍完进程的概念后,我们来转入内存。

     

    没有内存抽象的年代

        在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存。比如当执行如下指令时:

    mov reg1,1000

        这条指令会毫无想象力的将物理地址1000中的内容赋值给寄存器。不难想象,这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。

        没有内存抽象对于内存的管理通常非常简单,除去操作系统所用的内存之外,全部给用户程序使用。或是在内存中多留一片区域给驱动程序使用,如图1所示。

        1

        图1.没有内存抽象时,对内存的使用

      

        第一种情况操作系统存于RAM中,放在内存的低地址,第二种情况操作系统存在于ROM中,存在内存的高地址,一般老式的手机操作系统是这么设计的。

        如果这种情况下,想要操作系统可以执行多进程的话,唯一的解决方案就是和硬盘搞交换,当一个进程执行到一定程度时,整个存入硬盘,转而执行其它进程,到需要执行这个进程时,再从硬盘中取回内存,只要同一时间内存中只有一个进程就行,这也就是所谓的交换(Swapping)技术。但这种技术由于还是直接操作物理内存,依然有可能引起进程的崩溃。

        所以,通常来说,这种内存操作往往只存在于一些洗衣机,微波炉的芯片中,因为不可能有第二个进程去征用内存。

     

    内存抽象

        在现代的操作系统中,同一时间运行多个进程是再正常不过的了。为了解决直接操作内存带来的各种问题,引入的地址空间(Address Space),这允许每个进程拥有自己的地址。这还需要硬件上存在两个寄存器,基址寄存器(base register)和界址寄存器(limit register),第一个寄存器保存进程的开始地址,第二个寄存器保存上界,防止内存溢出。在内存抽象的情况下,当执行

    mov reg1,20

        这时,实际操作的物理地址并不是20,而是根据基址和偏移量算出实际的物理地址进程操作,此时操作的实际地址可能是:

    mov reg1,16245

     

        在这种情况下,任何操作虚拟地址的操作都会被转换为操作物理地址。而每一个进程所拥有的内存地址是完全不同的,因此也使得多进程成为可能。

        但此时还有一个问题,通常来说,内存大小不可能容纳下所有并发执行的进程。因此,交换(Swapping)技术应运而生。这个交换和前面所讲的交换大同小异,只是现在讲的交换在多进程条件下。交换的基本思想是,将闲置的进程交换出内存,暂存在硬盘中,待执行时再交换回内存,比如下面一个例子,当程序一开始时,只有进程A,逐渐有了进程B和C,此时来了进程D,但内存中没有足够的空间给进程D,因此将进程B交换出内存,分给进程D。如图2所示。

        2

        图2.交换技术

     

        通过图2,我们还发现一个问题,进程D和C之间的空间由于太小无法另任何进程使用,这也就是所谓的外部碎片。一种方法是通过紧凑技术(Memory Compaction)解决,通过移动进程在内存中的地址,使得这些外部碎片空间被填满。还有一些讨巧的方法,比如内存整理软件,原理是申请一块超大的内存,将所有进程置换出内存,然后再释放这块内存,从而使得从新加载进程,使得外部碎片被消除。这也是为什么运行完内存整理会狂读硬盘的原因。另外,使用紧凑技术会非常消耗CPU资源,一个2G的CPU没10ns可以处理4byte,因此多一个2G的内存进行一次紧凑可能需要好几秒的CPU时间。

        上面的理论都是基于进程所占的内存空间是固定的这个假设,但实际情况下,进程往往会动态增长,因此创建进程时分配的内存就是个问题了,如果分配多了,会产生内部碎片,浪费了内存,而分配少了会造成内存溢出。一个解决方法是在进程创建的时候,比进程实际需要的多分配一点内存空间用于进程的增长。一种是直接多分配一点内存空间用于进程在内存中的增长,另一种是将增长区分为数据段和栈(用于存放返回地址和局部变量),如图3所示。

        3

        图3.创建进程时预留空间用于增长

     

        当预留的空间不够满足增长时,操作系统首先会看相邻的内存是否空闲,如果空闲则自动分配,如果不空闲,就将整个进程移到足够容纳增长的空间内存中,如果不存在这样的内存空间,则会将闲置的进程置换出去。

         当允许进程动态增长时,操作系统必须对内存进行更有效的管理,操作系统使用如下两种方法之一来得知内存的使用情况,分别为1)位图(bitmap) 2)链表

         使用位图,将内存划为多个大小相等的块,比如一个32K的内存1K一块可以划为32块,则需要32位(4字节)来表示其使用情况,使用位图将已经使用的块标为1,位使用的标为0.而使用链表,则将内存按使用或未使用分为多个段进行链接,这个概念如图4所示。

        4

         图4.位图和链表表示内存的使用情况

     

         使用链表中的P表示进程,从0-2是进程,H表示空闲,从3-4表示是空闲。

         使用位图表示内存简单明了,但一个问题是当分配内存时必须在内存中搜索大量的连续0的空间,这是十分消耗资源的操作。相比之下,使用链表进行此操作将会更胜一筹。还有一些操作系统会使用双向链表,因为当进程销毁时,邻接的往往是空内存或是另外的进程。使用双向链表使得链表之间的融合变得更加容易。

        还有,当利用链表管理内存的情况下,创建进程时分配什么样的空闲空间也是个问题。通常情况下有如下几种算法来对进程创建时的空间进行分配。

    •      临近适应算法(Next fit)---从当前位置开始,搜索第一个能满足进程要求的内存空间
    •      最佳适应算法(Best fit)---搜索整个链表,找到能满足进程要求最小内存的内存空间
    •      最大适应算法(Wrost fit)---找到当前内存中最大的空闲空间
    •      首次适应算法(First fit) ---从链表的第一个开始,找到第一个能满足进程要求的内存空间

     

    虚拟内存(Virtual Memory)

        虚拟内存是现代操作系统普遍使用的一种技术。前面所讲的抽象满足了多进程的要求,但很多情况下,现有内存无法满足仅仅一个大进程的内存要求(比如很多游戏,都是10G+的级别)。在早期的操作系统曾使用覆盖(overlays)来解决这个问题,将一个程序分为多个块,基本思想是先将块0加入内存,块0执行完后,将块1加入内存。依次往复,这个解决方案最大的问题是需要程序员去程序进行分块,这是一个费时费力让人痛苦不堪的过程。后来这个解决方案的修正版就是虚拟内存。

        虚拟内存的基本思想是,每个进程有用独立的逻辑地址空间,内存被分为大小相等的多个块,称为(Page).每个页都是一段连续的地址。对于进程来看,逻辑上貌似有很多内存空间,其中一部分对应物理内存上的一块(称为页框,通常页和页框大小相等),还有一些没加载在内存中的对应在硬盘上,如图5所示。

        5

        图5.虚拟内存和物理内存以及磁盘的映射关系

     

        由图5可以看出,虚拟内存实际上可以比物理内存大。当访问虚拟内存时,会访问MMU(内存管理单元)去匹配对应的物理地址(比如图5的0,1,2),而如果虚拟内存的页并不存在于物理内存中(如图5的3,4),会产生缺页中断,从磁盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将磁盘中的页换出。

        而虚拟内存和物理内存的匹配是通过页表实现,页表存在MMU中,页表中每个项通常为32位,既4byte,除了存储虚拟地址和页框地址之外,还会存储一些标志位,比如是否缺页,是否修改过,写保护等。可以把MMU想象成一个接收虚拟地址项返回物理地址的方法。

        因为页表中每个条目是4字节,现在的32位操作系统虚拟地址空间会是2的32次方,即使每页分为4K,也需要2的20次方*4字节=4M的空间,为每个进程建立一个4M的页表并不明智。因此在页表的概念上进行推广,产生二级页表,二级页表每个对应4M的虚拟地址,而一级页表去索引这些二级页表,因此32位的系统需要1024个二级页表,虽然页表条目没有减少,但内存中可以仅仅存放需要使用的二级页表和一级页表,大大减少了内存的使用。

     

    页面替换算法

        因为在计算机系统中,读取少量数据硬盘通常需要几毫秒,而内存中仅仅需要几纳秒。一条CPU指令也通常是几纳秒,如果在执行CPU指令时,产生几次缺页中断,那性能可想而知,因此尽量减少从硬盘的读取无疑是大大的提升了性能。而前面知道,物理内存是极其有限的,当虚拟内存所求的页不在物理内存中时,将需要将物理内存中的页替换出去,选择哪些页替换出去就显得尤为重要,如果算法不好将未来需要使用的页替换出去,则以后使用时还需要替换进来,这无疑是降低效率的,让我们来看几种页面替换算法。

       

    最佳置换算法(Optimal Page Replacement Algorithm)

         最佳置换算法是将未来最久不使用的页替换出去,这听起来很简单,但是无法实现。但是这种算法可以作为衡量其它算法的基准。

     

    最近不常使用算法(Not Recently Used Replacement Algorithm)

         这种算法给每个页一个标志位,R表示最近被访问过,M表示被修改过。定期对R进行清零。这个算法的思路是首先淘汰那些未被访问过R=0的页,其次是被访问过R=1,未被修改过M=0的页,最后是R=1,M=1的页。

     

    先进先出页面置换算法(First-In,First-Out Page Replacement Algorithm)

        这种算法的思想是淘汰在内存中最久的页,这种算法的性能接近于随机淘汰。并不好。

     

    改进型FIFO算法(Second Chance Page Replacement Algorithm)

        这种算法是在FIFO的基础上,为了避免置换出经常使用的页,增加一个标志位R,如果最近使用过将R置1,当页将会淘汰时,如果R为1,则不淘汰页,将R置0.而那些R=0的页将被淘汰时,直接淘汰。这种算法避免了经常被使用的页被淘汰。

     

    时钟替换算法(Clock Page Replacement Algorithm)

        虽然改进型FIFO算法避免置换出常用的页,但由于需要经常移动页,效率并不高。因此在改进型FIFO算法的基础上,将队列首位相连形成一个环路,当缺页中断产生时,从当前位置开始找R=0的页,而所经过的R=1的页被置0,并不需要移动页。如图6所示。

        6

        图6.时钟置换算法

     

    最久未使用算法(LRU Page Replacement Algorithm)

        LRU算法的思路是淘汰最近最长未使用的页。这种算法性能比较好,但实现起来比较困难。

     

    下面表是上面几种算法的简单比较:

    算法 描述
    最佳置换算法 无法实现,最为测试基准使用
    最近不常使用算法 和LRU性能差不多
    先进先出算法 有可能会置换出经常使用的页
    改进型先进先出算法 和先进先出相比有很大提升
    最久未使用算法 性能非常好,但实现起来比较困难
    时钟置换算法 非常实用的算法

     

        上面几种算法或多或少有一些局部性原理的思想。局部性原理分为时间和空间上的局部性

        1.时间上,最近被访问的页在不久的将来还会被访问。

        2.空间上,内存中被访问的页周围的页也很可能被访问。



    展开全文
  • 1.原理说明  Linux内核中采 用了一种同时适用于32位和64位系统的内 存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系 统中,用到了四级页表,如图2-1所示四级页表分别为:  * 页全局目录(Page ...

      1.原理说明

      Linux内核中采 用了一种同时适用于32位和64位系统的内 存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系 统中,用到了四级页表,如图2-1所示常见的Linux内核中内存分配 - leon - 我的奋斗四级页表分别为:

      * 页全局目录(Page Global Directory)

      * 页上级目录(Page Upper Directory)

      * 页中间目录(Page Middle Directory)

      * 页表(Page Table)

      页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址,每一个页表项指 向一个页框常见的Linux内核中内存分配 - leon - 我的奋斗Linux中采用4KB大小的 页框作为标准的内存分配单元常见的Linux内核中内存分配 - leon - 我的奋斗

      多级分页目录结构

      1.1.伙伴系统算法

      在实际应用中,经常需要分配一组连续的页框,而频繁地申请和释放不同大小的连续页框,必然导致在已分配页框的内存块中分散了许多小块的 空闲页框常见的Linux内核中内存分配 - leon - 我的奋斗这样,即使这些页框是空闲的,其他需要分配连续页框的应用也很难得到满足常见的Linux内核中内存分配 - leon - 我的奋斗

      为了避免出现这种情况,Linux内核中引入了伙伴系统算法(buddy system)常见的Linux内核中内存分配 - leon - 我的奋斗把所有的空闲页框分组为11个 块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块常见的Linux内核中内存分配 - leon - 我的奋斗最大可以申请1024个连 续页框,对应4MB大小的连续内存常见的Linux内核中内存分配 - leon - 我的奋斗每个页框块的第一个页框的物理地址是该块大小的整数倍常见的Linux内核中内存分配 - leon - 我的奋斗

      假设要申请一个256个页框的块,先从256个页框的链表中查找空闲块,如果没有,就去512个 页框的链表中找,找到了则将页框块分为2个256个 页框的块,一个分配给应用,另外一个移到256个页框的链表中常见的Linux内核中内存分配 - leon - 我的奋斗如果512个页框的链表中仍没有空闲块,继续向1024个页 框的链表查找,如果仍然没有,则返回错误常见的Linux内核中内存分配 - leon - 我的奋斗

      页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块常见的Linux内核中内存分配 - leon - 我的奋斗

      1.2.slab分 配器

      slab分配器源于 Solaris 2.4 的 分配算法,工作于物理内存页框分配器之上,管理特定大小对象的缓存,进行快速而高效的内存分配常见的Linux内核中内存分配 - leon - 我的奋斗

      slab分配器为每种使用的内核对象建立单独的缓冲区常见的Linux内核中内存分配 - leon - 我的奋斗Linux 内核已经采用了伙伴系统管理物理内存页框,因此 slab分配器直接工作于伙伴系 统之上常见的Linux内核中内存分配 - leon - 我的奋斗每种缓冲区由多个 slab 组成,每个 slab就是一组连续的物理内存页框,被划分成了固定数目的对象常见的Linux内核中内存分配 - leon - 我的奋斗根据对象大小的不同,缺省情况下一个 slab 最多可以由 1024个页框构成常见的Linux内核中内存分配 - leon - 我的奋斗出于对齐 等其它方面的要求,slab 中分配给对象的内存可能大于用户要求的对象实际大小,这会造成一定的 内存浪费常见的Linux内核中内存分配 - leon - 我的奋斗

     

    2.常用内存分配函数

      2.1.__get_free_pages

      unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)

      __get_free_pages函数是最原始的内存分配方式,直接从伙伴系统中获取原始页框,返回值为第一个页框的起始地址常见的Linux内核中内存分配 - leon - 我的奋斗__get_free_pages在实现上只是封装了alloc_pages函 数,从代码分析,alloc_pages函数会分配长度为1<<order的 连续页框块常见的Linux内核中内存分配 - leon - 我的奋斗order参数的最大值由include/Linux/Mmzone.h文 件中的MAX_ORDER宏决定,在默认的2.6.18内 核版本中,该宏定义为10常见的Linux内核中内存分配 - leon - 我的奋斗也就是说在理论上__get_free_pages函 数一次最多能申请1<<10 * 4KB也就是4MB的 连续物理内存常见的Linux内核中内存分配 - leon - 我的奋斗但是在实际应用中,很可能因为不存在这么大量的连续空闲页框而导致分配失败常见的Linux内核中内存分配 - leon - 我的奋斗测试中,order为10时分配成功,order为11则返回错误常见的Linux内核中内存分配 - leon - 我的奋斗

      2.2.kmem_cache_alloc

      struct kmem_cache *kmem_cache_create(const char *name, size_t size,

      size_t align, unsigned long flags,

      void (*ctor)(void*, struct kmem_cache *, unsigned long),

      void (*dtor)(void*, struct kmem_cache *, unsigned long))

      void *kmem_cache_alloc(struct kmem_cache *c, gfp_t flags)

      kmem_cache_create/ kmem_cache_alloc是基于slab分配器的一种内存分配方式,适用于反复分配释放同一大小内存块的场合常见的Linux内核中内存分配 - leon - 我的奋斗首先用kmem_cache_create创建一个高速缓存区域,然后用kmem_cache_alloc从 该高速缓存区域中获取新的内存块常见的Linux内核中内存分配 - leon - 我的奋斗 kmem_cache_alloc一次能分配的最大内存由mm/slab.c文件中的MAX_OBJ_ORDER宏 定义,在默认的2.6.18内核版本中,该宏定义为5, 于是一次最多能申请1<<5 * 4KB也就是128KB的 连续物理内存常见的Linux内核中内存分配 - leon - 我的奋斗分析内核源码发现,kmem_cache_create函数的size参数大于128KB时会调用BUG()常见的Linux内核中内存分配 - leon - 我的奋斗测试结果验证了分析结果,用kmem_cache_create分 配超过128KB的内存时使内核崩溃常见的Linux内核中内存分配 - leon - 我的奋斗

      2.3.kmalloc

      void *kmalloc(size_t size, gfp_t flags)

      kmalloc是内核中最常用的一种内存分配方式,它通过调用kmem_cache_alloc函 数来实现常见的Linux内核中内存分配 - leon - 我的奋斗kmalloc一次最多能申请的内存大小由include/Linux/Kmalloc_size.h的 内容来决定,在默认的2.6.18内核版本中,kmalloc一 次最多能申请大小为131702B也就是128KB字 节的连续物理内存常见的Linux内核中内存分配 - leon - 我的奋斗测试结果表明,如果试图用kmalloc函数分配大于128KB的内存,编译不能通过常见的Linux内核中内存分配 - leon - 我的奋斗

      2.4.vmalloc

      void *vmalloc(unsigned long size)

      前面几种内存分配方式都是物理连续的,能保证较低的平均访问时间常见的Linux内核中内存分配 - leon - 我的奋斗但是在某些场合中,对内存区的请求不是很频繁,较高的内存访问时间也 可以接受,这是就可以分配一段线性连续,物理不连续的地址,带来的好处是一次可以分配较大块的内存常见的Linux内核中内存分配 - leon - 我的奋斗图3-1表 示的是vmalloc分配的内存使用的地址范围常见的Linux内核中内存分配 - leon - 我的奋斗vmalloc对 一次能分配的内存大小没有明确限制常见的Linux内核中内存分配 - leon - 我的奋斗出于性能考虑,应谨慎使用vmalloc函数常见的Linux内核中内存分配 - leon - 我的奋斗在测试过程中, 最大能一次分配1GB的空间常见的Linux内核中内存分配 - leon - 我的奋斗

      Linux内核部分内存分布

      2.5.dma_alloc_coherent

      void *dma_alloc_coherent(struct device *dev, size_t size,

      ma_addr_t *dma_handle, gfp_t gfp)

      DMA是一种硬件机制,允许外围设备和主存之间直接传输IO数据,而不需要CPU的参与,使用DMA机制能大幅提高与设备通信的 吞吐量常见的Linux内核中内存分配 - leon - 我的奋斗DMA操作中,涉及到CPU高速缓 存和对应的内存数据一致性的问题,必须保证两者的数据一致,在x86_64体系结构中,硬件已经很 好的解决了这个问题, dma_alloc_coherent和__get_free_pages函数实现差别不大,前者实际是调用__alloc_pages函 数来分配内存,因此一次分配内存的大小限制和后者一样常见的Linux内核中内存分配 - leon - 我的奋斗__get_free_pages分配的内 存同样可以用于DMA操作常见的Linux内核中内存分配 - leon - 我的奋斗测试结果证明,dma_alloc_coherent函 数一次能分配的最大内存也为4M常见的Linux内核中内存分配 - leon - 我的奋斗

      2.6.ioremap

      void * ioremap (unsigned long offset, unsigned long size)

      ioremap是一种更直接的内存“分配”方式,使用时直接指定物理起始地址和需要分配内存的大小,然后将该段 物理地址映射到内核地址空间常见的Linux内核中内存分配 - leon - 我的奋斗ioremap用到的物理地址空间都是事先确定的,和上面的几种内存 分配方式并不太一样,并不是分配一段新的物理内存常见的Linux内核中内存分配 - leon - 我的奋斗ioremap多用于设备驱动,可以让CPU直接访问外部设备的IO空间常见的Linux内核中内存分配 - leon - 我的奋斗ioremap能映射的内存由原有的物理内存空间决定,所以没有进行测试常见的Linux内核中内存分配 - leon - 我的奋斗

      2.7.Boot Memory

      如果要分配大量的连续物理内存,上述的分配函数都不能满足,就只能用比较特殊的方式,在Linux内 核引导阶段来预留部分内存常见的Linux内核中内存分配 - leon - 我的奋斗

      2.7.1.在内核引导时分配内存

      void* alloc_bootmem(unsigned long size)

      可以在Linux内核引导过程中绕过伙伴系统来分配大块内存常见的Linux内核中内存分配 - leon - 我的奋斗使用方法是在Linux内核引导时,调用mem_init函数之前 用alloc_bootmem函数申请指定大小的内存常见的Linux内核中内存分配 - leon - 我的奋斗如果需要在其他地方调用这块内存,可以将alloc_bootmem返回的内存首地址通过EXPORT_SYMBOL导 出,然后就可以使用这块内存了常见的Linux内核中内存分配 - leon - 我的奋斗这种内存分配方式的缺点是,申请内存的代码必须在链接到内核中的代码里才能使用,因此必须重新编译内核,而且内存管理系统 看不到这部分内存,需要用户自行管理常见的Linux内核中内存分配 - leon - 我的奋斗测试结果表明,重新编译内核后重启,能够访问引导时分配的内存块常见的Linux内核中内存分配 - leon - 我的奋斗

      2.7.2.通过内核引导参数预留顶部内存

      在Linux内核引导时,传入参数“mem=size”保留顶部的内存区间常见的Linux内核中内存分配 - leon - 我的奋斗比如系统有256MB内 存,参数“mem=248M”会预留顶部的8MB内存,进入系统后可以调用ioremap(0xF800000,0x800000)来申请这段内存常见的Linux内核中内存分配 - leon - 我的奋斗

      3.几种分配函数的比较

     

    1.kmalloc

       kmalloc内存分配和malloc相似,除非被阻塞否则他执行的速度非常快,而且不对获得空间清零.

    说明:在用kmalloc申请函数后,要清零用memset()函数对申请的内存进行清零。

     

    2.kamlloc函数原型:

    #include

    Void *kmalloc(size_t size, int flags);

    (1)第一个参数是要分配的块的大小

    (2)第二个参数是分配标志(flags),他提供了多种kmalloc的行为。

    (3)第三个最常用的GFP_KERNEL;
     

    A.表示内存分配(最终总是调用get_free_pages来实现实际的分配;这就是GFP前缀的由来)是代表运行在内核空间的进程执行的。使用GFP_KERNEL容许kmalloc在分配空闲内存时候如果内存不足容许把当前进程睡眠以等待。因此这时分配函数必须是可重入的。如果在进程上下文之外如:中断处理程序、tasklet以及内核定时器中这种情况下current进程不该睡眠,驱动程序该使用GFP_ATOMIC.

    B.GFP_ATOMIC

    用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠.

    C.GFP_KERNEL

    内核内存的正常分配. 可能睡眠.

    D.GFP_USER

    用来为用户空间页来分配内存; 它可能睡眠.

    E.GFP_HIGHUSER

    如同 GFP_USER, 但是从高端内存分配, 如果有. 高端内存在下一个子节描述.

    F.GFP_NOFS,GFP_NOIO

    这个标志功能如同 GFP_KERNEL, 但是它们增加限制到内核能做的来满足请求. 一个 GFP_NOFS 分配不允许进行任何文件系统调用, 而 GFP_NOIO 根本不允许任何 I/O 初始化. 它们主要地用在文件系统和虚拟内存代码, 那里允许一个分配睡眠, 但是递归的文件系统调用会是一个坏注意.

    上面列出的这些分配标志可以是下列标志的相或来作为参数, 这些标志改变这些分配如何进行:

    __GFP_DMA

    这个标志要求分配在能够 DMA 的内存区. 确切的含义是平台依赖的并且在下面章节来解释.

    __GFP_HIGHMEM

    这个标志指示分配的内存可以位于高端内存.

    __GFP_COLD

    正常地, 内存分配器尽力返回\"缓冲热\"的页 -- 可能在处理器缓冲中找到的页. 相反, 这个标志请求一个\"冷\"页, 它在一段时间没被使用. 它对分配页作 DMA 读是有用的, 此时在处理器缓冲中出现是无用的. 一个完整的对如何分配 DMA 缓存的讨论看\"直接内存存取\"一节在第 1 章.

    __GFP_NOWARN

    这个很少用到的标志阻止内核来发出警告(使用 printk ), 当一个分配无法满足.

    __GFP_HIGH

    这个标志标识了一个高优先级请求, 它被允许来消耗甚至被内核保留给紧急状况的最后的内存页.

    __GFP_REPEAT

    __GFP_NOFAIL

    __GFP_NORETRY

    这些标志修改分配器如何动作, 当它有困难满足一个分配. __GFP_REPEAT 意思是\" 更尽力些尝试\" 通过重复尝试 -- 但是分配可能仍然失败. __GFP_NOFAIL 标志告诉分配器不要失败; 它尽最大努力来满足要求. 使用 __GFP_NOFAIL 是强烈不推荐的; 可能从不会有有效的理由在一个设备驱动中使用它. 最后, __GFP_NORETRY 告知分配器立即放弃如果得不到请求的内存.

    Ø  内存区段

         __GFP_DMA和__GFP_HIGHMEM的使用与平台相关,Linux把内存分成3个区段:可用于DMA的内存、常规内存、以及高端内存。X86平台上ISA设备DMA区段是内存的前16MB,而PCI设备无此限制。

    内存区后面的机制在 mm/page_alloc.c 中实现, 而内存区的初始化在平台特定的文件中, 常常在 arch 目录树的 mm/init.c。

    3.kamlloc的使用方法:

         Linux 处理内存分配通过创建一套固定大小的内存对象池. 分配请求被这样来处理, 进入一个持有足够大的对象的池子并且将整个内存块递交给请求者. 驱动开发者应当记住的一件事情是, 内核只能分配某些预定义的, 固定大小的字节数组.

    如果你请求一个任意数量内存, 你可能得到稍微多于你请求的, 至多是 2 倍数量. 同样, 程序员应当记住 kmalloc 能够处理的最小分配是 32 或者 64 字节, 依赖系统的体系所使用的页大小. kmalloc 能够分配的内存块的大小有一个上限. 这个限制随着体系和内核配置选项而变化. 如果你的代码是要完全可移植, 它不能指望可以分配任何大于 128 KB. 如果你需要多于几个 KB, 但是, 有个比 kmalloc 更好的方法来获得内存。在设备驱动程序或者内核模块中动态开辟内存,不是用malloc,而是kmalloc ,vmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,vfree,或free_pages. kmalloc函数返回的是虚拟地址(线性地址). kmalloc特殊之处在于它分配的内存是物理上连续的,这对于要进行DMA的设备十分重要. 而用vmalloc分配的内存只是线性地址连续,物理地址不一定连续,不能直接用于DMA.

      注意kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。

      内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。

      另外,很多硬件需要一块比较大的连续内存用作DMA传送。这块内存需要一直驻留在内存,不能被交换到文件中去。但是kmalloc最多只能开辟大小为32XPAGE_SIZE的内存,一般的PAGE_SIZE=4kB,也就是128kB的大小的内存。

    3.kmalloc和vmalloc的区别

    • vmalloc()与 kmalloc()都可用于分配内存

    • kmalloc()分配的内存处于3GB~high_memory之 间,这段内核空间与物理内存的映射一一对应

    •vmalloc()分配的内存在 VMALLOC_START~4GB之间,这段非连续内 存区映射到物理内存也可能是非连续的

    • 在内核空间中调用kmalloc()分配连续物理空间,而调用vmalloc()分配非物理连续空间。

    • 把kmalloc()所分配内核空间中的地址称为内核逻辑地址

    • 把vmalloc()分配的内核空间中的地址称 为内核虚拟地址

    • vmalloc()在分配过程中须更新内核页表

    总结:

    1.kmalloc和vmalloc分配的是内核的内存,malloc分配的是用户的内存

    2.kmalloc保证分配的内存在物理上是连续的, kmalloc()分配的内存在0xBFFFFFFF-0xFFFFFFFF以上的内存中,driver一般是用它来完成对DS的分配,更适合于类似设备驱动的程序来使用;

    3.vmalloc保证的是在虚拟地址空间上的连续,vmalloc()则是位于物理地址非连续,虚地址连续区,起始位置由VMALLOL_START来决定,一般作为交换区、模块的分配。

    3.kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大(因为vmalloc还可以处理交换空间)。

    4.内存只有在要被DMA访问的时候才需要物理上连续,vmalloc比kmalloc要慢

    5.vmalloc使用的正确场合是分配一大块,连续的,只在软件中存在的,用于缓冲的内存区域。不能在微处理器之外使用。

    6.vmalloc 中调用了 kmalloc (GFP—KERNEL),因此也不能应用于原子上下文。

    7.kmalloc和 kfree管理内核段内分配的内存,这是真实地址已知的实际物理内存块。

    8.vmalloc对应于vfree,分配连续的虚拟内存,但是物理上不一定连续。
    9.kmalloc分配内存是基于slab,因此slab的一些特性包括着色,对齐等都具备,性能较好。物理地址和逻辑地址都是连续的

    展开全文
  • C/C++内存分配管理

    2018-08-13 14:57:23
    内存分配及管理 1.内存分配方式 在C++中内存分为5个区,分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。 堆:堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言...
  • 内存分配的三种方式

    2018-08-14 15:04:23
    要回答这个问题,我们必须先要理解C++的内存管理方式,需要站在内存四区的角度去审视一下代码,这样整个理解了内存分配的方式,回答的时候就只需要注意一些细节了。写一个例子然后画一下内存示意图。 int getmem...
  • 连续内存分配 首次适配:空闲分区以地址递增的次序链接。分配内存时顺序查找,找到大小能满足要求的第一个空闲分区。 最优适配:空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区。 最坏适配:空闲...
  • 内存分配机制的发展过程: 第一阶段——程序直接操作物理内存。 某台计算机总的内存大小是128M,现在同时运行两个程序A和B,A需占用内存10M,B需占用内存100。计算机在给程序分配内存时会采取这样的方法:先将...
  • 内存的四种分配方式

    2018-03-23 22:53:26
    由于堆是程序员管理的,如果管理不当会导致内存泄露的问题,内存泄露指已经分配内存空间无法被系统回收也无法被继续使用。解决这个问题,C++可以使用智能指针对象去指向分配内存,在对象析构时释放内存防止内存...
  • 目录 1.malloc()2.free()3.calloc()4.realloc()5....所开辟的内存是在栈中开辟的固定大小的 ,如a是4字节 ,数组b是40字节 ,并且数组在申明时必须指定其长度 , 因为数组的内存是在编译时分配好的 . 如果我们想在...
  • **C/C++程序内存分配**一、预备知识—程序的内存分配一个由c/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中...
  • 本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Java。这类文章网上有很多,但大多比较零碎。本文从认知过程角度出发,将带给读者一个系统的介绍。 进入正题前首先要知道的是Java程序运行在JVM...
  • 内存分配方式有两种,连续内存分配方式和离散内存分配方式。不同的分配方式又有不同的分配算法。 内存分配算法,其实就是:有一大块空闲的资源,如何合理地分配资源?内存分配的思想可以用到很多其他的领域。比如...
  • C语言中手把手教你动态内存分配动态内存分配常见的内存分配的错误先上一个内存分配的思维导图:便于联想想象,理解: 首先我们介绍一下内存分配的方式:1:在静态存储区域中进行分配 内存在程序编译的时候就已经分配...
  • 图解Golang的内存分配

    2019-12-19 21:56:27
    一般程序的内存分配 在讲Golang的内存分配之前,让我们先来看看一般程序的内存分布情况: 以上是程序内存的逻辑分类情况。 我们再来看看一般程序的内存的真实(真实逻辑)图: Go的内存分配核心思想 Go是内置运行时的...
  • 在涉及到内存分配时,我们一般都要考虑到两种内存分配方式,一种是动态内存分配,另一种是静态内存分配,我们该怎么理解这两者的区别呢? 在我看来,静态内存分配和动态内存分配比较典型的例子就是数组和链表,数组...
  • 文章目录一、连续内存分配1、内存碎片的问题(1)外部碎片(2)内部碎片2、连续内存分配算法(1)首次适配(2)最优适配(3)最差适配3、碎片整理方法4、连续内存分配的缺点二、非连续内存分配1、非连续分配的优点2...
  • 一、为什么需要动态内存分配? 在C++程序中,所有内存需求都是在程序执行之前通过定义所需的变量来确定的。 但是可能存在程序的内存需求只能在运行时确定的情况。 例如,当需要的内存取决于用户输入。 在这些情况下...
  • 对于大多数开发者而言,系统的内存分配就是一个黑盒子,就是几个API的调用。有你就给我,没有我就想别的办法。实际深入进去时,才发现这个领域里也是百家争鸣,非常热闹。有操作系统层面的内存分配器(Memory ...
1 2 3 4 5 ... 20
收藏数 1,168,619
精华内容 467,447
关键字:

内存分配