LinuxUNIX系统编程手册——(七)内存分配

7.1 在堆上分配内存

进程可以通过增加堆的大小来分配内存,所谓堆是一段长度可变的连续虚拟内存,始于进程的未初始化数据段末尾,随着内存的分配和释放而增减。通常将堆的当前内存边界称为“program break”。

在这里插入图片描述

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

改变堆的大小(即分配或释放内存),其实就像命令内核改变进程的program break 位置一样简单。最初,program break正好位于未初始化数据段末尾之后(与&end的位置相同)

在 program break 的位置抬升后,程序可以访问新分配区域内的任何内存地址,而此时物理内存页尚未分配。内核会在进程首次试图访问这些虚拟内存地址时自动分配新的物理内存页。

系统调用 brk()会将 program break 设置为参数 end_data_segment 所指定的位置。由于虚拟内存以页为单位进行分配,end_data_segment 实际会四舍五入到下一个内存页的边界处。

调用 sbrk()将program break 在原有地址上增加从参数 increment 传入的大小。用于声明 increment 的 intptr_t 类型属于整数数据类型。若调用成功,sbrk()返回前一个program break 的地址。换言之,如果program break 增加,那么返回值是指向这块新分配内存起始位置的指针。

int brk(void *end_data_segment);			/* 成功返回0,失败返回-1 */
void sbrk(intptr_t increment);				/* 成功返回前一个program break 的地址,失败返回(void *)-1 */

当试图将 program break 设置为一个低于其初始值(即低于&end)的位置时,有可能会导致无法预知的行为,例如,当程序试图访问的数据位于初始化或未初始化数据段中当前尚不存在的部分时,就会引发分段内存访问错误(segmentation fault)(SIGSEGV 信号)。program break 可以设定的精确上限取决于一系列因素,这包括进程中对数据段大小的资源限制,以及内存映射、共享内存段、共享库的位置。

**调用 sbrk(0)将返回 program break 的当前位置,对其不做改变。**在意图跟踪堆的大小,或是监视内存分配函数包的行为时,可能会用到这一用法。

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

malloc( )函数在堆上分配参数 size 字节大小的内存,并返回指向新分配内存起始位置处的指针,其所分配的内存未经初始化。

void *malloc(size_t size);			/* 成功返回分配内存的指针,失败返回NULL并设置 errno 以返回错误信息。 */

由于 malloc()的返回类型为 void*,因而可以将其赋给任意类型的 C 指针。malloc()返回内存块所采用的字节对齐方式,总是适宜于高效访问任何类型的 C 语言数据结构。在大多数硬件架构上,这实际意味着 malloc 是基于 8 字节或 16 字节边界来分配内存的。若无法分配内存(或许是因为已经抵达 program break 所能达到的地址上限),则 malloc()返回 NULL,并设置 errno 以返回错误信息。虽然分配内存失败的可能性很小,但所有对 malloc()以及后续提及的相关函数的调用都应对返回值进行错误检查。

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

void free(void *ptr);

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

  • 被释放的内存块通常会位于堆的中间,而非堆的顶部,因而降低 porgram break 是不可能的。
  • 它最大限度地减少了程序必须执行的 sbrk()调用次数。(正如 3.1 节指出的,系统调用的开销虽小,却也颇为可观。)
  • 在大多数情况下,降低 program break 的位置不会对那些分配大量内存的程序有多少帮助,因为它们通常倾向于持有已分配内存或是反复释放和重新分配内存,而非释放所有内存后再持续运行一段时间。

7.1.3 程序示例

下面的程序说明了 free()函数对 program break 的影响。该程序在分配了多块内存后,根据(可选)命令行参数来释放其中的部分或全部。

#include "tlpi_hdr.h"

#define MAX_ALLOCS 1000000

