zoukankan      html  css  js  c++  java
  • TLPI读书笔记第7章-内存分配

    许多系统程序需要为动态数据结构(例如,链表和二叉树)分配额外内存,此类数据结构的大小由运行时所获取的信息决定。本章将介绍用于在堆或堆栈上分配内存的函数

    7.1 在堆上分配内存

    进程可以通过增加堆的大小来分配内存,所谓堆是一段长度可变的连续虚拟内存,始于进程的未初始化数据段末尾,随着内存的分配和释放而增减(见图 6-1)。通常将堆的当前内存边界称为“ program break”。 稍后将介绍 C 语言程序分配内存所惯用的 malloc 函数族,但首先还要从 malloc 函数族所基于的 brk()和 sbrk()开始谈起。

    7.1.1 调整 program break: brk()和 sbrk()

    改变堆的大小(即分配或释放内存),其实就像命令内核改变进程的 program break 位置一样简单。最初, program break 正好位于未初始化数据段末尾之后 在 program break 的位置抬升后,程序可以访问新分配区域内的任何内存地址,而此时物理内存页尚未分配。内核会在进程首次试图访问这些虚拟内存地址时自动分配新的物理内存页。 传统的 UNIX 系统提供了两个操纵 program break 的系统调用: brk()和 sbrk(),在 Linux 中依然可用。虽然代码中很少直接使用这些系统调用,但了解它们有助于弄清内存分配的工作过程。

    #include<unistd.h>
    int brk(void *end_data_segment);
    void sbrk(intptr_t increment);

    系统调用 brk()会将 program break 设置为参数 end_data_segment 所指定的位置。由于虚拟内存以页为单位进行分配, end_data_segment 实际会四舍五入到下一个内存页的边界处。 当试图将 program break 设置为一个低于其初始值(即低于&end)的位置时,有可能会导致无法预知的行为,例如,当程序试图访问的数据位于初始化或未初始化数据段中当前尚不存在的部分时,就会引发分段内存访问错误( segmentation fault)。

    program break 可以设定的精确上限取决于一系列因素,这包括进程中对数据段大小的资源限制,以及内存映射、共享内存段、共享库的位置。 调用 sbrk()将 program break 在原有地址上增加从参数 increment 传入的大小。(在 Linux 中,sbrk()是在 brk()基础上实现的一个库函数。 )用于声明 increment 的 intptr_t 类型属于整数数据类型。若调用成功, sbrk()返回前一个 program break 的地址。换言之,如果 program break 增加,那么返回值是指向这块新分配内存起始位置的指针。 调用 sbrk(0)将返回 program break 的当前位置,对其不做改变。在意图跟踪堆的大小,或是监视内存分配函数包的行为时,可能会用到这一用法。

    7.1.2 在堆上分配内存: malloc()和 free()

    一般情况下, C 程序使用 malloc 函数族在堆上分配和释放内存。较之 brk()和 sbrk(),这些函数具备不少优点,如下所示。

    1.属于 C 语言标准的一部分。

    2.更易于在多线程程序中使用。

    3.接口简单,允许分配小块内存。

    4.允许随意释放内存块,它们被维护于一张空闲内存列表中,在后续内存分配调用时循环使用。 malloc( )函数在堆上分配参数 size 字节大小的内存, 并返回指向新分配内存起始位置处的指针,其所分配的内存未经初始化。

    #include<stdlib.h>
    void *malloc(size_t size);
    /*若无法分配内存,则 malloc()返回 NULL*/

    由于 malloc()的返回类型为 void*,因而可以将其赋给任意类型的 C 指针。 malloc()返回内存块所采用的字节对齐方式,总是适宜于高效访问任何类型的 C 语言数据结构。在大多数硬件架构上,这实际意味着 malloc 是基于 8 字节或 16 字节边界来分配内存的。

    若无法分配内存(或许是因为已经抵达 program break 所能达到的地址上限),则 malloc()返回 NULL, 并设置 errno 以返回错误信息。 虽然分配内存失败的可能性很小, 但所有对 malloc()以及后续提及的相关函数的调用都应对返回值进行错误检查

    free()函数释放 ptr 参数所指向的内存块,该参数应该是之前由 malloc(),或者本章后续描述的其他堆内存分配函数之一所返回的地址。

    #include<stdlib.h>
    void free(void *ptr);

    一般情况下, free()并不降低 program break 的位置,而是将这块内存填加到空闲内存列表中,供后续的malloc()函数循环使用。这么做是出于以下几个原因。

    1.被释放的内存块通常会位于堆的中间,而非堆的顶部,因而降低 porgram break 是不可能的。

    2.它最大限度地减少了程序必须执行的 sbrk()调用次数。

    3.在大多数情况下,降低 program break 的位置不会对那些分配大量内存的程序有多少帮助,因为它们通常倾向于持有已分配内存或是反复释放和重新分配内存,而非释放所有内存后再持续运行一段时间。 如果传给 free()的是一个空指针,那么函数将什么都不做。 在调用 free()后对参数 ptr 的任何使用,例如将其再次传递给 free(),将产生错误,并可能导致不可预知的结果

    程序示例

    程序清单 7-1 中的程序说明了 free()函数对 program break 的影响。 该程序在分配了多块内存后,根据(可选)命令行参数来释放其中的部分或全部。 前两个命令行参数指定了分配内存块的数量和大小。

    第三个命令行参数指定了释放内存块的循环步长。如果是 1(这也是省略此参数时的默认值),那么程序将释放每块已分配的内存,如果为 2,那么每隔一块释放一块已分配内存,以此类推。第四个和第五个命令行参数指定需要释放的内存块范围。如果省略这两个参数,那么将(以第三个命令行参数所指定的步长)释放全部范围内的已分配内存。

    用下面的命令行运行程序清单 7-1 的程序,将会分配 1000 个内存块,且每隔一个内存块释放一个内存块。输出结果显示,释放所有内存块后, program break 的位置仍然与分配所有内存块后的水平相当。 下面的命令行要求除了最后一块内存块,释放所有已分配的内存块。再一次, program break保持在了“高水位线”。 但是,如果在堆顶部释放完整的一组连续内存块,会观察到 program break 从峰值上降下来,这表明 free()使用了 sbrk()来降低 program break。在这里,命令行释放了已分配内存的最后 500 个内存块。 在这种情况下, free()函数的 glibc 实现会在释放内存时将相邻的空闲内存块合并为一整块更大的内存(这样做是为了避免在空闲内存列表中包含大量的小块内存碎片,这些碎片会因空间太小而难以满足后续的 malloc()请求),因而也有能力识别出堆顶部的整个空闲区域

    调用 free()还是不调用 free()

    当进程终止时,其占用的所有内存都会返还给操作系统,这包括在堆内存中由 malloc 函数包内一系列函数所分配的内存。 基于内存的这一自动释放机制, 对于那些分配了内存并在进程终止前持续使用的程序而言,通常会省略对 free()的调用。这在程序中分配了多块内存的情况下可能会特别有用,因为加入多次对 free()的调用不但会消耗大量的 CPU 时间,而且可能会使代码趋于复杂。 虽然依靠终止进程来自动释放内存对大多数程序来说是可以接受的, 但基于以下几个原因,最好能够在程序中显式释放所有的已分配内存。 1.显式调用 free()能使程序在未来修改时更具可读性和可维护性。 2.如果使用 malloc 调试库来查找程序的内存泄漏问题,那么会将任何未经显式释放处理的内存报告为内存泄漏。这会使发现真正内存泄漏的工作复杂化。

    7.1.3 malloc()和 free()的实现

    尽管 malloc()和 free()所提供的内存分配接口比之 brk()和 sbrk()要容易许多,但在使用时仍然容易犯下各种编程错误。理解 malloc()和 free()的实现,将使我们洞悉产生这些错误的原因以及如何才能避免此类错误。

    malloc()的实现很简单。它首先会扫描之前由 free()所释放的空闲内存块列表,以求找到尺寸大于或等于要求的一块空闲内存。 (取决于具体实现,采用的扫描策略会有所不同。例如,first-fit 或 best-fito。 )如果这一内存块的尺寸正好与要求相当,就把它直接返回给调用者。

    如果是一块较大的内存,那么将对其进行分割,在将一块大小相当的内存返回给调用者的同时,把较小的那块空闲内存块保留在空闲列表中。 如果在空闲内存列表中根本找不到足够大的空闲内存块,那么 malloc()会调用 sbrk()以分配更多的内存。为减少对 sbrk()的调用次数, malloc()并未只是严格按所需字节数来分配内存,而是以更大幅度(以虚拟内存页大小的数倍)来增加 program break,并将超出部分置于空闲内存列表。 至于 free()函数的实现则更为有趣。当 free()将内存块置于空闲列表之上时,是如何知晓内存块大小的?这是通过一个小技巧来实现的。当 malloc()分配内存块时,会额外分配几个字节来存放记录这块内存大小的整数值。该整数位于内存块的起始处,而实际返回给调用者的内存地址恰好位于这一长度记录字节之后,如图 7-1 所示

    应该认识到, C 语言允许程序创建指向堆中任意位置的指针,并修改其指向的数据,包括由 free()和 malloc()函数维护的内存块长度、指向前一空闲块和后一空闲块的指针。辅之以之前的描述,一旦推究起隐晦难解的编程缺陷来,这无疑形同掉进了火药桶。

    例如,假设经由一个错误指针, 程序无意间增加了冠于一块已分配内存的长度值, 并随即释放这块内存, free() 因之会在空闲列表中记录下这块长度失真的内存。随后, malloc()也许会重新分配这块内存,从而导致如下场景:程序的两个指针分别指向两块它认为互不相干的已分配内存,但实际上这两块内存却相互重叠。至于其他的出错情况则数不胜数。 要避免这类错误,应该遵守以下规则。

    1.分配一块内存后,应当小心谨慎,不要改变这块内存范围外的任何内容。错误的指针运算,或者循环更新内存块内容时出现的一字之偏错误,都有可能导致这一情况。

    2.释放同一块已分配内存超过一次是错误的。 Linux 上的 glibc 库经常报出分段错误( SIGSEGV 信号)。这是好事,因为它提醒我们犯下了一个编程错误。然而,当两次 释放同一块内存时,更常见的后果是导致不可预知的行为。

    3.若非经由 malloc 函数包中函数所返回的指针,绝不能在调用 free()函数时使用。

    4.在编写需要长时间运行的程序(例如, shell 或网络守护进程)时,出于各种目的,如果需要反复分配内存,那么应当确保释放所有已使用完毕的内存。如若不然,堆将稳步增长,直至抵达可用虚拟内存的上限,在此之后分配内存的任何尝试都将以失败告终。这种情况被称之为“内存泄漏”。

    malloc 调试的工具和库

    如果不遵循上述准则,可能会在代码中引入既难以理解又难以重现的缺陷。而使用 glibc提供的 malloc 调试工具或者任何一款 malloc 调试库,都会显著降低发现这些缺陷的难度,这也是设计它们的目的所在。 以下是 glibc 提供的 malloc 调试工具的部分功能。

    1.mtrace()和 muntrace()函数分别在程序中打开和关闭对内存分配调用进行跟踪的功能。这些函数要与环境变量 MALLOC_TRACE 搭配使用,该变量定义了写入跟踪信息的文件名。在被调用时, mtrace()会检查是否定义了该文件,又是否可以打开文件并写入。如果一切正常,那么会在文件里跟踪和记录所有对 malloc 函数包中函数的调用。由于生成文件不易于理解,还提供有一个脚本( mtrace)用于分析文件,并生成易于理解的汇总报告。出于安全原因,设置用户 ID 和设置组 ID 的程序会忽略对 mtrace()的调用。

    2.mcheck()和 mprobe()函数允许程序对已分配内存块进行一致性检查。例如,当程序试图在已分配内存之外进行写操作时,它们将捕获这个错误。这些函数提供的功能和下述 malloc 调试库有重叠之处。使用这些函数的程序,必须使用 cc-lmcheck 选项与mcheck 库链接。

    3.MALLOC_CHECK_环境变量(注意结尾处的下划线)提供了类似于 mcheck()和mprobe()函数的功能。 (两者之间的一个显著区别在于使用: MALLOC_CHECK_无需对程序进行修改和重新编译。 )通过为此变量设置不同的整数值,可以控制程序对内存分配错误的响应方式。可能的设置有:

    0,意即忽略错误; 1,意即在标准错误输出中打印诊断错误; 2,意即调用 abort()来终止程序。

    并非所有的内存分配和释放错误都是由 MALLOC_CHECK_检测出的,它所发现的只是常见错误。但是,这种技术快速、易用,较之于 malloc 调试库具有较低的运行时开销。出于安全原因,设置用户 ID 和设置组 ID 的程序将忽略 MALLOC_CHECK_设置。 关于以上所有功能更为详细的信息可以参考 glibc 手册。 而就 malloc 调试库而言,其提供了和标准 malloc 函数包相同的 API,但附加了捕获内存分配错误的功能。

    要使用调试库,需要在编译时链接调试库,而非标准 C 函数库的 malloc 函数包。由于调试库通常会降低运行速度,增加内存消耗,或是两者兼而有之,应当仅在调试时使用, 而在正式发布产品时链接标准库的 malloc 包。 这些库分别是: Electric Fence、dmalloc、Valgrind、Insure++

    控制和监测 malloc 函数包

    glibc 手册介绍了一系列非标准函数,可用于监测和控制 malloc 包中函数的内存分配,其 中包括如下几个函数。 1.函数 mallopt()能修改各项参数,以控制 malloc()所采用的算法。例如,此类参数之一就指定了在调用 sbrk()函数进行堆收缩之前,在空闲列表尾部必须保有的可释放内存空间的最小值。另一参数则规定了从堆中分配的内存块大小的上限,超出上限的内存块则使用 mmap()系统调用来分配。 2.mallinfo()函数返回一个结构,其中包含由 malloc()分配内存的各种统计数据。众多 UNIX 实现提供各种版本的 mallopt()和 mallinfo()。然而,这些函数所提供的接口却随实现而不同,因而也无法移植。

    7.1.4 在堆上分配内存的其他方法

    除了 malloc(), C 函数库还提供了一系列在堆上分配内存的其他函数,在这里将逐一介绍。

    用 calloc()和 realloc()分配内存

    函数 calloc()用于给一组相同对象分配内存

    #include<stdlib.h>
    void calloc(size_t numitems,size_t size);
    /*参数mumitems指定分配对象的数量, size指定每个对象的大小。在分配了适当大小的内存块后, calloc()返回指向这块内存起始处的指针(如果无法分配内存, 则返回 NULL)。 */

    与 malloc()不同, calloc()会将已分配的内存初始化为 0 下面是 calloc()的一个使用范例:

    struct {...} myStruct;
    struct myStruct *p;
    p=calloc(1000,sizeof(struct myStruct));
    if(p==NULL)
       errExit("calloc");

    realloc()函数用来调整(通常是增加)一块内存的大小,而此块内存应是之前由 malloc 包中 函数所分配的。

    #include<stdlib.h>
    void *realloc(void *ptr,size_t size);

    参数 ptr 是指向需要调整大小的内存块的指针。参数 size 指定所需调整大小的期望值。如果成功, realloc()返回指向大小调整后内存块的指针。与调用前的指针相比,二者指向 的位置可能不同。如果发生错误, realloc()返回 NULL,对 ptr 指针指向的内存块则原封不动 若 realloc()增加了已分配内存块的大小,则不会对额外分配的字节进行初始化。

    使用 calloc()或 realloc()分配的内存应使用 free()来释放。

    调用 realloc(ptr,0)等效于在 free(ptr)之后调用 malloc(0)。若 ptr 为 NULL,则 realloc(NULL,size)相当于调用 malloc(size)。 通常情况下,当增大已分配内存时, realloc()会试图去合并在空闲列表中紧随其后且大小满足要求的内存块。若原内存块位于堆的顶部,那么 realloc()将对堆空间进行扩展。 如果这块内存位于堆的中部,且紧邻其后的空闲内存空间大小不足, realloc()会分配一块新内存,并将原有数据复制到新内存块中。

    最后这种情况最为常见,还会占用大量 CPU资源。一般情况下,应尽量避免调用 realloc()。 既然 realloc()可能会移动内存,对这块内存的后续引用就必须使用 realloc()的返回指针。可以用 realloc()来重新定位由变量 ptr 指向的内存块,代码如下:

    nptr=realloc(ptr,newsize);
    if(nptr==NULL){
       /*hander error*/
    }else{
       ptr=nptr;
    }

    本例并没有把 realloc()的返回值直接赋给 ptr,因为一旦调用 realloc()失败,那么 ptr 会被置为 NULL,从而无法访问现有内存块。 由于 realloc()可能会移动内存块,任何指向该内存块内部的指针在调用 realloc()之后都可能不再可用。仅有一种内存块内的位置引用方法依然有效,即以指向此块内存起始处的指针再加上一个偏移量来进行定位,这将在 48.6 节中详细讨论。

    分配对齐的内存: memalign()和 posix_memalign()

    设计函数 memalign()和 posix_memalign()的目的在于分配内存时,起始地址要与 2 的整数次幂边界对齐,该特征对于某些应用非常有用。

    #include<malloc.h>
    void *memalign(size_t boundary,size_t size);

    函数 memalign()分配 size 个字节的内存, 起始地址是参数 boundary 的整数倍, 而 boundary必须是 2 的整数次幂。函数返回已分配内存的地址。 函数 memalign()并非在所有 UNIX 实现上都存在。 大多数提供 memalign()的其他 UNIX 实现都要求引<stdlib.h>而非<malloc.h>以获得函数声明。 SUSv3 并未纳入 memalign(),而是规范了一个类似函数,名为 posix_memalign()。该函数由标准委员会于近期创制,只是出现在了少数 UNIX 实现上。

    #include<stdlib.h>
    int posix_memalign(void **memptr,size_t alignment,size_t size);

    函数 posix_memalign()与 memalign()存在以下两方面的不同。 1.已分配的内存地址通过参数 memptr 返回。 2.内存与 alignment 参数的整数倍对齐, alignment 必须是 sizeof( void*)(在大多数硬件架构上是 4 或 8 个字节)与 2 的整数次幂两者间的乘积。 还要注意该函数与众不同的返回值,成功返回0,出错时不是返回-1,而是直接返回一个错误号(即通常在 errno 中返回的正整数)。 如果 SizeOf(void *)为 4,就可以使用 posix_memalign()分配 65536 字节的内存,并与 4096字节的边界对齐,代码如下:

    int s;
    void *memptr;
    s=posix_memalign(&memptr,1024*sizeof(void *),65536);
    if(s!=0)
       /*hander error*/

    由 memalign()或 posix_memalign()分配的内存块应该调用 free()来释放。

    7.2 在堆栈上分配内存: alloca()

    和 malloc 函数包中的函数功能一样, alloca()也可以动态分配内存,不过不是从堆上分配内存,而是通过增加栈帧的大小从堆栈上分配。根据定义,当前调用函数的栈帧位于堆栈的顶部,故而这种方法是可行的。因此,帧的上方存在扩展空间,只需修改堆栈指针值即可。

    #include<alloca.h>
    void *alloca(size_t size);

    参数 size 指定在堆栈上分配的字节数。函数 alloca()将指向已分配内存块的指针作为其返回值。不需要(实际上也绝不能)调用 free()来释放由 alloca()分配的内存。同样,也不可能调用realloc()来调整由 alloca()分配的内存大小。 旧版本的 glibc 和其他一些 UNIX 实现(主要是 BSD 的衍生版本),要获取 alloca()声明需引入<stdlib.h>而非<alloca.h>。 若调用 alloca()造成堆栈溢出,则程序的行为无法预知,特别是在没有收到一个 NULL 返回值通知错误的情况下。 请注意,不能在一个函数的参数列表中调用 alloca(),如下所示:

    func(x,alloca(size),z)

    这会使 alloca()分配的堆栈空间出现在当前函数参数的空间内(函数参数都位于栈帧内的固定位置)。相反,必须采用这样的代码:

    void *y;
    y=alloca(size);
    func(x,y,z);/*func返回时,alloca分配的空间自动释放*/

    使用 alloca()来分配内存相对于 malloc()有一定优势。其中之一是, alloca()分配内存的速度要快于 malloc(),因为编译器将 alloca()作为内联代码处理,并通过直接调整堆栈指针来实现。此外, alloca()也不需要维护空闲内存块列表。

    另一个优点在于,由 alloca()分配的内存随栈帧的移除而自动释放,亦即当调用 alloca 的函数返回之时。之所以如此,是因为函数返回时所执行的代码会重置栈指针寄存器,使其指向前一帧的末尾(即,假设堆栈向下增长,则指向恰好位于当前栈帧起始处之上的地址)。

    由于在函数的所有返回路径中都无需确保去释放所有的已分配内存,一些函数的编码也变得简单得多。 在信号处理程序中调用 longjmp()(以执行非局部跳转时, alloca()的作用尤其突出。此时,在“起跳”函数和“落地”函数之间的函数中,如果使用了 malloc()来分配内存,要想避免内存泄漏就极其困难,甚至是不可能的。

    与之相反,alloca()完全可以避免这一问题,因为堆栈是由这些调用展开的,所以已分配的内存会被自动释放。

  • 相关阅读:
    【黑金动力社区】【原创博文集锦】《Verilog HDL那些事儿》导读
    [黑金原创教程][连载]关于《iBoard 电子学堂》的学习及进阶方式
    发布《iBoard 电子学堂》DEMO代码
    Cyclone III中LVDS的设计
    [黑金原创教程][连载][iBoard 电子学堂][第八卷 设计任意波发生器]第三篇 直接数字合成(DDS)原理
    专业级FPGA视频图像处理开发板发布
    [黑金原创教程][连载][iBoard 电子学堂][第〇卷 电子基础]第二篇 电路图与印刷电路板
    [黑金原创教程][连载][iBoard 电子学堂][第八卷 设计任意波发生器]第一篇 iBoard 任意波发生器简介
    [黑金原创教程][连载][iBoard 电子学堂][第〇卷 电子基础]第一篇 认识电子元器件
    ucgui 在《iBoard 电子学堂》上的移植
  • 原文地址:https://www.cnblogs.com/wangbin2188/p/14666541.html
Copyright © 2011-2022 走看看