glibc 知:手册03:虚拟地址分配和分页

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字节的块,其地址是alignment的倍数。alignment必须是 2 的幂,size必须是alignment的倍数。

aligned_alloc 函数在出错时返回一个空指针,并将 errno 设置为下列值之一:

  • ENOMEM 没有足够的内存来满足请求。
  • EINVAL alignment不是 2 的幂。

此函数是在 ISO C11 中引入的,因此可能比 posix_memalign 对现代非 POSIX 系统具有更好的可移植性。

函数:void * memalign (size t boundary, size t size)

Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock fd mem | See POSIX Safety Concepts.

memalign 函数分配一个size字节的块,其地址是boundary的倍数。boundary必须是2的幂!函数 memalign 的工作原理是分配一个更大的块,然后返回块内指定边界上的地址。

memalign 函数在出错时返回一个空指针,并将 errno 设置为下列值之一:

  • ENOMEM 没有足够的内存来满足请求。
  • EINVAL boundary不是 2 的幂。

memalign 函数已过时,应改为使用 aligned_alloc 或 posix_memalign。

函数:int posix_memalign (void **memptr, size t alignment, size t size)

Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock fd mem | See POSIX Safety Concepts.

posix_memalign 函数类似于 memalign 函数,因为它返回一个 size 字节对齐到 alignment 倍数的缓冲区。但是它对参数 alignment 增加了一个要求:该值必须是 sizeof(void *) 的两倍的幂。

如果函数成功分配内存,则在 *mempr 中返回指向已分配内存的指针,并且返回值为零。否则,该函数将返回一个错误值来指示问题。返回的可能错误值为:

  • ENOMEM 没有足够的内存来满足请求。
  • EINVAL alignment 不是 sizeof (void *) 的两倍的幂。

此功能是在 POSIX 1003.1d 中引入的。尽管此函数已被 aligned_alloc 取代,但它更易于移植到不支持ISO C11 的旧POSIX 系统。

函数:void * valloc (size t size)

Preliminary: | MT-Unsafe init | AS-Unsafe init lock | AC-Unsafe init lock fd mem | See POSIX Safety Concepts.

使用 valloc 就像使用 memalign 并将页面大小作为第一个参数的值传递。它是这样实现的:

void *
valloc (size_t size)
{
    return memalign (getpagesize (), size);
}

valloc 函数已过时,应改用 aligned_alloc 或 posix_memalign。

2.2.3.7. malloc可调参数(mallopt)

Malloc Tunable Parameters

您可以使用 mallopt 函数调整动态内存分配的一些参数。该函数是通用的 SVID/XPG 接口,定义在 malloc.h 中。

函数:int mallopt (int param, int value)

Preliminary: | MT-Unsafe init const:mallopt | AS-Unsafe init lock | AC-Unsafe init lock | See POSIX Safety Concepts.

调用 mallopt 时,param 参数指定要设置的参数,value 是要设置的新值。在 malloc.h 中定义的 param 的可能选择是:

  • M_MMAP_MAX

    使用 mmap 分配的最大块数。将此设置为零将禁用所有 mmap 的使用。

    该参数的默认值为 65536。

    通过将环境变量 MALLOC_MMAP_MAX_ 设置为所需的值,也可以在启动时为进程设置此参数。

  • M_MMAP_THRESHOLD

    使用 mmap 系统调用将所有大于此值的块分配到正常堆之外。这样可以保证这些块的内存 free 后可以返回给系统。请注意,小于此阈值的请求仍可能通过 mmap 分配。

    如果未设置此参数,则默认值设置为 128 KiB,并动态调整阈值以适应程序的分配模式。如果设置了参数,则禁用动态调整,并将值静态设置为输入值。

    通过将环境变量 MALLOC_MMAP_THRESHOLD_ 设置为所需的值,也可以在启动时为进程设置此参数。

  • M_PERTURB

    如果非零,则在分配内存块(由 calloc 分配时除外)和释放时,根据此参数的某些低位的位填充内存块。这可用于调试未初始化或已释放堆内存的使用。请注意,此选项不保证释放的块将具有任何特定值。它只保证块被释放之前的内容将被覆盖。

    此参数的默认值为 0。

    通过将环境变量 MALLOC_PERTURB_ 设置为所需的值,也可以在启动时为进程设置此参数。

  • M_TOP_PAD

    此参数确定当需要扩展 arena 时从系统获得的额外内存量。它还指定缩小 arena 时要保留的字节数。这提供了堆大小的必要滞后,从而可以避免过多的系统调用。

    此参数的默认值为 0。

    通过将环境变量 MALLOC_TOP_PAD_ 设置为所需的值,也可以在启动时为进程设置此参数。

  • M_TRIM_THRESHOLD

    这是最顶层的可释放块的最小大小(以字节为单位),它将触发系统调用以将内存返回给系统。如果未设置此参数,则默认值设置为 128 KiB,并动态调整阈值以适应程序的分配模式。如果设置了参数,则禁用动态调整,并将值静态设置为提供的输入。

    通过将环境变量 MALLOC_TRIM_THRESHOLD_ 设置为所需的值,也可以在启动时为进程设置此参数。

  • M_ARENA_TEST

    该参数指定在对 arena 数量限制进行测试之前可以创建的 arena 数量。如果设置了 M_ARENA_MAX,则忽略该值。

    此参数的默认值在 32 位系统上为 2,在 64 位系统上为 8。

    通过将环境变量 MALLOC_ARENA_TEST 设置为所需的值,也可以在启动时为进程设置此参数。

  • M_ARENA_MAX

    此参数设置要使用的 arena 数量,而与系统中的核心数量无关。

    此可调参数的默认值为 0,这意味着对 arenas 数量的限制由在线 CPU 内核数决定。对于 32 位系统,限制是在线内核数的两倍,在 64 位系统上,它是八倍的核心在线数量。请注意,默认值并非源自 M_ARENA_TEST 的默认值,而是独立计算的。

    通过将环境变量 MALLOC_ARENA_MAX 设置为所需的值,也可以在启动时为进程设置此参数。

2.2.3.8. 堆一致性检查(mcheck)

Heap Consistency Checking

您可以使用 mcheck 函数要求 malloc 检查动态内存的一致性,并使用 LD_PRELOAD 环境变量预加载 malloc 调试库 libc_malloc_debug。这个函数是一个 GNU 扩展,在 mcheck.h 中声明。

函数:int mcheck (void (*abortfn) (enum mcheck status status))

Preliminary: | MT-Unsafe race:mcheck const:malloc hooks | AS-Unsafe corrupt | AC-Unsafe corrupt | See POSIX Safety Concepts.

调用 mcheck 告诉 malloc 执行偶尔的(occasional)一致性检查。这些将捕获诸如写入超过使用 malloc 分配的块的末尾之类的东西。abortfn 参数是发现不一致时调用的函数。如果您提供一个空指针,则 mcheck 使用一个默认函数,该函数打印一条消息并调用 abort(请参阅终止程序)。您提供的函数使用一个参数调用,该参数说明检测到哪种不一致;其类型如下所述。

一旦你用 malloc 分配了任何东西,再开始分配检查为时已晚。所以 mcheck 在这种情况下什么都不做。如果调用太晚,该函数返回 -1,否则返回 0(成功时)。

安排尽早调用 mcheck 的最简单方法是在链接程序时使用选项“-lmcheck”;那么你根本不需要修改你的程序源。或者,您可以使用调试器在程序启动时插入对 mcheck 的调用,例如,这些 gdb 命令将在程序启动时自动调用 mcheck:

(gdb) break main
Breakpoint 1, main (argc=2, argv=0xbffff964) at whatever.c:10
(gdb) command 1
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>call mcheck(0)
>continue
>end
(gdb) ...

但是,这仅在所涉及的任何对象的初始化函数都没有调用任何 malloc 函数时才有效,因为 mcheck 必须在第一个此类函数之前调用。

函数:enum mcheck_status mprobe (void *pointer)

Preliminary: | MT-Unsafe race:mcheck const:malloc hooks | AS-Unsafe corrupt | AC-Unsafe corrupt | See POSIX Safety Concepts.

mprobe 函数允许您显式检查特定分配块中的不一致。您必须已经在程序开始时调用了 mcheck 来进行偶尔的检查;调用 mprobe 请求在调用时进行额外的一致性检查。

参数 pointer 必须是 malloc 或 realloc 返回的指针。mprobe 返回一个值,说明发现了什么不一致(如果有)。这些值如下所述。

数据类型:enum mcheck_status

此枚举类型描述了在分配的块中检测到哪种不一致(如果有)。以下是可能的值:

  • MCHECK_DISABLED
    在第一次分配之前没有调用 mcheck。无法进行一致性检查。
  • MCHECK_OK
    未检测到不一致。
  • MCHECK_HEAD
    块之前的数据被立即修改。这通常发生在数组索引或指针减少太多时。
  • MCHECK_TAIL
    块之后的数据被立即修改。当数组索引或指针增加太多时,通常会发生这种情况。
  • MCHECK_FREE
    该块已被释放。