int
main(int argc, char *argv[])
{
    char *ptr[MAX_ALLOCS];
    int freeStep, freeMin, freeMax, blockSize, numAllocs, j;

    printf("\n");

    if (argc < 3 || strcmp(argv[1], "--help") == 0)
        usageErr("%s num-allocs block-size [step [min [max]]]\n", argv[0]);

    numAllocs = getInt(argv[1], GN_GT_0, "num-allocs");			/* 获取分配内存的数量argv[1] */
    if (numAllocs > MAX_ALLOCS)
        cmdLineErr("num-allocs > %d\n", MAX_ALLOCS);

    blockSize = getInt(argv[2], GN_GT_0 | GN_ANY_BASE, "block-size");		/* 获取分配内存块的大小argv[2] */

    freeStep = (argc > 3) ? getInt(argv[3], GN_GT_0, "step") : 1;		/* 获取释放内存块的循环步长argv[3] */
    freeMin =  (argc > 4) ? getInt(argv[4], GN_GT_0, "min") : 1;		/* 获取释放内存块的开始下限argv[4] */
    freeMax =  (argc > 5) ? getInt(argv[5], GN_GT_0, "max") : numAllocs;		/* 获取释放内存块的结束上限argv[5] */

    /* 保证分配内存的数量小于释放内存块的数量上限 */
    if (freeMax > numAllocs)
        cmdLineErr("free-max > num-allocs\n");

    printf("Initial program break:          %10p\n", sbrk(0));

    printf("Allocating %d*%d bytes\n", numAllocs, blockSize);
    for (j = 0; j < numAllocs; j++) {
        ptr[j] = (char *)malloc(blockSize);
        if (ptr[j] == NULL)
            errExit("malloc");
    }

    printf("Program break is now:           %10p\n", sbrk(0));

    printf("Freeing blocks from %d to %d in steps of %d\n",
                freeMin, freeMax, freeStep);
    /* 按释放内存块的上下限并以循环步长freeStep开始释放内存块 */
    for (j = freeMin - 1; j < freeMax; j += freeStep)
        free(ptr[j]);

    printf("After free(), program break is: %10p\n", sbrk(0));

    exit(EXIT_SUCCESS);
}
vainx@DESKTOP-0DN0PNJ:~/wsl-code/tlpi-book/memalloc$ ./free_and_sbrk 1000 10240 2

Initial program break:          0x56306f0b4000
Allocating 1000*10240 bytes
Program break is now:           0x56306fa7c000
Freeing blocks from 1 to 1000 in steps of 2
After free(), program break is: 0x56306fa7c000
vainx@DESKTOP-0DN0PNJ:~/wsl-code/tlpi-book/memalloc$ ./free_and_sbrk 1000 10240 1 1 999

Initial program break:          0x555f18128000
Allocating 1000*10240 bytes
Program break is now:           0x555f18af0000
Freeing blocks from 1 to 999 in steps of 1
After free(), program break is: 0x555f18af0000

但是,如果在堆顶部释放完整的一组连续内存块,会观察到program break 从峰值上降下来,这表明free()使用了 sbrk()来降低program break。在这里,命令行释放了已分配内存的最后500 个内存块。

vainx@DESKTOP-0DN0PNJ:~/wsl-code/tlpi-book/memalloc$ ./free_and_sbrk 1000 10240 1 500 1000

Initial program break:          0x55aea8fa0000
Allocating 1000*10240 bytes
Program break is now:           0x55aea9968000
Freeing blocks from 500 to 1000 in steps of 1
After free(), program break is: 0x55aea9481000

在这种情况下,free()函数的 glibc 实现会在释放内存时将相邻的空闲内存块合并为一整块更大的内存(这样做是为了避免在空闲内存列表中包含大量的小块内存碎片,这些碎片会因空间太小而难以满足后续的 malloc()请求),因而也有能力识别出堆顶部的整个空闲区域。

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

