Linux系统编程:内存分配

内存分配

参考书籍《Linux/UNIX系统编程手册》

示例程序源码:https://github.com/nanshuaibo/tlpi/tree/main

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

image-20240426161122152

调整program :brk()和sbrk()

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

image-20240426152211743

系统调用 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 增加,那么返回值是指向这块新分配内存起始位置的指针

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

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

  • 属于 C 语言标准的一部分。
  • 更易于在多线程程序中使用。
  • 接口简单,允许分配小块内存。
  • 允许随意释放内存块,它们被维护于一张空闲内存列表中,在后续内存分配调用时循环使用。

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

image-20240426152708036

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

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

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

image-20240426153132820

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

  • 被释放的内存块通常会位于堆的中间,而非堆的顶部,因而不会降低 porgram break 。
  • 可以减少程序必须执行的 sbrk()调用次数。
  • 在大多数情况下,降低 program break 的位置不会对那些分配大量内存的程序有多少帮助,因为它们通常倾向于持有已分配内存或是反复释放和重新分配内存,而非释放所有内存后再持续运行一段时间。

给 free()传入一个空指针并不是错误代码。但是在调用 free()后对参数 ptr 的任何使用,例如将其再次传递给 free(),将产生错误,并可能

导致不可预知的结果。

程序实例

下面的程序说明了 free()函数对 program break 的影响。该程序在分配了多块内存后,根据(可选)命令行参数来释放其中的部分或全部。前两个命令行参数指定了分配内存块的数量和大小。第三个命令行参数指定了释放内存块的循环步长。如果是 1(这也是省略此参数时的默认值),那么程序将释放每块已分配的内存,如果为 2,那么每隔一块释放一块已分配内存,以此类推。第四个和第五个命令行参数指定需要释放的内存块范围。如果省略这两个参数,那么将(以第三个命令行参数所指定的步长)释放全部范围内的已分配内存。

#define _BSD_SOURCE
#include "tlpi_hdr.h" // 包含 TLPI(The Linux Programming Interface)头文件,提供一些系统编程的常用函数和定义

#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) // 如果命令行参数数量不足或者第一个参数为 "--help",则输出使用说明并退出
        usageErr("%s num-allocs block-size [step [min [max]]]\n", argv[0]);

    numAllocs = getInt(argv[1], GN_GT_0, "num-allocs"); // 获取要分配的内存块数量
    if (numAllocs > MAX_ALLOCS) // 如果要分配的内存块数量超过了最大允许数量,则报错退出
        cmdLineErr("num-allocs > %d\n", MAX_ALLOCS);

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

    freeStep = (argc > 3) ? getInt(argv[3], GN_GT_0, "step") : 1; // 获取释放内存块的步长,默认为1
    freeMin =  (argc > 4) ? getInt(argv[4], GN_GT_0, "min") : 1; // 获取最小要释放的内存块编号,默认为1
    freeMax =  (argc > 5) ? getInt(argv[5], GN_GT_0, "max") : numAllocs; // 获取最大要释放的内存块编号,默认为总共分配的内存块数量

    if (freeMax > numAllocs) // 如果最大释放的内存块编号超过了总共分配的内存块数量,则报错退出
        cmdLineErr("free-max > num-allocs\n");

    printf("Initial program break:          %10p\n", sbrk(0)); // 打印初始程序 break 的位置

    printf("Allocating %d*%d bytes\n", numAllocs, blockSize); // 打印要分配的内存块的信息
    for (j = 0; j < numAllocs; j++) { // 循环分配内存块
        ptr[j] = malloc(blockSize); // 使用 malloc 函数分配内存块
        if (ptr[j] == NULL) // 如果分配失败,则报错退出
            errExit("malloc");
    }

    printf("Program break is now:           %10p\n", sbrk(0)); // 打印分配内存后程序 break 的位置

    printf("Freeing blocks from %d to %d in steps of %d\n",
                freeMin, freeMax, freeStep); // 打印要释放内存块的信息
    for (j = freeMin - 1; j < freeMax; j += freeStep) // 循环释放内存块
        free(ptr[j]); // 使用 free 函数释放内存块

    printf("After free(), program break is: %10p\n", sbrk(0)); // 打印释放内存后程序 break 的位置

    exit(EXIT_SUCCESS); // 正常退出程序
}