在使用 malloc、realloc 和 free 时检查和防止错误的另一种可能性是设置环境变量 MALLOC_CHECK_。当 MALLOC_CHECK_ 设置为小于 4 的非零值时,将使用一种特殊的(效率较低的)实现,该实现旨在容忍简单的错误,例如使用相同参数的两次 free 调用,或单个字节的溢出(一个错误)。然而,并非所有此类错误都可以得到保护,并且可能导致内存泄漏。与 mcheck 的情况一样,需要预加载 libc_malloc_debug 库以启用 MALLOC_CHECK_ 功能。如果没有这个预加载的库,设置 MALLOC_CHECK_ 将无效。

任何检测到的堆损坏都会导致进程立即终止。

MALLOC_CHECK_ 存在一个问题:在 SUID 或 SGID 二进制文件中,由于与正常程序行为不同,它可能被利用,或者它现在向标准错误描述符写入一些内容。因此,对于 SUID 和 SGID 二进制文件,默认情况下禁用 MALLOC_CHECK_ 的使用。系统管理员可以通过添加文件 /etc/suid-debug 再次启用它(内容不重要,可以为空)。

那么,使用 MALLOC_CHECK_ 和使用“-lmcheck”链接有什么区别呢? MALLOC_CHECK_ 与“-lmcheck”正交。添加了“-lmcheck”以实现向后兼容性。MALLOC_CHECK_ 和“-lmcheck”都应该发现相同的错误——但是使用 MALLOC_CHECK_ 你不需要重新编译你的应用程序。

2.2.3.9. 统计使用malloc分配的内存信息(mallinfo2)

Statistics for Memory Allocation with malloc

您可以通过调用 mallinfo2 函数获取有关动态内存分配的信息。此函数及其相关数据类型在 malloc.h 中声明;它们是标准 SVID/XPG 版本的扩展。

数据类型:struct mallinfo2

此结构类型用于返回有关动态内存分配器的信息。它包含以下成员:

size_t arena
    这是 malloc 用 sbrk 分配的内存的总大小,以字节为单位。
size_t ordblks
    这是未使用的块数。
    内存分配器大小从操作系统内部获取内存块,然后将它们分割以满足各个 malloc 请求。
size_t smblks
    该字段未使用。
size_t hblks
    这是使用 mmap 分配的块的总数。
size_t hblkhd
    这是使用 mmap 分配的内存的总大小,以字节为单位。
size_t usmblks
    该字段未使用且始终为 0size_t fsmblks
    该字段未使用。
size_t uordblks
    这是 malloc 分配的块所占用的内存总大小。
size_t fordblks
    这是空闲(未使用)块占用的内存总大小。
size_t keepcost
    这是通常与堆末端接壤的最高可释放块的大小(即虚拟地址空间数据段的高端)。

函数:struct mallinfo2 mallinfo2 (void)

Preliminary: | MT-Unsafe init const:mallopt | AS-Unsafe init lock | AC-Unsafe init lock | See POSIX Safety Concepts.

此函数以 struct mallinfo2 类型的结构返回有关当前动态内存使用情况的信息。

2.2.3.10. malloc相关函数总结

Summary of malloc-Related Functions

以下是使用 malloc 的函数的摘要:

void *malloc (size_t size)
    分配一个 size 字节的块。

void free (void *addr)
    释放之前由 malloc 分配的块。

void *realloc (void *addr, size_t size)
    使以前由 malloc 分配的块更大或更小,可能通过将其复制到新位置。

void *reallocarray (void *ptr, size_t nmemb, size_t size)
    将 malloc 先前分配的块的大小更改为 nmemb * size 个字节,与 realloc 一样。

void *calloc (size_t count, size_t eltsize)
    使用 malloc 分配一个 count * eltsize 字节块,并将其内容设置为零。

void *valloc (size_t size)
    从页边界开始分配大小字节块。

void *aligned_alloc (size_t size, size_t alignment)
    从 alignment 倍数的地址开始分配 size 字节块。

int posix_memalign (void **memptr, size_t alignment, size_t size)
    从 alignment 倍数的地址开始分配 size 字节块。

void *memalign (size_t size, size_t boundary)
    分配一个 size 字节的块,从一个 boundary 倍数的地址开始。

int mallopt (int param, int value)
    调整可调参数。

int mcheck (void (*abortfn) (void))
    告诉 malloc 对动态分配的内存执行偶尔的一致性检查,并在发现不一致时调用 abortfn。

struct mallinfo2 mallinfo2 (void)
    返回有关当前动态内存使用情况的信息。

2.2.4. 分配调试

Allocation Debugging

基于不使用垃圾回收动态内存分配的语言进行编程时,一项复杂的任务是查找内存泄漏。长时间运行的程序必须确保动态分配的对象在其生命周期结束时被释放。如果这没有发生,系统迟早会耗尽内存。

GNU C 库中的 malloc 实现提供了一些简单的方法来检测此类泄漏并获取一些信息以找到位置。为此,应用程序必须以由环境变量启用的特殊模式启动。如果未启用调试模式,则程序不会受到速度损失。

2.2.4.1. 如何安装追踪功能(mtrace)

How to install the tracing functionality

函数:void mtrace (void)

Preliminary: | MT-Unsafe env race:mtrace init | AS-Unsafe init heap corrupt lock | AC-Unsafe init corrupt lock fd mem | See POSIX Safety Concepts.

mtrace 函数提供了一种方法来跟踪调用它的程序中的内存分配事件。它在库中默认禁用,可以通过使用 LD_PRELOAD 环境变量预加载调试库 libc_malloc_debug 来启用。

当调用 mtrace 函数时,它会查找名为 MALLOC_TRACE 的环境变量。这个变量应该包含一个有效的文件名。用户必须具有写入权限。如果文件已存在,则将其截断。如果没有设置环境变量或者它没有命名一个可以打开写入的有效文件,则什么也不做。malloc 等的行为没有改变。出于显而易见的原因,如果应用程序安装时设置了 SUID 或 SGID 位,也会发生这种情况。

如果命名文件成功打开,mtrace 会为函数 malloc、realloc 和 free 安装特殊处理程序。从那时起,这些函数的所有使用都被跟踪并协议化到文件中。当然,现在对跟踪函数的所有调用都会有速度损失,因此在正常使用期间不应启用跟踪。

此功能是 GNU 扩展,通常在其他系统上不可用。原型可以在 mcheck.h 中找到。

函数:void muntrace (void)

Preliminary: | MT-Unsafe race:mtrace locale | AS-Unsafe corrupt heap | AC-Unsafe corrupt mem lock fd | See POSIX Safety Concepts.

在使用 mtrace 启用跟踪 malloc 调用后,可以调用 muntrace 函数。如果没有(成功)调用 mtrace,则 muntrace 什么也不做。

否则,它会卸载 malloc、realloc 和 free 的处理程序,然后关闭协议文件。不再有调用被协议处理,程序再次全速运行。

此功能是 GNU 扩展,通常在其他系统上不可用。原型可以在 mcheck.h 中找到。

2.2.4.2. 示例程序摘录

Example program excerpts

即使跟踪功能不会影响程序的运行时行为,在所有程序中调用 mtrace 也不是一个好主意。试想一下,您使用 mtrace 调试程序,并且调试会话中使用的所有其他程序也跟踪它们的 malloc 调用。所有程序的输出文件都相同,因此无法使用。因此,只有在为调试而编译时才应该调用 mtrace。因此,程序可以像这样开始:

#include <mcheck.h>

int
main (int argc, char *argv[])
{
#ifdef DEBUGGING
    mtrace ();
#endif
    ...
}

如果您想在程序的整个运行时跟踪调用,这就是您所需要的。或者,您可以随时调用 muntrace 来停止跟踪。甚至可以通过对 mtrace 的新调用再次重新启动跟踪。但这可能会导致不可靠的结果,因为可能会调用未调用的函数。请注意,不仅应用程序使用跟踪函数,库(包括 C 库本身)也使用这些函数。

最后一点也是为什么在程序终止之前调用 muntrace 不是一个好主意的原因。库仅在程序从 main 返回或调用 exit 后才被通知程序终止,因此在此之前无法释放它们使用的内存。

所以最好的办法是调用 mtrace 作为程序中的第一个函数,并且永远不要调用 muntrace。因此,该程序跟踪几乎所有 malloc 函数的使用(除了那些由程序的构造函数或使用的库执行的调用)。

2.2.4.3. 一些或多或少聪明的想法

Some more or less clever ideas