**当进程终止时,其占用的所有内存都会返还给操作系统,这包括在堆内存中由 malloc 函数包内一系列函数所分配的内存。**基于内存的这一自动释放机制,对于那些分配了内存并在进程终止前持续使用的程序而言,通常会省略对 free()的调用。这在程序中分配了多块内存的情况下可能会特别有用,因为加入多次对free()的调用不但会消耗大量的CPU 时间,而且可能会使代码趋于复杂。

虽然依靠终止进程来自动释放内存对大多数程序来说是可以接受的,但基于以下几个原因,最好能够在程序中显式释放所有的已分配内存。

  • 显式调用 free()能使程序在未来修改时更具可读性和可维护性。
  • **如果使用 malloc 调试库(如下所述)来查找程序的内存泄漏问题,那么会将任何未经显式释放处理的内存报告为内存泄漏。**这会使发现真正内存泄漏的工作复杂化。

7.1.5 malloc()和 free()的实现

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

**如果在空闲内存列表中根本找不到足够大的空闲内存块,那么 malloc()会调用 sbrk()以分配更多的内存。**为减少对 sbrk()的调用次数,malloc()并未只是严格按所需字节数来分配内存,而是以更大幅度(以虚拟内存页大小的数倍)来增加program break,并将超出部分置于空闲内存列表。

至于 free()函数的实现则更为有趣。当 free()将内存块置于空闲列表之上时,是如何知晓内存块大小的?这是通过一个小技巧来实现的。当 malloc()分配内存块时,会额外分配几个字节来存放记录这块内存大小的整数值。该整数位于内存块的起始处,而实际返回给调用者的内存地址恰好位于这一长度记录字节之后,如图

在这里插入图片描述

当将内存块置于空闲内存列表(双向链表)时,free()会使用内存块本身的空间来存放链表指针,将自身添加到列表中,如图 所示。

在这里插入图片描述

随着对内存不断地释放和重新分配,空闲列表中的空闲内存会和已分配的在用内存混杂在一起,如图

在这里插入图片描述

应该认识到,C 语言允许程序创建指向堆中任意位置的指针,并修改其指向的数据,包括由 free()和 malloc()函数维护的内存块长度、指向前一空闲块和后一空闲块的指针。辅之以之前的描述,一旦推究起隐晦难解的编程缺陷来,这无疑形同掉进了火药桶。例如,假设经由一个错误指针,程序无意间增加了冠于一块已分配内存的长度值,并随即释放这块内存,free()因之会在空闲列表中记录下这块长度失真的内存。随后,malloc()也许会重新分配这块内存,从而导致如下场景:程序的两个指针分别指向两块它认为互不相干的已分配内存,但实际上这两块内存却相互重叠。至于其他的出错情况则数不胜数。

要避免这类错误,应该遵守以下规则。

  • 分配一块内存后,应当小心谨慎,不要改变这块内存范围外的任何内容。错误的指针运算,或者循环更新内存块内容时出现的“off-by-one”(一字之偏)错误,都有可能导致这一情况。
  • 释放同一块已分配内存超过一次是错误的。Linux 上的 glibc 库经常报出分段错误(SIGSEGV 信号)。这是好事,因为它提醒我们犯下了一个编程错误。然而,当两次释放同一块内存时,更常见的后果是导致不可预知的行为。
  • 若非经由 malloc 函数包中函数所返回的指针,绝不能在调用 free()函数时使用。
  • 在编写需要长时间运行的程序(例如,shell 或网络守护进程)时,出于各种目的,如果需要反复分配内存,那么应当确保释放所有已使用完毕的内存。如若不然,堆将稳步增长,直至抵达可用虚拟内存的上限,在此之后分配内存的任何尝试都将以失败告终。这种情况被称之为**“内存泄漏”**。

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

函数 calloc()用于给一组相同对象分配内存。参数 mumitems 指定分配对象的数量,size 指定每个对象的大小。与 malloc()不同,calloc()会将已分配的内存初始化为 0。