如下命令分配1000个内存块且每隔一个内存块释放一个内存块,可以看到free()前后的Program break没有变化。

# sudo ./free_and_sbrk 1000 1024 2

Initial program break:          0x558f8332a000
Allocating 1000*1024 bytes
Program break is now:           0x558f83411000
Freeing blocks from 1 to 1000 in steps of 2
After free(), program break is: 0x558f83411000

除最后一个内存块,释放所有已分配的内存块。program break依然保持在了“高水位线”。

# sudo ./free_and_sbrk 1000 1024 1 1 999 

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

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

# sudo ./free_and_sbrk 1000 1024 1 500 1000

Initial program break:          0x55567db47000
Allocating 1000*1024 bytes
Program break is now:           0x55567dc2e000
Freeing blocks from 500 to 1000 in steps of 1
After free(), program break is: 0x55567dbc7000

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

如果释放最后10个内存块,看到 program break并不会变化:

# sudo ./free_and_sbrk 1000 1024 1 990 1000

Initial program break:          0x563f1e009000
Allocating 1000*1024 bytes
Program break is now:           0x563f1e0f0000
Freeing blocks from 990 to 1000 in steps of 1
After free(), program break is: 0x563f1e0f0000

仅当堆顶空闲内存“足够”大的时候,free()函数的 glibc 实现会调用 sbrk()来降低 program break 的地址,至于“足够”与否则取决于 malloc 函数包行为的控制参数(128 KB 为典型值)。这减少了必须对 sbrk()发起的调用次数(亦即对 brk()系统调用的调用次数)。

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

但基于以下几个原因,最好能够在程序中显式释放所有的已分配内存:

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

malloc和free的实现

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

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

malloc()分配内存块时,会额外分配几个字节来存放记录这块内存大小的整数值用来给free释放内存时使用。该整数位于内存块的起始处,而实际返回给调用者的内存地址恰好位于这一长度记录字节之后,

image-20240426161139498

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

image-20240426161158959

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

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

image-20240426161657445

参数 mumitems 指定分配对象的数量,size 指定每个对象的大小。在分配了适当大小的内存块后,calloc()返回指向这块内存起始处的指针(如果无法分配内存,则返回 NULL)。与 malloc()不同,calloc()会将已分配的内存初始化为 0。

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

image-20240426161725345

参数 ptr 是指向需要调整大小的内存块的指针。参数 size 指定所需调整大小的期望值。

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

通常情况下,当增大已分配内存时,realloc()会试图去合并在空闲列表中紧随其后 且大小满足要求的内存块。若原内存块位于堆的顶部,那么 realloc()将对堆空间进行扩展。如果这块内存位于堆的中部,且紧邻其后的空闲内存空间大小不足,realloc()会分配一块新内存,并将原有数据复制到新内存块中。最后这种情况最为常见,还会占用大量 CPU资源。一般情况下,应尽量避免调用 realloc()。

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

设计函数 memalign()和 posix_memalign()的目的在于分配内存时,起始地址要与 2 的整数次幂边界对齐

image-20240426162316005

函数 memalign()分配 size 个字节的内存,起始地址是参数 boundary 的整数倍,而 boundary必须是 2 的整数次幂。函数返回已分配内存的地址。函数 memalign()并非在所有 UNIX 实现上都存在。大多数提供 memalign()的其他 UNIX 实现都要求引用<stdlib.h>而非<malloc.h>以获得函数声明。

posix_memalign()函数在少数 UNIX 实现:

image-20240426162403989

函数 posix_memalign()与 memalign()存在以下不同:

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

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

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

image-20240426162557959

不能在一个函数的参数列表中调用 alloca(),如:

image-20240426162630166

这会使 alloca()分配的堆栈空间出现在当前函数参数的空间内(函数参数都位于栈帧内的固定位置)。

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

  • 26
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值