你知道情况。该程序已准备好进行调试,并且在所有调试会话中运行良好。但是一旦它在没有调试的情况下启动,就会出现错误。一个典型的例子是内存泄漏,只有在我们关闭调试时才可见。如果您预见到这种情况,您仍然可以获胜。只需使用与以下小程序等效的东西:

#include <mcheck.h>
#include <signal.h>

static void
enable (int sig)
{
    mtrace ();
    signal (SIGUSR1, enable);
}

static void
disable (int sig)
{
    muntrace ();
    signal (SIGUSR2, disable);
}

int
main (int argc, char *argv[])
{
    ...
    signal (SIGUSR1, enable);
    signal (SIGUSR2, disable);
    ...
}

即,如果程序是在环境中设置了 MALLOC_TRACE 的情况下启动的,则用户可以随时启动内存调试器。输出当然不会显示在第一个信号之前发生的分配,但是如果存在内存泄漏,这仍然会显示出来。

2.2.4.4. 解释跟踪

Interpreting the traces

如果您看一下输出,它将类似于以下内容:

= Start
[0x8048209] - 0x8064cc8
[0x8048209] - 0x8064ce0
[0x8048209] - 0x8064cf8
[0x80481eb] + 0x8064c48 0x14
[0x80481eb] + 0x8064c60 0x14
[0x80481eb] + 0x8064c78 0x14
[0x80481eb] + 0x8064c90 0x14
= End

这一切意味着什么并不重要,因为跟踪文件不是供人阅读的。因此,不注意可读性。取而代之的是 GNU C 库附带的一个程序,它可以解释跟踪并以用户友好的方式输出摘要。该程序称为 mtrace(它实际上是一个 Perl 脚本),它接受一两个参数。在任何情况下,必须指定带有跟踪输出的文件的名称。如果可选参数位于跟踪文件的名称之前,则它必须是生成跟踪的程序的名称。

drepper$ mtrace tst-mtrace log
No memory leaks.

在这种情况下,程序 tst-mtrace 运行并生成了一个跟踪文件日志。mtrace 打印的消息显示代码没有问题,所有分配的内存随后都被释放。

如果我们在上面给出的示例跟踪上调用 mtrace,我们会得到不同的输出:

drepper$ mtrace errlog
- 0x08064cc8 Free 2 was never alloc'd 0x8048209
- 0x08064ce0 Free 3 was never alloc'd 0x8048209
- 0x08064cf8 Free 4 was never alloc'd 0x8048209
Memory not freed:
-----------------
Address Size Caller
0x08064c48 0x14 at 0x80481eb
0x08064c60 0x14 at 0x80481eb
0x08064c78 0x14 at 0x80481eb
0x08064c90 0x14 at 0x80481eb

我们只用一个参数调用了 mtrace,因此脚本没有机会找出跟踪中给出的地址的含义。我们可以做得更好:

drepper$ mtrace tst errlog
- 0x08064cc8 Free 2 was never alloc'd /home/drepper/tst.c:39
- 0x08064ce0 Free 3 was never alloc'd /home/drepper/tst.c:39
- 0x08064cf8 Free 4 was never alloc'd /home/drepper/tst.c:39
Memory not freed:
-----------------
Address Size Caller
0x08064c48 0x14 at /home/drepper/tst.c:33
0x08064c60 0x14 at /home/drepper/tst.c:33
0x08064c78 0x14 at /home/drepper/tst.c:33
0x08064c90 0x14 at /home/drepper/tst.c:33

突然间,输出变得更有意义了,用户可以立即看到导致问题的函数调用的位置。

解释这个输出并不复杂。最多检测两种不同的情况。首先,free 被调用用于从未被分配函数返回的指针。这通常是一个非常糟糕的问题,输出的前三行显示了它的样子。像这样的情况非常罕见,如果它们出现,它们会非常剧烈地出现:程序通常会崩溃。

另一种更难检测的情况是内存泄漏。正如您在输出中看到的那样,mtrace 函数收集了所有这些信息,因此可以说程序从源文件 /home/drepper/tst-mtrace.c 的第 33 行调用了四次分配函数,而在程序终止之前没有释放此内存。这是否是一个真正的问题还有待调查。

2.2.5. 替换malloc

Replacing malloc

GNU C 库支持用具有相同接口的不同分配器替换内置 malloc 实现。对于动态链接的程序,这通过 ELF 符号插入发生,或者使用共享对象依赖关系或 LD_PRELOAD。对于静态链接,必须先链接 m​​alloc 替换库,然后再链接 libc.a(显式或隐式)。

注意:未能提供完整的替换函数集(即应用程序、GNU C 库和其他链接库使用的所有函数)可能导致静态链接失败,并在运行时导致堆损坏和应用程序崩溃。替换函数应该实现它们在 GNU C 库中对应的行为;例如,替换free 也应该保留errno。

下表给出了自定义 malloc 必须提供的最小功能集。

malloc
free
calloc
realloc

GNU C 库需要这些与 malloc 相关的函数才能工作。

GNU C 库中的 malloc 实现提供了库本身不使用的附加功能,但其他系统库和应用程序经常使用这些功能。通用替代 malloc 实现也应该提供这些函数的定义。它们的名称列在下表中。

aligned_alloc
malloc_usable_size
memalign
posix_memalign
pvalloc
valloc

此外,非常旧的应用程序可能会使用过时的 cfree 函数。

其他与 malloc 相关的函数(例如 mallopt 或 mallinfo2)在使用替换 malloc 时不会产生任何影响或返回不正确的统计信息。但是,未能替换这些函数通常不会导致崩溃或其他不正确的应用程序行为,但可能会导致静态链接失败。

GNU C 库中还有其他函数(reallocarray、strdup 等)未在上面列出,但会将新分配的内存返回给调用者。不支持替换这些函数,并且可能会产生不正确的结果。这些函数的 GNU C 库实现在可用时调用替换分配器函数,因此它们可以与 malloc 替换一起正常工作。

2.2.6. Obstacks

obstack 是一个包含一堆对象的内存池。您可以创建任意数量的单独 obstacks,然后在指定的 obstacks 中分配对象。在每个 obstack 中,最后分配的对象必须始终是第一个释放的对象,但不同的 obstack 彼此独立。

除了释放顺序的这一限制之外,obstack 是完全通用的:一个 obstack 可以包含任意数量的任意大小的对象。它们是用宏实现的,因此只要对象通常很小,分配通常非常快。每个对象唯一的空间开销是在合适的边界上启动每个对象所需的填充。

2.2.6.1. 创建 Obstacks

Creating Obstacks

用于操作 obstack 的实用程序在头文件 obstack.h 中声明。

数据类型:struct obstack

obstack 由 struct obstack 类型的数据结构表示。这种结构有一个小的固定大小;它记录了 obstack 的状态以及如何找到分配对象的空间。它不包含任何对象本身。您不应该尝试直接访问结构的内容;仅使用本章中描述的功能。

您可以声明 struct obstack 类型的变量并将它们用作 obstacks,或者您可以像任何其他类型的对象一样动态分配 obstacks。obstacks 的动态分配允许您的程序具有可变数量的不同堆栈。(你甚至可以在另一个 obstack 中分配一个 obstack 结构,但这很少有用。)所有使用 obstack 的函数都需要你指定使用哪个 obstack。

您可以使用 struct obstack * 类型的指针来执行此操作。在下文中,当严格来说手头的对象就是这样一个指针时,我们经常说“一个 obstack”。

obstack 中的对象被打包成称为块的大块。struct obstack 结构指向当前正在使用的块链。

每当您分配一个不适合前一个块的对象时,obstack 库都会获得一个新块。由于 obstack 库会自动管理块,因此您无需过多关注它们,但您确实需要提供 obstack 库应该用来获取块的函数。通常你提供一个直接或间接使用 malloc 的函数。您还必须提供一个函数来释放一个块。这些事项将在下一节中描述。

2.2.6.2. 准备使用 Obstacks

Preparing for Using Obstacks

您计划在其中使用 obstack 函数的每个源文件都必须包含头文件 obstack.h,如下所示:

#include <obstack.h>

此外,如果源文件使用宏 obstack_init,它必须声明或定义两个将由 obstack 库调用的函数或宏。一,obstack_chunk_alloc,用于分配对象被打包的内存块。另一个 obstack_chunk_free 用于在释放其中的对象时返回块。这些宏应该出现在源文件中任何使用 obstacks 之前。

通常这些被定义为通过中介 xmalloc 使用 malloc。这是通过以下一对宏定义完成的:

#define obstack_chunk_alloc xmalloc
#define obstack_chunk_free free

虽然使用 obstacks 获得的内存确实来自 malloc,但使用 obstacks 更快,因为 malloc 被调用的频率较低,用于更大的内存块。

