文章目录
1. 前言
The GNU C Library Reference Manual for version 2.35
2. 虚拟地址分配和分页
Virtual Memory Allocation And Paging
本章描述了进程如何在使用 GNU C 库的系统中管理和使用内存。
GNU C 库有几个函数可以以各种方式动态分配虚拟内存。它们的通用性和效率各不相同。该库还提供了用于控制分页和实际内存分配的功能。
本章不讨论内存映射 I/O。请参阅内存映射 I/O。
2.1. 进程内存概念
Process Memory Concepts
进程可用的最基本资源之一是内存。系统组织内存的方式有很多种,但在典型的一种方式中,每个进程都有一个线性虚拟地址空间,地址从零到某个巨大的最大值。它不必是连续的;即,并非所有这些地址实际中都可用于存储数据。
虚拟内存被分成页(典型值为 4 KB)。支持虚拟内存的每一页的是实际内存的一页(称为帧)或一些辅助存储,通常是磁盘空间。磁盘空间可能是交换空间或只是一些普通的磁盘文件。实际上,一个全为零的页有时根本不存在一个标志说它是全零的。
实际内存或后备存储的同一帧可以支持属于多个进程的多个虚拟页。通常是这种情况,例如,GNU C 库代码占用的虚拟内存。包含 printf 函数的同一个实际内存帧支持每个在其程序中调用 printf 的现有进程中的虚拟内存页。
为了让程序访问虚拟页的任何部分,该页此时必须由实际帧支持或连接到实际帧。但是由于虚拟内存通常比实际内存多得多,因此页必须定期在实际内存和后备存储之间来回移动,当进程需要访问它们时进入实际内存,然后在不再需要时退回到后备存储。这种移动称为分页。
当一个程序试图访问一个当时没有实际内存支持的页时,这被称为页错误。当发生页错误时,内核暂停进程,将页放入实际页帧中(这称为“分页(paging in)”或“缺页中断(faulting in)”),然后恢复进程,以便从进程的角度来看,页一直在实际内存中。事实上,对于进程来说,所有页似乎总是在实际内存中。除了一件事:通常为几纳秒的指令的执行时间突然变得非常非常长(因为内核通常必须执行I/O才能完成换页(page-in))。对于对此敏感的程序,锁定页面中描述的功能可以控制它。
在每个虚拟地址空间内,一个进程必须跟踪哪些地址是什么,该进程称为内存分配。分配通常会让人想到分配稀缺资源,但在虚拟内存的情况下,这不是主要目标,因为它通常比任何人需要的要多得多。进程内的内存分配主要只是确保同一字节的内存不用于存储两个不同的东西。
进程以两种主要方式分配内存:通过 exec 和以编程方式。实际上,fork是第三种方式,但不是很有趣。请参阅创建进程。
Exec是为进程创建虚拟地址空间,将其基本程序加载到其中并执行程序的操作。它由“exec”函数族(例如 execl)完成。该操作获取一个程序文件(可执行文件),它分配空间以加载可执行文件中的所有数据,加载它并将控制权转移给它。该数据最值得注意的是程序的指令(文本),还有程序中的文字和常量,甚至一些变量:具有静态存储类的 C 变量(请参阅 C 程序中的内存分配)。
一旦该程序开始执行,它就会使用程序分配来获得额外的内存。在带有 GNU C 库的 C 程序中,有两种程序分配方式:自动分配和动态分配。请参阅 C 程序中的内存分配。
内存映射 I/O 是另一种形式的动态虚拟内存分配。将内存映射到文件意味着声明某个进程地址范围的内容应与指定的常规文件的内容相同。系统使虚拟内存最初包含文件的内容,如果您修改内存,系统会将相同的修改写入文件。请注意,由于虚拟内存和页错误的魔力,在程序访问虚拟内存之前,系统没有理由执行 I/O 来读取文件或为其内容分配实际内存。请参阅内存映射 I/O。
正如它以编程方式分配内存一样,程序可以以编程方式解除分配(释放)它。您无法释放 exec 分配的内存。当程序退出或执行时,你可能会说它的所有内存都被释放了,但是由于在这两种情况下地址空间都不存在了,这一点真的没有实际意义。请参阅程序终止。
进程的虚拟地址空间被划分为段。段是虚拟地址的连续范围。三个重要的部分是:
- 文本段包含程序的指令、文字和静态常量。它由 exec 分配,并在虚拟地址空间的生命周期内保持相同的大小。
- 数据段是程序的工作存储。它可以由 exec 预分配和预加载,进程可以通过调用函数来扩展或收缩它,如参见调整数据段大小中所述。它的下端是固定的。
- 栈段包含一个程序栈。它随着栈的增长而增长,但在栈缩小时不会缩小。
2.2. 为程序数据分配存储空间
Allocating Storage For Program Data
本节介绍普通程序如何管理其数据的存储,包括著名的 malloc 函数和 GNU C 库和 GNU 编译器专用的一些更高级的工具。
2.2.1. C 程序中的内存分配
Memory Allocation in C Programs
C 语言支持通过 C 程序中的变量分配两种内存:
-
静态分配是在声明静态或全局变量时发生的。每个静态或全局变量定义一个固定大小的空间块。当您的程序启动(执行操作的一部分)时,空间被分配一次,并且永远不会被释放。
-
当您声明一个自动变量(例如函数参数或局部变量)时,就会发生自动分配。自动变量的空间在输入包含声明的复合语句时分配,并在退出该复合语句时释放。
在 GNU C 中,自动存储的大小可以是一个变化的表达式。在其他 C 实现中,它必须是常量。
第三种重要的内存分配,动态分配,不受 C 变量支持,但可通过 GNU C 库函数获得。
2.2.1.1. 动态内存分配
Dynamic Memory Allocation
动态内存分配是一种技术,程序在运行时确定在哪里存储一些信息。当您需要的内存量或继续需要多长时间取决于程序运行之前未知的因素时,您需要动态分配。
例如,您可能需要一个块来存储从输入文件中读取的行;由于一行的长度没有限制,因此您必须动态分配内存,并在您读更多行时使其动态变大。
或者,您可能需要为输入数据中的每条记录或每个定义设置一个块;由于您无法提前知道会有多少,因此您必须在读时为每条记录或定义分配一个新块。
当您使用动态分配时,内存块的分配是程序明确请求的操作。当您想要分配空间时调用函数或宏,并使用参数指定大小。如果要释放空间,可以通过调用另一个函数或宏来实现。你可以随时随地做这些事情。
C 变量不支持动态分配;没有“动态”存储类,也永远不会有一个 C 变量的值存储在动态分配的空间中。获得动态分配内存的唯一方法是通过系统调用(通常是通过 GNU C 库函数调用),而引用动态分配空间的唯一方法是通过指针。因为不太方便,而且动态分配的实际过程需要更多的计算时间,程序员通常只在静态分配和自动分配都不起作用时才使用动态分配。
例如,如果要动态分配一些空间来保存 struct foobar,则不能声明 struct foobar 类型的变量,其内容是动态分配的空间。但是您可以声明一个指针类型的变量 struct foobar * 并为其分配空间地址。然后你可以在这个指针变量上使用运算符’*‘和’->'来引用空间的内容:
{
struct foobar *ptr = malloc (sizeof *ptr);
ptr->name = x;
ptr->next = current_foobar;
current_foobar = ptr;
}
2.2.2. GNU 分配器
The GNU Allocator
GNU C 库中的 malloc 实现派生自 ptmalloc (pthreads malloc),而后者又派生自 dlmalloc (Doug Lea malloc)。这个 malloc 可以根据它们的大小和可能由用户控制的某些参数以两种不同的方式分配内存。最常见的方法是从大的连续内存区域分配部分内存(称为块)并管理这些区域以优化它们的使用并减少不可用块形式的浪费。传统上,系统堆被设置为一个大内存区域,但 GNU C 库 malloc 实现维护多个这样的区域以优化它们在多线程应用程序中的使用。每个这样的区域在内部被称为一个arena。
与其他版本相反,GNU C 库中的 malloc 不会将块大小四舍五入为 2 的幂,无论是大的还是小的大小。相邻的块可以在空闲时合并,无论它们的大小是多少。这使得该实现适用于各种分配模式,而通常不会因碎片而导致大量内存浪费。多个 arena 的存在允许多个线程同时在单独的 arena 中分配内存,从而提高性能。
内存分配的另一种方式是非常大的块,即比页大得多。这些请求使用 mmap 分配(匿名或通过 /dev/zero;请参阅内存映射 I/O)。这具有很大的优势,即这些块在被释放时会立即返回到系统。因此,不会发生大块被“锁定”在较小块之间的情况,即使在调用 free 之后也会浪费内存。要使用的 mmap 的大小阈值是动态的,并根据程序的分配模式进行调整。mallopt 可用于使用 M_MMAP_THRESHOLD 静态调整阈值,并且可以使用 M_MMAP_MAX 完全禁用 mmap 的使用;请参阅 Malloc 可调参数。
GNU 分配器的更详细的技术描述保存在 GNU C 库 wiki 中。请参阅 https://sourceware.org/glibc/wiki/MallocInternals。
可以使用您自己的自定义 malloc 代替 GNU C 库提供的内置分配器。请参阅替换 malloc。
2.2.3. 无约束分配
Unconstrained Allocation
最通用的动态分配工具是 malloc。它允许您随时分配任何大小的内存块,随时使它们变大或变小,并在任何时候(或从不)单独释放块。
2.2.3.1. 基本内存分配(malloc)
Basic Memory Allocation
要分配一块内存,请调用 malloc。此函数的原型位于 stdlib.h 中。
函数:void * malloc (size t size)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock fd mem | See POSIX Safety Concepts.
此函数返回一个指向新分配的块 size 字节长的指针,如果无法分配块,则返回一个空指针(设置 errno)。
块的内容是未定义的;您必须自己初始化它(或使用 calloc 代替;请参阅分配已清除空间)。通常,您会将值转换为指向要存储在块中的对象类型的指针。这里我们展示了一个这样做的例子,以及使用库函数 memset 用零初始化空间(请参阅复制字符串和数组):
struct foo *ptr = malloc (sizeof *ptr);
if (ptr == 0) abort ();
memset (ptr, 0, sizeof (struct foo));
您可以将 malloc 的结果存储到任何指针变量中而无需强制转换,因为 ISO C 在必要时会自动将类型 void * 转换为另一种类型的指针。但是,如果需要类型但上下文未指定类型,则强制类型转换是必要的。
请记住,在为字符串分配空间时,malloc 的参数必须是字符串长度加一。这是因为字符串以不计入字符串“长度”但需要空间的空字符终止。例如:
char *ptr = malloc (length + 1);
2.2.3.2. malloc的例子
Examples of malloc
如果没有更多可用空间,则 malloc 返回一个空指针。您应该检查每次调用 malloc 的值。编写一个调用 malloc 并在值为空指针时报告错误的子例程很有用,仅当值非零时才返回。该函数通常称为 xmalloc:
void *
xmalloc (size_t size)
{
void *value = malloc (size);
if (value == 0)
fatal ("virtual memory exhausted");
return value;
}
这是一个使用 malloc 的真实示例(通过 xmalloc)。函数 savestring 会将一系列字符复制到新分配的以空字符结尾的字符串中:
char *
savestring (const char *ptr, size_t len)
{
char *value = xmalloc (len + 1);
value[len] = '\0';
return memcpy (value, ptr, len);
}
malloc 为您提供的块保证是对齐的,以便它可以保存任何类型的数据。在 GNU 系统上,地址在 32 位系统上始终是 8 的倍数,在 64 位系统上始终是 16 的倍数。很少需要任何更高的边界(例如页面边界);对于这些情况,请使用 aligned_alloc 或 posix_memalign(请参阅分配对齐的内存块)。
请注意,位于块末尾之后的内存可能正在用于其他用途;也许一个块已经被另一个 malloc 调用分配了。如果您尝试将块视为比您要求的更长的时间,您可能会破坏 malloc 用于跟踪其块的数据,或者您可能会破坏另一个块的内容。如果您已经分配了一个块并发现您希望它更大,请使用 realloc(请参阅更改块的大小)。
可移植性说明:
- 在 GNU C 库中,成功的 malloc(0) 返回一个指向新分配的大小为零的块的非空指针;其他实现可能会返回 NULL。POSIX 和 ISO C 标准允许这两种行为。
- 在 GNU C 库中,失败的 malloc 调用会设置 errno,但 ISO C 不需要这样做,并且非 POSIX 实现在失败时不需要设置 errno。
- 在 GNU C 库中,当大小超过 PTRDIFF_MAX 时,malloc 总是失败,以避免减去指针或使用带符号索引的程序出现问题。在这种情况下,其他实现可能会成功,从而导致以后出现未定义的行为。
2.2.3.3. 释放malloc分配的内存(free)
Freeing Memory Allocated with malloc
当您不再需要使用 malloc 获得的块时,请使用 free 函数使该块可再次分配。此函数的原型位于 stdlib.h 中。
函数:void free (void *ptr)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock fd mem | See POSIX Safety Concepts.
free 函数释放 ptr 指向的内存块。
释放块会改变块的内容。不要期望在释放块后在块中找到任何数据(例如指向块链中下一个块的指针)。在释放它之前复制你需要的任何东西!这是释放链中所有块的正确方法的示例,以及它们指向的字符串:
struct chain
{
struct chain *next;
char *name;
};
void
free_chain (struct chain *chain)
{
while (chain != 0)
{
struct chain *next = chain->next;
free (chain->name);
free (chain);
chain = next;
}
}
有时,free 实际上可以将内存返回给操作系统,并使进程更小。通常,它所能做的就是允许稍后调用 malloc 来重用空间。同时,该空间作为 malloc 内部使用的空闲列表的一部分保留在您的程序中。
free 函数保留 errno 的值,因此清理代码不必担心在调用 free 时保存和恢复 errno。尽管 ISO C 和 POSIX.1-2017 都不需要 free 保留 errno,但 POSIX 的未来版本计划要求它。
在程序结束时释放块没有意义,因为当进程终止时,程序的所有空间都归还给系统。
2.2.3.4. 更改块的大小(realloc)
Changing the Size of a Block
当您必须开始使用该块时,您通常无法确定最终需要多大的块。例如,块可能是一个缓冲区,用于保存从文件中读取的行;无论您最初制作缓冲区多长时间,您都可能遇到更长的行。
您可以通过调用 realloc 或 reallocarray 使块更长。这些函数在 stdlib.h 中声明。
函数:void * realloc (void *ptr, size t newsize)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock fd mem | See POSIX Safety Concepts.
realloc 函数将地址为 ptr 的块的大小更改为 newsize。由于块末尾之后的空间可能正在使用中,realloc 可能会发现有必要将块复制到有更多可用空间的新地址。realloc 的返回值是块的新地址。如果需要移动块,realloc 会复制旧的内容。
如果你为 ptr 传递一个空指针,realloc 的行为就像’malloc (newsize)'。否则,如果 newsize 为零,realloc 释放块并返回 NULL。否则,如果 realloc 无法重新分配请求的大小,则返回 NULL 并设置 errno;原始块不受干扰。
函数:void * reallocarray (void *ptr, size t nmemb, size t size)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock fd mem | See POSIX Safety Concepts.
reallocarray 函数将地址为 ptr 的块的大小更改为足够长以包含 nmemb 元素的向量(vector),每个元素的大小为size。它等效于“realloc (ptr, nmemb * size)”,但如果乘法溢出,reallocarray 会安全失败,方法是将 errno 设置为 ENOMEM,返回一个空指针,并保持原始块不变。
当分配块的新大小是可能溢出的乘法结果时,应使用 reallocarray 而不是 realloc。
可移植性说明:此功能不是任何标准的一部分。它最初是在 OpenBSD 5.6 中引入的。
与 malloc 一样,如果没有可用的内存空间使块变大,realloc 和 reallocarray 可能会返回空指针。发生这种情况时,原始块保持不变;它没有被修改或搬移。
在大多数情况下,当 realloc 失败时,原始块发生的情况并没有什么不同,因为应用程序在内存不足时无法继续,唯一要做的就是给出一个致命的错误消息。编写和使用通常称为 xrealloc 和 xreallocarray 的子例程通常很方便,它们像 xmalloc 对 malloc 所做的那样处理错误消息:
void *
xreallocarray (void *ptr, size_t nmemb, size_t size)
{
void *value = reallocarray (ptr, nmemb, size);
if (value == 0)
fatal ("Virtual memory exhausted");
return value;
}
void *
xrealloc (void *ptr, size_t size)
{
return xreallocarray (ptr, 1, size);
}
您还可以使用 realloc 或 reallocarray 使块更小。这样做的原因是为了避免在只需要一点内存空间时占用大量内存空间。在几种分配实现中,有时需要复制一个块,因此如果没有其他可用空间,它可能会失败。
可移植性说明:
- 可移植程序不应尝试将块重新分配为零大小。在其他实现中,如果 ptr 不为空,realloc (ptr, 0) 可能会释放块并返回指向大小为零的对象的非空指针,或者它可能会失败并返回 NULL 而不会释放块。ISO C17 标准允许这些变化。
- 在 GNU C 库中,如果结果块的大小超过 PTRDIFF_MAX,则重新分配会失败,以避免减去指针或使用带符号索引的程序出现问题。其他实现可能会成功,从而导致以后出现未定义的行为。
- 在 GNU C 库中,如果新大小与旧大小相同,则 realloc 和 reallocarray 保证不会更改任何内容并返回您提供的相同地址。但是,POSIX 和 ISO C 允许函数重新定位对象或在这种情况下失败。
2.2.3.5. 分配已清理空间(calloc)
Allocating Cleared Space
函数 calloc 分配内存并将其清除为零。它在 stdlib.h 中声明。
函数:void * calloc (size t count, size t eltsize)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock fd mem | See POSIX Safety Concepts.
此函数分配一个足够长的块以包含 count 元素的向量,每个元素的大小为 eltsize。在 calloc 返回之前,它的内容被清零。
您可以按如下方式定义 calloc:
void *
calloc (size_t count, size_t eltsize)
{
void *value = reallocarray (0, count, eltsize);
if (value != 0)
memset (value, 0, count * eltsize);
return value;
}
但总的来说,不保证 calloc 在内部调用 reallocarray 和 memset。例如,如果 calloc 实现由于其他原因知道新的内存块为零,则无需使用 memset 再次将块清零。此外,如果应用程序在 C 库之外提供自己的 reallocarray,则 calloc 可能不会使用该重新定义。请参阅替换 malloc。
2.2.3.6. 分配对齐的内存块(aligned_alloc)
Allocating Aligned Memory Blocks
在 GNU 系统中 malloc 或 realloc 返回的块的地址始终是 8 的倍数(或 64 位系统上的 16)。如果您需要一个地址是 2 的高次幂的倍数的块,请使用aligned_alloc或posix_memalign。aligned_alloc 和 posix_memalign 在 stdlib.h 中声明。
函数:void * aligned_alloc (size t alignment, size t size)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock fd mem | See POSIX Safety Concepts.
aligned_alloc 函数分配一个size字节的

GCC库提供了多种动态内存分配工具,如malloc、calloc、realloc和free,以及用于调试的mcheck和mtrace。malloc初始化分配,calloc分配并清零,realloc调整内存大小,free释放内存。GNU分配器可以动态调整其行为以优化性能。mcheck和mtrace用于检测内存泄漏。此外,还可以使用obstacks进行快速、无约束的分配,以及通过aligned_alloc分配对齐的内存。通过mallopt可以调整分配参数,通过mcheck进行堆一致性检查。在调试时,可以使用valgrind或gdb进行更深入的内存错误检测。
最低0.47元/天 解锁文章
1540

被折叠的 条评论
为什么被折叠?