void *calloc(size_t numitems, size_t size);			/* 成功返回分配内存起始处的指针,错误返回NULL */

下面是 calloc()的一个使用范例:

struct 
{
    /* Some field definitions */
} myStruct;
struct myStruct *p;

p = calloc(1000, sizeof(struct myStruct));
if(p == NULL)
    errExit("calloc");

realloc()函数用来调整(通常是增加)一块内存的大小,而此块内存应是之前由 malloc 包中函数所分配的。参数 ptr 是指向需要调整大小的内存块的指针。参数 size 指定所需调整大小的期望值。

void *reallo(void *ptr, size_t size);		/* 成功返回指向大小调整后内存块的指针,与调用前的指针相比,
二者指向的位置可能不同;失败返回NULL,对 ptr 指针指向的内存块则原封不动 */

使用 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)
{
    /* 错误处理 */
}
else
    ptr = nptr;

并没有把 realloc()的返回值直接赋给 ptr,因为一旦调用 realloc()失败,那么 ptr 会被置为 NULL,从而无法访问现有内存块。

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

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

void *memalign(size_t boundary, size_t size);		/* 成功返回分配内存的指针,错误返回NULL */ 

函数 memalign()分配 size 个字节的内存,起始地址是参数 boundary 的整数倍,而 boundary必须是 2 的整数次幂。函数返回已分配内存的地址。

函数 memalign()并非在所有 UNIX 实现上都存在。大多数提供 memalign()的其他 UNIX 实现都要求引用<stdlib.h>而非<malloc.h>以获得函数声明。SUSv3 并未纳入 memalign(),而是规范了一个类似函数,名为 posix_memalign()。该函数由标准委员会于近期创制,只是出现在了少数 UNIX 实现上。

int posix_memalign(void **memptr, size_t alignment, size_t size);		/* 成功返回0,失败返回错误号 */

函数 posix_memalign()与 memalign()存在以下两方面的不同。

  • 已分配的内存地址通过参数 memptr 返回。
  • 内存与 alignment 参数的整数倍对齐,alignment 必须是 sizeof(void*)(在大多数硬件架构上是 4 或 8 个字节)与 2 的整数次幂两者间的乘积。

如果 SizeOf(void *)为 4,就可以使用 posix_memalign()分配 65536 字节的内存,并与 4096字节的边界对齐,代码如下:

int s;
void *memptr;

s = posix_memalign(&memptr, 1024 * sizeof(void *), 65536);
if(s != 0)
    /* 处理错误 */

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

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

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

void *alloca(size_t size);		/* 返回已分配内存块的指针 */

参数size 指定在堆栈上分配的字节数。

**不需要(实际上也绝不能)调用 free()来释放由 alloca()分配的内存。**同样,也不可能调用realloc()来调整由 alloca()分配的内存大小。

若调用alloca()造成堆栈溢出,则程序的行为无法预知,特别是在没有收到一个NULL返回值通知错误的情况下。(事实上,在此情况下,可能会收到一个SIGSEGV 信号。

请注意,不能在一个函数的参数列表中调用 alloca(),如下所示:

func(x, alloca(size), z);			/* 错误的 */

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

void *y;
y = alloca(size);
func(x,y,z);

使用 alloca()来分配内存相对于 malloc()有一定优势。其中之一是,alloca()分配内存的速度要快于 malloc(),因为编译器将 alloca()作为内联代码处理,并通过直接调整堆栈指针来实现。此外,alloca()也不需要维护空闲内存块列表。另一个优点在于,由 alloca()分配的内存随栈帧的移除而自动释放,亦即当调用 alloca 的
函数返回之时。所以如此,是因为函数返回时所执行的代码会重置栈指针寄存器,使其指向前一帧的末尾(即,假设堆栈向下增长,则指向恰好位于当前栈帧起始处之上的地址)。由于在函数的所有返回路径中都无需确保去释放所有的已分配内存,一些函数的编码也变得简单得多。

  • 23
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值