在运行时,在程序可以使用 struct obstack 对象作为 obstack 之前,它必须通过调用 obstack_init 来初始化 obstack。

函数:int obstack_init (struct obstack *obstack-ptr)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Safe mem | See POSIX Safety Concepts.

初始化 obstack obstack-ptr 以分配对象。该函数调用 obstack 的 obstack_chunk_alloc 函数。如果内存分配失败,则调用 obstack_alloc_failed_handler 指向的函数。obstack_init 函数始终返回 1(兼容性注意:如果分配失败,旧版本的 obstack 返回 0)。

下面是两个如何为 obstack 分配空间并初始化它的示例。首先,一个是静态变量的 obstack:

static struct obstack myobstack;
...
obstack_init (&myobstack);

其次,一个本身是动态分配的 obstack:

struct obstack *myobstack_ptr
  = (struct obstack *) xmalloc (sizeof (struct obstack));
obstack_init (myobstack_ptr);

变量:obstack_alloc_failed_handler

这个变量的值是一个指向 obstack 在 obstack_chunk_alloc 分配内存失败时使用的函数的指针。默认操作是打印消息并中止。您应该提供一个函数,该函数调用 exit 或 longjmp 并且不返回。

void my_obstack_alloc_failed (void)
...
obstack_alloc_failed_handler = &my_obstack_alloc_failed;
2.2.6.3. Obstack 中的分配

Allocation in an Obstack

在 obstack 中分配对象最直接的方法是使用 obstack_alloc,它的调用方式几乎与 malloc 类似。

函数:void * obstack_alloc (struct obstack *obstack-ptr, int size)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

这会在 obstack 中分配一个未初始化的 size 字节块并返回其地址。这里 obstack-ptr 指定在哪个 obstack 中分配块;它是表示 obstack 的 struct obstack 对象的地址。每个 obstack 函数或宏都要求您指定 obstack-ptr 作为第一个参数。

如果需要分配新的内存块,该函数调用 obstack 的 obstack_chunk_alloc 函数;如果 obstack_chunk_alloc 分配内存失败,它会调用 obstack_alloc_failed_handler。

例如,这是一个函数,它在特定的 obstack 中分配字符串 str 的副本,该 obstack 位于变量 string_obstack 中:

struct obstack string_obstack;

char *
copystring (char *string)
{
    size_t len = strlen (string) + 1;
    char *s = (char *) obstack_alloc (&string_obstack, len);
    memcpy (s, string, len);
    return s;
}

要分配具有指定内容的块,请使用函数 obstack_copy,声明如下:

函数:void * obstack_copy (struct obstack *obstack-ptr, void *address, int size)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

这会分配一个块并通过从 address 开始复制 size 个字节的数据来初始化它。如果 obstack_chunk_alloc 分配内存失败,它会调用 obstack_alloc_failed_handler。

函数:void * obstack_copy0 (struct obstack *obstack-ptr, void *address, int size)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

与 obstack_copy 类似,但附加了一个包含空字符的额外字节。这个额外的字节不计入参数大小。

obstack_copy0 函数便于将字符序列作为空终止字符串复制到 obstack 中。以下是它的使用示例:

char *
obstack_savestring (char *addr, int size)
{
    return obstack_copy0 (&myobstack, addr, size);
}

将此与前面使用 malloc 的保存字符串示例进行对比。

2.2.6.4. 释放 Obstack 中的对象

Freeing Objects in an Obstack

要释放在 obstack 中分配的对象,请使用函数 obstack_free。由于 obstack 是一堆对象,释放一个对象会自动释放最近在同一 obstack 中分配的所有其他对象。

函数:void obstack_free (struct obstack *obstack-ptr, void *object)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt | See POSIX Safety Concepts.

如果 object 是空指针,则释放 obstack 中分配的所有内容。否则,object 必须是在 obstack 中分配的对象的地址。然后释放对象,以及从对象开始在 obstack-ptr 中分配的所有内容。

请注意,如果 object 是空指针,则结果是未初始化的 obstack。要释放 obstack 中的所有内存,但使其有效以供进一步分配,请使用 obstack 上分配的第一个对象的地址调用 obstack_free:

obstack_free (obstack_ptr, first_object_allocated_ptr);

回想一下 obstack 中的对象被分组为块。当块中的所有对象都空闲时,obstack 库会自动释放块。然后其他的 obstacks,或者非 obstack 分配,可以重用 chunk 的空间。

2.2.6.5. Obstack 函数和宏

Obstack Functions and Macros

使用 obstacks 的接口可以定义为函数或宏,这取决于编译器。obstack 工具适用于所有 C 编译器,包括 ISO C 和传统 C 编译器,但如果您计划使用 GNU C 以外的编译器,则必须采取预防措施。

如果您使用的是老式的非 ISO C 编译器,所有 obstack “函数”实际上都仅定义为宏。您可以像调用函数一样调用这些宏,但不能以任何其他方式使用它们(例如,不能获取它们的地址)。

调用宏需要特别注意:即,第一个操作数(obstack 指针)可能不包含任何副作用,因为它可能会被计算多次。例如,如果你这样写:

obstack_alloc(get_obstack(), 4);

你会发现 get_obstack 可能会被多次调用。如果你使用 *obstack_list_ptr++ 作为 obstack 指针参数,你会得到非常奇怪的结果,因为增量可能会发生多次。

在 ISO C 中,每个函数都有一个宏定义和一个函数定义。如果您在不调用函数的情况下获取函数的地址,则使用函数定义。普通调用默认使用宏定义,但您可以通过在括号中写入函数名称来请求函数定义,如下所示:

char *x;
void *(*funcp) ();
/* Use the macro. */
x = (char *) obstack_alloc (obptr, size);
/* Call the function. */
x = (char *) (obstack_alloc) (obptr, size);
/* Take the address of the function. */
funcp = obstack_alloc;

这与 ISO C 中标准库函数的情况相同。

警告:当您使用宏时,您必须注意避免在第一个操作数中产生副作用,即使在 ISO C 中也是如此。

如果您使用 GNU C 编译器,则不需要这种预防措施,因为 GNU C 中的各种语言扩展允许定义宏以便每个参数只计算一次。

2.2.6.6. 生长对象

Growing Objects

由于 obstack 块中的内存是按顺序使用的,因此可以逐步构建一个对象,一次将一个或多个字节添加到对象的末尾。使用这种技术,您无需知道将在对象中放入多少数据,直到您完成它。我们称之为生长对象的技术。本节介绍了向生长对象添加数据的特殊功能。

当你开始生长一个对象时,你不需要做任何特别的事情。使用其中一个函数向对象添加数据会自动启动它。但是,有必要明确说明对象何时完成。这是通过函数 obstack_finish 完成的。

这样构建的对象的实际地址在对象完成之前是未知的。在那之前,您总是有可能添加太多数据,以至于必须将对象复制到新的块中。

当 obstack 用于一个生长的对象时,你不能将它用于另一个对象的普通分配。如果您尝试这样做,已添加到正在生长的对象的空间将成为另一个对象的一部分。

函数:void obstack_blank (struct obstack *obstack-ptr, int size)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

添加到生长对象的最基本函数是 obstack_blank,它在不初始化的情况下添加空间。

函数:void obstack_grow (struct obstack *obstack-ptr, void *data, int size)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

要添加一块已初始化的空间,请使用 obstack_grow,它是 obstack_copy 的生长对象类似物。它将 size 字节的数据添加到不断生长的对象中,从数据中复制内容。

函数:void obstack_grow0 (struct obstack *obstack-ptr, void *data, int size)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

这是 obstack_copy0 的生长对象类似物。它添加从数据复制的 size 字节,后跟一个额外的空字符。

函数:void obstack_1grow (struct obstack *obstack-ptr, char c)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

要一次添加一个字符,请使用函数 obstack_1grow。它将包含 c 的单个字节添加到正在生长的对象中。

函数:void obstack_ptr_grow (struct obstack *obstack-ptr, void *data)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

添加指针的值可以使用函数 obstack_ptr_grow。它添加包含数据值的 sizeof (void *) 字节。

函数:void obstack_int_grow (struct obstack *obstack-ptr, int data)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

可以使用 obstack_int_grow 函数添加一个 int 类型的值。它将 sizeof (int) 字节添加到不断生长的对象中,并使用 data 的值对其进行初始化。

函数:void * obstack_finish (struct obstack *obstack-ptr)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt | See POSIX Safety Concepts.

当您完成对象的增长后,使用函数 obstack_finish 将其关闭并返回其最终地址。

完成对象后,obstack 可用于普通分配或增长另一个对象。

此函数可以在与 obstack_alloc 相同的条件下返回一个空指针。

当你通过增长来构建一个对象时,你可能需要知道它后来变成了多长时间。当你生长对象时,你不需要跟踪这个,因为你可以在使用 obstack_object_size 函数完成对象之前从 obstack 中找出长度,声明如下:

函数:int obstack_object_size (struct obstack *obstack-ptr)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Safe | See POSIX Safety Concepts.

此函数返回生长对象的当前大小,以字节为单位。请记住在完成对象之前调用此函数。完成后,obstack_object_size 将返回零。

如果您已经开始增长一个对象并希望取消它,您应该完成它然后释放它,如下所示:

obstack_free (obstack_ptr, obstack_finish (obstack_ptr));

如果没有对象在增长,这将无效。

您可以使用带有负大小参数的 obstack_blank 来使当前对象更小。只是不要试图将其缩小到零长度以上——如果你这样做,不知道会发生什么。

2.2.6.7. 超快速生长的对象

Extra Fast Growing Objects

用于生长对象的常用函数会产生检查当前块中是否有新增长空间的开销。如果您经常以小步长的方式构建对象,那么这种开销可能会很大。

您可以通过使用特殊的“快速增长”函数来减少开销,这些函数无需检查即可生长对象。为了拥有一个健壮的程序,您必须自己进行检查。如果每次要向对象添加数据时都以最简单的方式进行检查,那么您还没有保存任何内容,因为这是普通增长函数所做的。但是,如果您可以减少检查频率,或者更有效地检查,那么您可以使程序更快。

obstack_room 函数返回当前块中可用的空间量。声明如下:

函数:int obstack_room (struct obstack *obstack-ptr)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Safe | See POSIX Safety Concepts.

这将返回可以使用快速增长函数安全地添加到 obstack obstack-ptr 中当前生长对象(或即将启动的对象)的字节数。

虽然您知道有空间,但您可以使用这些快速增长函数将数据添加到不断生长的对象:

函数:void obstack_1grow_fast (struct obstack *obstack-ptr, char c)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

函数 obstack_1grow_fast 将一个包含字符 c 的字节添加到 obstack obstack-ptr 中的生长对象。

函数:void obstack_ptr_grow_fast (struct obstack *obstack-ptr, void *data)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Safe | See POSIX Safety Concepts.

函数 obstack_ptr_grow_fast 将包含数据值的 sizeof (void *) 字节添加到 obstack obstack-ptr 中的生长对象。

函数:void obstack_int_grow_fast (struct obstack *obstack-ptr, int data)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Safe | See POSIX Safety Concepts.

obstack_int_grow_fast 函数将包含数据值的 sizeof (int) 个字节添加到 obstack obstack-ptr 中的生长对象中。

函数:void obstack_blank_fast (struct obstack *obstack-ptr, int size)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Safe | See POSIX Safety Concepts.

函数 obstack_blank_fast 将 size 字节添加到 obstack obstack-ptr 中不断生长的对象而不初始化它们。

当您使用 obstack_room 检查空间并且没有足够的空间来添加您要添加的内容时,快速增长功能是不安全的。在这种情况下,只需使用相应的普通增长函数即可。很快,这会将对象复制到一个新块中;那么将有很多可用的空间。

因此,每次使用普通的增长函数时,请使用 obstack_room 检查是否有足够的空间。一旦对象被复制到一个新的块中,就会再次有足够的空间,因此程序将再次开始使用快速增长功能。

这是一个例子:

void
add_string (struct obstack *obstack, const char *ptr, int len)
{
    while (len > 0)
    {
        int room = obstack_room (obstack);
        if (room == 0)
        {
            /* Not enough room. Add one character slowly,
            which may copy to a new chunk and make room. */
            obstack_1grow (obstack, *ptr++);
            len--;
        }
        else
        {
            if (room > len)
                room = len;
            /* Add fast as much as we have room for. */
            len -= room;
            while (room-- > 0)
                obstack_1grow_fast (obstack, *ptr++);
        }
    }
}
2.2.6.8. Obstack 的状态

Status of an Obstack

以下是提供有关 obstack 中当前分配状态信息的函数。您可以使用它们来了解对象,同时仍在增长它。

函数:void * obstack_base (struct obstack *obstack-ptr)

Preliminary: | MT-Safe | AS-Unsafe corrupt | AC-Safe | See POSIX Safety Concepts.

该函数返回 obstack-ptr 中当前生长对象的起始地址。如果您立即完成该对象,它将具有该地址。如果你先把它变大,它可能会超过当前块——然后它的地址就会改变!

如果没有对象在增长,则该值表示您分配的下一个对象将从哪里开始(再次假设它适合当前块)。

函数:void * obstack_next_free (struct obstack *obstack-ptr)

Preliminary: | MT-Safe | AS-Unsafe corrupt | AC-Safe | See POSIX Safety Concepts.

此函数返回 obstack obstack-ptr 当前块中第一个空闲字节的地址。这是当前生长对象的结束。如果没有对象在增长,obstack_next_free 返回与 obstack_base 相同的值。

函数:int obstack_object_size (struct obstack *obstack-ptr)

Preliminary: | MT-Safe race:obstack-ptr | AS-Safe | AC-Safe | See POSIX Safety Concepts.

此函数返回当前生长对象的大小(以字节为单位)。这相当于

obstack_next_free (obstack-ptr) - obstack_base (obstack-ptr)
2.2.6.9. Obstacks中的数据对齐

Alignment of Data in Obstacks

每个 obstack 都有一个对齐边界;在 obstack 中分配的每个对象都会自动从指定边界的倍数的地址开始。默认情况下,此边界是对齐的,以便对象可以保存任何类型的数据。

要访问 obstack 的对齐边界,请使用宏 obstack_alignment_mask,其函数原型如下所示:

宏:int obstack_alignment_mask (struct obstack *obstack-ptr)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

该值是位掩码;位为 1 表示对象地址中的对应位应为 0。掩码值应为 2 的幂次方小 1;效果是所有对象地址都是 2 的幂的倍数。掩码的默认值是允许对齐对象保存任何类型数据的值:例如,如果它的值为 3,则任何类型的数据都可以 存储在地址为 4 的倍数的位置。掩码值为 0 表示对象可以从 1 的任何倍数开始(即不需要对齐)。

宏 obstack_alignment_mask 的扩展是一个左值,所以你可以通过赋值来改变掩码。例如,这个语句:

obstack_alignment_mask (obstack_ptr) = 0;

具有关闭指定 obstack 中对齐处理的效果。

请注意,对齐掩码的更改直到下一次在 obstack 中分配或完成对象之后才会生效。如果你没有增长一个对象,你可以通过调用 obstack_finish 使新的对齐掩码立即生效。这将完成一个长度为零的对象,然后为下一个对象进行适当的对齐。

2.2.6.10. Obstack块

Obstack Chunks

Obstack 的工作原理是为自己分配大块的空间,然后在这些块中分配空间以满足您的请求。除非您指定不同的块大小,否则块的长度通常为 4096 字节。块大小包括 8 个字节的开销,这些开销实际上并未用于存储对象。无论指定的大小如何,都会在必要时为长对象分配更长的块。

obstack 库通过调用您必须定义的函数 obstack_chunk_alloc 来分配块。当一个块不再需要时,因为您已经释放了其中的所有对象,obstack 库通过调用 obstack_chunk_free 来释放该块,您还必须定义它。

这两个必须在每个使用 obstack_init 的源文件中定义(作为宏)或声明(作为函数)。大多数情况下,它们被定义为这样的宏:

#define obstack_chunk_alloc malloc
#define obstack_chunk_free free

请注意,这些是简单的宏(无参数)。带参数的宏定义不起作用!如果 obstack_chunk_alloc 或 obstack_chunk_free 本身不是函数名,则必须单独扩展为函数名。

如果您使用 malloc 分配块,则块大小应该是 2 的幂。选择默认块大小 4096 是因为它足够长,可以满足 obstack 上的许多典型请求,但又足够短,不会浪费太多内存。最后一个块的一部分尚未使用。

宏:int obstack_chunk_size (struct obstack *obstack-ptr)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

这将返回给定 obstack 的块大小。

由于此宏扩展为左值,因此您可以通过为其分配一个新值来指定新的块大小。这样做不会影响已经分配的块,但会改变将来为该特定 obstack 分配的块的大小。使块大小变小不太可能有用,但是如果您分配许多大小与块大小相当的对象,则将其变大可能会提高效率。以下是如何干净地做到这一点:

if (obstack_chunk_size (obstack_ptr) < new-chunk-size)
    obstack_chunk_size (obstack_ptr) = new-chunk-size;
2.2.6.11. Obstack函数总结

Summary of Obstack Functions

这是与 obstacks 相关的所有功能的摘要。每个都将 obstack 的地址(struct obstack *)作为其第一个参数。

void obstack_init (struct obstack *obstack-ptr)
    初始化 obstack 的使用。

void *obstack_alloc (struct obstack *obstack-ptr, int size)
    分配一个大小为未初始化字节的对象。

void *obstack_copy (struct obstack *obstack-ptr, void *address, int size)
    分配一个大小字节的对象,其内容从地址复制。

void *obstack_copy0 (struct obstack *obstack-ptr, void *address, int size)
    分配一个 size+1 字节的对象,它们的大小是从地址复制的,最后是一个空字符。

void obstack_free (struct obstack *obstack-ptr, void *object)
    空闲对象(以及在指定 obstack 中分配的所有内容比对象更近)。

void obstack_blank (struct obstack *obstack-ptr, int size)
    将 size 未初始化的字节添加到不断生长的对象。

void obstack_grow (struct obstack *obstack-ptr, void *address, int size)
    将从地址复制的大小字节添加到生长的对象。

void obstack_grow0 (struct obstack *obstack-ptr, void *address, int size)
    将从地址复制的 size 字节添加到生长的对象,然后添加另一个包含空字符的字节。

void obstack_1grow (struct obstack *obstack-ptr, char data-char)
    将一个包含 data-char 的字节添加到不断生长的对象中。

void *obstack_finish (struct obstack *obstack-ptr)
    最终确定正在生长的对象并返回其永久地址。

int obstack_object_size (struct obstack *obstack-ptr)
    获取当前生长对象的当前大小。

void obstack_blank_fast (struct obstack *obstack-ptr, int size)
    在不检查是否有足够空间的情况下将 size 未初始化字节添加到不断生长的对象。

void obstack_1grow_fast (struct obstack *obstack-ptr, char data-char)
    将一个包含 data-char 的字节添加到一个不断生长的对象中,而不检查是否有足够的空间。

int obstack_room (struct obstack *obstack-ptr)
    获取现在可用于增长当前对象的空间量。

int obstack_alignment_mask (struct obstack *obstack-ptr)
    用于对齐对象开头的蒙版。这是一个左值。

int obstack_chunk_size (struct obstack *obstack-ptr)
    分配块的大小。这是一个左值。

void *obstack_base (struct obstack *obstack-ptr)
    当前生长对象的暂定起始地址。

void *obstack_next_free (struct obstack *obstack-ptr)
    在当前生长的对象结束之后的地址。

2.2.7. 可变大小的自动存储

Automatic Storage with Variable Size

函数 alloca 支持一种半动态分配,其中块是动态分配但自动释放的。

使用 alloca 分配块是一个显式操作;您可以根据需要分配任意数量的块,并在运行时计算大小。但是,当您退出调用 alloca 的函数时,所有块都会被释放,就像它们是在该函数中声明的自动变量一样。没有办法明确地释放空间。

alloca 的原型在 stdlib.h 中。这个函数是一个 BSD 扩展。

函数:void * alloca (size t size)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

alloca 的返回值是一个 size 字节的内存块的地址,分配在调用函数的栈帧中。

不要在函数调用的参数中使用 alloca——你会得到不可预知的结果,因为 alloca 的栈空间会出现在函数参数空间的中间的栈上。要避免的一个例子是 foo (x, alloca (4), y)。

2.2.7.1. alloca示例

alloca Example

作为使用 alloca 的示例,这里有一个函数,它打开一个由两个参数字符串连接而成的文件名,并返回一个文件描述符或-1表示失败:

int
open2 (char *str1, char *str2, int flags, int mode)
{
    char *name = (char *) alloca (strlen (str1) + strlen (str2) + 1);
    stpcpy (stpcpy (name, str1), str2);
    return open (name, flags, mode);
}

以下是使用 malloc 和 free 获得相同结果的方法:

int
open2 (char *str1, char *str2, int flags, int mode)
{
    char *name = malloc (strlen (str1) + strlen (str2) + 1);
    int desc;
    if (name == 0)
        fatal ("virtual memory exceeded");
    stpcpy (stpcpy (name, str1), str2);
    desc = open (name, flags, mode);
    free (name);
    return desc;
}

如您所见,使用 alloca 更简单。但是 alloca 还有其他更重要的优点和一些缺点。

2.2.7.2. alloca的优势

Advantages of alloca

以下是 alloca 可能优于 malloc 的原因:

  • 使用 alloca 浪费的空间非常小,而且速度非常快。(它由 GNU C 编译器开放编码。)

  • 由于 alloca 没有用于不同大小的块的单独池,因此用于任何大小块的空间都可以重用于任何其他大小。alloca 不会导致内存碎片。

  • 使用 longjmp 完成的非本地退出在通过调用 alloca 的函数退出时自动释放使用 alloca 分配的空间。这是使用 alloca 的最重要原因。

    为了说明这一点,假设您有一个函数 open_or_report_error 如果成功则返回一个描述符,如 open,但如果失败则不返回其调用者。如果文件无法打开,它会打印一条错误消息并使用 longjmp 跳到程序的命令级别。让我们更改 open2 以使用此子例程:

    int
    open2 (char *str1, char *str2, int flags, int mode)
    {
        char *name = (char *) alloca (strlen (str1) + strlen (str2) + 1);
        stpcpy (stpcpy (name, str1), str2);
        return open_or_report_error (name, flags, mode);
    }
    

由于 alloca 的工作方式,即使发生错误,它分配的内存也会被释放,无需特别努力。

相比之下,如果以这种方式更改 open2 的先前定义(使用 malloc 和 free),则会产生内存泄漏。即使您愿意进行更多更改来修复它,也没有简单的方法可以做到这一点。

2.2.7.3. alloca的缺点

Disadvantages of alloca

这些是 alloca 与 malloc 相比的缺点:

  • 如果您尝试分配的内存超出机器所能提供的范围,您将不会收到干净的错误消息。相反,你会得到一个致命的信号,就像你从无限递归中得到的一样;可能是分段违规(请参阅程序错误信号)。
  • 一些非 GNU 系统不支持 alloca,因此它的可移植性较差。然而,用 C 语言编写的 alloca 的较慢仿真可用于具有此缺陷的系统。
2.2.7.4. GNU C可变大小数组

在 GNU C 中,您可以用一个可变大小的数组来替换大多数使用的 alloca。下面是 open2 的样子:

int open2 (char *str1, char *str2, int flags, int mode)
{
    char name[strlen (str1) + strlen (str2) + 1];
    stpcpy (stpcpy (name, str1), str2);
    return open (name, flags, mode);
}

但是 alloca 并不总是等价于可变大小的数组,原因如下:

  • 可变大小数组的空间在数组名称范围的末尾被释放。用 alloca 分配的空间一直保留到函数结束。
  • 可以在循环中使用 alloca,在每次迭代时分配一个额外的块。这对于可变大小的数组是不可能的。

注意:如果在一个函数中混合使用 alloca 和可变大小的数组,退出声明了可变大小数组的作用域会释放在该作用域执行期间分配有 alloca 的所有块。

2.3. 调整数据段的大小

Resizing the Data Segment

本节中的符号在 unistd.h 中声明。

您通常不会使用本节中的函数,因为为程序数据分配存储中描述的函数更易于使用。这些是 GNU C 库内存分配器的接口,它使用下面的函数。下面的函数是系统调用的简单接口。

函数:int brk (void *addr)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

brk 将调用进程的数据段的高端设置为 addr。

段的结束地址定义为段中最后一个字节的地址加 1。

如果 addr 低于数据段的低端,则该函数无效。(顺便说一句,这被认为是成功的。)

如果它会导致数据段与另一个段重叠或超过进程的数据存储限制,则该函数将失败。

该函数以数据存储和栈在同一段中的常见历史案例命名。数据存储分配从段的底部向上增长,而栈从段的顶部向下增长,它们之间的帷幕称为中断(break)。

成功时返回值为零。失败时,返回值为 -1 并相应地设置 errno。以下 errno 值特定于此函数:

  • ENOMEM 该请求将导致数据段与另一个段重叠或超出进程的数据存储限制。

函数:void *sbrk (ptrdiff t delta)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

此函数与 brk 相同,只是您将数据段的新端指定为距当前端的 delta 偏移量,并且成功时返回值是数据段的结果端的地址而不是零。

这意味着您可以使用“sbrk(0)”来找出数据段的当前结尾是什么。

2.4. 内存保护

Memory Protection

当使用 mmap 映射页面时,可以使用 protection flags 参数指定页面保护标志。请参阅内存映射 I/O

以下标志可用:

  • PROT_WRITE

    内存可以写入。

  • PROT_READ

    内存可以读取。在某些架构上,此标志意味着也可以执行内存(就好像同时指定了 PROT_EXEC 一样)。

  • PROT_EXEC

    存储器可用于存储随后可以执行的指令。在大多数架构上,此标志意味着可以读取内存(就像指定了 PROT_READ 一样)。

  • PROT_NONE

    该标志必须单独指定。

    内存被保留,但不能读取、写入或执行。如果在对 mmap 的调用中指定了此标志,则将留出一个虚拟内存区域以供将来在进程中使用,并且没有 MAP_FIXED 标志的 mmap 调用将不会将其用于后续分配。对于匿名映射,内核在创建映射时不会为分配保留任何物理内存。

操作系统可能会单独跟踪这些标志,即使底层硬件出于访问检查的目的将它们视为相同(就像某些平台上的 PROT_READ 和 PROT_EXEC 一样)。在 GNU 系统上,PROT_EXEC 总是意味着 PROT_READ,以便用户可以查看在他们的系统上执行的机器代码。

不适当的访问将导致段错误。

分配后,可以使用 mprotect 函数更改保护标志。

函数:int mprotect (void *address, size t length, int protection)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

成功调用 mprotect 函数会更改从 address 开始的至少 length 字节内存的保护标志。

地址必须与映射的页面大小对齐。系统页面大小可以通过使用 _SC_PAGESIZE 参数调用 sysconf 来获得。系统页面大小是匿名内存映射和大多数文件映射的页面保护可以更改的粒度。从特殊文件或设备映射的内存可能具有比系统页面大小更大的页面粒度,并且可能需要更大的对齐。

length 是必须更改其保护标志的字节数。它会自动四舍五入到系统页面大小的下一个倍数。

protection 是上述 PROT_* 标志的组合。

mprotect 函数在成功时返回 0,在失败时返回 -1。

为此函数定义了以下 errno 错误条件:

  • ENOMEM
    系统无法分配资源来满足请求。如果系统中没有足够的物理内存来分配后备存储,就会发生这种情况。如果新的保护标志会导致内存区域与其相邻区域分离,并且超出此类不同内存区域数量的进程限制,也会发生该错误。
  • EINVAL
    地址未正确对齐到映射的页面边界,或者长度(向上舍入到系统页面大小后)不是映射的适用页面大小的倍数,或者保护中的标志组合无效。
  • EACCES
    基于文件的映射的文件未使用与保护兼容的打开标志打开。
  • EPERM
    系统安全策略不允许使用指定标志的映射。例如,可能不允许同时为 PROT_EXEC 和 PROT_WRITE 的映射。

如果使用 mprotect 函数通过指定 PROT_NONE 保护标志来使内存区域不可访问,并且稍后恢复访问,则内存将保留其先前的内容。

在某些系统上,可能无法指定首次创建映射时不存在的其他标志。例如,如果初始保护标志为“PROT_READ | PROT_WRITE”,则使内存区域可执行的尝试可能会失败。

通常,mprotect 函数可用于更改任何进程内存,无论它是如何分配的。但是,该函数的可移植使用要求它仅与 mmap 或 mmap64 返回的内存区域一起使用。

2.4.1. 内存保护键

Memory Protection Keys

在某些系统上,可以使用内存保护键向特定页面添加更多限制。这些限制的作用如下:

  • 所有内存页面都与保护键相关联。默认保护键不会导致在内存访问期间应用任何额外的保护。可以使用 pkey_alloc 函数分配新键,并使用 pkey_mprotect 将其应用于页面。
  • 每个线程对每个保护键都有一组单独的访问权限限制。这些访问权限可以使用 pkey_set 和 pkey_get 函数进行操作。
  • 在内存访问期间,系统获取访问页面的保护键并使用它来确定适用的访问权限,如为当前线程配置的那样。如果访问受到限制,则会导致段错误,这些检查是在 mprotect 或 pkey_mprotect 设置的 PROT_* 保护标志之外发生的。

新线程和子进程继承当前线程的访问权限。如果随后分配了保护键,现有线程(当前线程除外)将对与新分配的键关联的访问权限使用未指定的系统默认值。

进入信号处理程序后,系统会重置当前线程的访问权限,以便可以访问具有默认键的页面,但未指定其他保护键的访问权限。

应用程序应使用 pkey_alloc 分配一次键,并将键应用于需要使用 pkey_mprotect 进行特殊保护的内存区域:

int key = pkey_alloc (0, PKEY_DISABLE_ACCESS);
if (key < 0)
    /* Perform error checking, including fallback for lack of support. */
    ...;
/* Apply the key to a special memory region used to store critical
   data. */
if (pkey_mprotect (region, region_length,
        PROT_READ | PROT_WRITE, key) < 0)
    ...; /* Perform error checking (generally fatal). */

如果由于缺乏对内存保护键的支持而导致键分配失败,通常可以跳过 pkey_mprotect 调用。在这种情况下,该区域将默认不受保护。也可以使用键值为 -1 调用 pkey_mprotect,在这种情况下,它的行为方式与 mprotect 相同。

在分配给内存页面的键分配后,pkey_set 可用于临时获取对内存区域的访问权并再次放弃它:

if (key >= 0 && pkey_set (key, 0) < 0)
    ...; /* Perform error checking (generally fatal). */
/* At this point, the current thread has read-write access to the
memory region. */
...
/* Revoke access again. */
if (key >= 0 && pkey_set (key, PKEY_DISABLE_ACCESS) < 0)
    ...; /* Perform error checking (generally fatal). */

在这个例子中,一个负的键值表示没有分配任何键,这意味着系统缺乏对内存保护键的支持,不需要改变当前线程的访问权限(因为它总是有访问权限)。

与使用 mprotect 更改页面保护标志相比,这种方法有两个优点: 它是线程安全的,即仅更改当前线程的访问权限,因此另一个线程同时更改自己的访问权限以获得访问权限 映射不会突然看到其访问权限被撤销。而且 pkey_set 通常不涉及对内核的调用和上下文切换,因此效率更高。

函数:int pkey_alloc (unsigned int flags, unsigned int restrictions)

Preliminary: | MT-Safe | AS-Safe | AC-Unsafe corrupt | See POSIX Safety Concepts.

分配新的保护键。flags 参数是保留的,必须为零。限制参数指定应用于当前线程的访问权限(就像下面的 pkey_set 一样)。其他线程的访问权限不会改变。

该函数返回新的保护键、一个非负数,或错误时返回 -1。

为此函数定义了以下 errno 错误条件:

ENOSYS
    系统不实施内存保护键。
EINVAL
    flags 参数不为零。
    限制参数无效。
    系统不实施内存保护键或在禁用内存保护键的模式下运行。
ENOSPC
    已分配所有可用的保护键。
    系统不实施内存保护键或在禁用内存保护键的模式下运行。

函数:int pkey_free (int key)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

解除分配保护键,以便它可以被 pkey_alloc 重用。

调用此函数不会更改释放的保护键的访问权限。调用线程和其他线程可以保留对它的访问权,即使它随后被再次分配。因此,不建议调用 pkey_free 函数。

  • ENOSYS 系统不实施内存保护键。
  • EINVAL key 参数不是有效的保护键。

函数:int pkey_mprotect (void *address, size t length, int protection, int key)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

与mprotect类似,但也将内存区域的内存保护键设置为key。

一些系统使用内存保护键来模拟保护标志的某些组合。在这种情况下,指定显式保护键的行为可能就像在保护中指定了其他标志一样,即使默认保护键不会发生这种情况。例如,某些系统只能使用默认保护键支持仅 PROT_EXEC 映射,如果在没有 PROT_READ 的情况下指定 PROT_EXEC,则具有使用 pkey_alloc 分配的键的内存仍然是可读的。

如果 key 为 -1,则将默认保护键应用于映射,就像调用了 mprotect 一样。

pkey_mprotect 函数在成功时返回 0,在失败时返回 -1。为此函数定义了与 mprotect 相同的 errno 错误条件,并添加了以下内容:

  • EINVAL key 参数不是 -1 或使用 pkey_alloc 分配的有效内存保护键。
  • ENOSYS 系统没有实现内存保护key,key不为-1。

函数:int pkey_set (int key, unsigned int rights)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

更改当前线程对内存页的访问权限,使用保护键键为权限。如果权限为零,则不会在页面保护标志之上应用额外的访问限制。否则,权限是以下标志的组合:

  • PKEY_DISABLE_WRITE
    随后尝试使用指定的保护键写入内存将会出错。
  • PKEY_DISABLE_ACCESS
    随后尝试使用指定的保护键写入或读取内存将出错。

未指定为标志的操作不受限制。特别是,这意味着如果使用 PROT_EXEC 保护标志进行映射并且指定了 PKEY_DISABLE_ACCESS,则该内存区域将保持可执行。

使用未由 pkey_alloc 分配的保护键调用 pkey_set 函数会导致未定义的行为。这意味着在不支持内存保护键的系统上调用这个函数是未定义的。

pkey_set 函数在成功时返回 0,在失败时返回 -1。

为此函数定义了以下 errno 错误条件:

  • EINVAL 系统不支持权限参数中表达的访问权限限制。

函数:int pkey_get (int key)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

返回当前线程对带有保护键 key 的内存页的访问权限。返回值为零或 PKEY_DISABLE_* 标志的组合;请参阅 pkey_set 函数。

使用未由 pkey_alloc 分配的保护键调用 pkey_get 函数会导致未定义的行为。这意味着在不支持内存保护键的系统上调用这个函数是未定义的。

2.5. 锁定页面

Locking Pages

您可以告诉系统将特定的虚拟内存页与实际页帧相关联并保持这种方式 - 即,如果页尚未被调入并标记它,则它永远不会被调出,因此 永远不会导致页错误。这称为锁定页面。

本章中的函数锁定和解锁调用进程的页面。

2.5.1. 为什么要锁定页面

Why Lock Pages

因为页面错误会导致已调出的页面被透明地调入,所以进程很少需要关注锁定页面。然而,人们有时有两个原因:

  • 速度。仅当进程对执行简单内存访问所需的时间不敏感时,页面错误才是透明的。对时间要求严格的进程,尤其是实时进程,可能无法等待或无法容忍执行速度的变化。由于这个原因需要锁定页面的进程可能还需要其他进程的优先级以使用 CPU。在某些情况下,程序员比系统的需求分页分配器更清楚哪些页面应该保留在实际内存中以优化系统性能。在这种情况下,锁定页面会有所帮助。
  • 隐私。如果您将秘密保存在虚拟内存中,并且该虚拟内存被分页,则增加了秘密被泄露的机会。例如,如果密码被写出到磁盘交换空间,则在虚拟和真实内存被擦除干净后很长时间它可能仍然存在。

请注意,当您锁定页面时,可用于支持其他虚拟内存(由相同或其他进程)的页面框架减少,这可能意味着更多页面错误,这意味着系统运行更慢。事实上,如果你锁定了足够多的内存,一些程序可能会因为缺乏真正的内存而根本无法运行。

2.5.2. 锁定内存详细信息

Locked Memory Details

内存锁与虚拟页面相关联,而不是与真实帧相关联。分页规则是:如果一个帧至少支持一个锁定页面,则不要将其分页。

内存锁不堆叠。即,您不能两次锁定特定页面,因此必须将其解锁两次才能真正解锁。它要么被锁定,要么没有。

内存锁一直存在,直到拥有内存的进程显式解锁它。(但进程终止和执行会导致虚拟内存不复存在,您可能会说这意味着它不再被锁定)。

内存锁不被子进程继承。(但请注意,在现代 Unix 系统上,在 fork 之后,父级和子级的虚拟地址空间由相同的实际页框支持,因此子级享有父级的锁)。

由于它能够影响其他进程,因此只有超级用户可以锁定页面。任何进程都可以解锁自己的页面。

系统对进程可以锁定的内存量以及它可以专用于它的实际内存量设置了限制。

在 Linux 中,锁定页面并不像您想象的那样被锁定。两个不是共享内存的虚拟页面仍然可以由同一个真实框架支持。当内核知道两个虚拟页面都包含相同的数据时,内核会以效率的名义这样做,即使其中一个或两个虚拟页面都被锁定了也会这样做。

但是当一个进程修改其中一个页面时,内核必须为它获取一个单独的帧并用页面的数据填充它。这称为写时复制页面错误。这需要很短的时间,在病态的情况下,获取该帧可能需要 I/O。

为确保您的程序不会发生这种情况,不要只是锁定页面。也给他们写信,除非你知道你永远不会写信给他们。并且为了确保您为堆栈预先分配了帧,请输入一个声明一个大于您需要的最大堆栈大小的 C 自动变量的范围,将其设置为某个值,然后从其范围返回。

2.5.3. 锁定和解锁页面的功能

Functions To Lock And Unlock Pages

本节中的符号在 sys/mman.h 中声明。这些函数由 POSIX.1b 定义,但它们的可用性取决于您的内核。如果你的内核不允许这些函数,它们存在但总是失败。它们可用于 Linux 内核。

可移植性说明:POSIX.1b 要求当 mlock 和 munlock 函数可用时,文件 unistd.h 定义宏 _POSIX_MEMLOCK_RANGE,文件 limits.h 将宏 PAGESIZE 定义为内存页的大小(以字节为单位)。它要求当 mlockall 和 munlockall 函数可用时,unistd.h 文件定义宏 _POSIX_MEMLOCK。GNU C 库符合这个要求。

函数:int mlock (const void *addr, size t len)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

mlock 锁定一系列调用进程的虚拟页面。

内存范围从地址 addr 开始,长度为 len 个字节。实际上,由于您必须锁定整个页面,因此它是包含指定范围的任何部分的页面范围。

当函数成功返回时,这些页面中的每一个都由(连接到)一个真实框架(常驻)支持,并被标记为保持这种状态。这意味着该函数可能会导致页面进入并且必须等待它们。

当功能失败时,不影响任何页面的锁定状态。

如果函数成功,则返回值为零。否则,它是 -1 并相应地设置 errno。特定于此函数的 errno 值是:

ENOMEM
    - 调用进程的虚拟地址空间中至少有一些指定的地址范围不存在。
    - 锁定将导致进程超出其锁定页限制。
EPERM
    调用进程不是超级用户。
EINVAL
    len 不是正数。
ENOSYS
    内核不提供 mlock 功能。

函数:int mlock2 (const void *addr, size t len, unsigned int flags)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

此功能类似于 mlock。如果 flags 为零,则对 mlock2 的调用与对 mlock 的等效调用完全相同。

flags 参数必须是零个或多个以下标志的组合:

  • MLOCK_ONFAULT

    只有那些在指定地址范围内已经在内存中的页面会被立即锁定。如果出现页面错误和内存分配,该范围内的其他页面会自动锁定。

与 mlock 一样,mlock2 在成功时返回零,在失败时返回 -1,相应地设置 errno。为 mlock2 定义的其他 errno 值是:

  • EINVAL 此系统不支持指定的(非零)标志参数。

您可以使用 mlockall 锁定所有进程的内存。您可以使用 munlock 或 munlockall 解锁内存。

为了避免 C 程序中的所有页面错误,您必须使用 mlockall,因为程序使用的某些内存对 C 代码是隐藏的,例如 堆栈和自动变量,你不知道要告诉 mlock 什么地址。

函数:int munlock (const void *addr, size t len)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

munlock 解锁一系列调用进程的虚拟页面。

munlock 是 mlock 的倒数,其功能与 mlock 完全类似,只是没有 EPERM 故障。

函数:int mlockall (int flags)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

mlockall 锁定进程的虚拟内存地址空间中的所有页面,和/或将来添加到其中的任何页面。这包括代码、数据和堆栈段的页面,以及共享库、用户空间内核数据、共享内存和内存映射文件。

flags 是由以下宏表示的一串单比特标志。他们告诉 mlockall 你想要它的哪些功能。所有其他位必须为零。

  • MCL_CURRENT
    锁定当前存在于调用进程的虚拟地址空间中的所有页面。
  • MCL_FUTURE
    设置一种模式,以便将来添加到进程虚拟地址空间的任何页面从出生时就被锁定。此模式不会影响同一进程拥有的未来地址空间,因此替换进程地址空间的 exec 会清除 MCL_FUTURE。

当函数成功返回并且您指定 MCL_CURRENT 时,所有进程的页面都由(连接到)真实帧(它们是常驻的)支持并被标记为保持这种状态。这意味着该函数可能会导致页面进入并且必须等待它们。

当进程因为成功执行此函数并指定 MCL_CURRENT 而处于 MCL_FUTURE 模式时,如果锁定额外空间会导致进程超出其锁定页数限制。在无法容纳的地址空间添加是堆栈扩展的情况下,堆栈扩展失败,内核向进程发送 SIGSEGV 信号。

当该功能失效时,不影响任何页面的锁定状态或未来的锁定模式。

如果函数成功,则返回值为零。否则,它是 -1 并相应地设置 errno。特定于此函数的 errno 值是:

ENOMEM
    - 调用进程的虚拟地址空间中至少有一些指定的地址范围不存在。
    - 锁定将导致进程超出其锁定页限制。
EPERM
    调用进程不是超级用户。
EINVAL
    标志中的未定义位不为零。
ENOSYS
    内核不提供 mlockall 功能。

您可以使用 mlock 仅锁定特定页面。您可以使用 munlockall 和 munlock 解锁页面。

函数:int munlockall (void)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

munlockall 解锁调用进程的虚拟地址空间中的每个页面,并关闭 MCL_FUTURE 未来锁定模式。

如果函数成功,则返回值为零。否则,它是 -1 并相应地设置 errno。此函数失败的唯一原因是所有函数和系统调用都可能失败的一般原因,因此没有特定的 errno 值。

3. 参考

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值