Linux 基础七 内存

news2025/1/7 15:02:54

在操作系统中,进程的内存布局通常分为几个区域,包括代码段、已初始化的数据段、未初始化的数据段(BSS段)、堆和栈。其中,用于动态内存分配,其大小可以在运行时根据需要增长或收缩。

文章目录

      • 7.1 在堆上分配内存
        • 7.1.1 调整 program break:brk() 和 sbrk()
        • 内存分配的工作过程
        • 限制和注意事项
        • 标准化状态
      • 7.1.2 在堆上分配内存:`malloc()` 和 `free()`
        • `malloc()` 函数
        • `free()` 函数
        • `malloc()` 和 `free()` 的优点
        • 示例程序:`free()` 对 `program break` 的影响
        • 运行结果分析
        • 总结
      • 7.1.3 `malloc()` 和 `free()` 的实现
        • 1. `malloc()` 的实现
        • 2. `free()` 的实现
        • 3. 内存管理和常见错误
        • 4. 总结
      • 4. `malloc` 调试工具和库
        • 1. glibc 提供的 `malloc` 调试工具
          • 1.1 `mtrace()` 和 `muntrace()`
          • 1.2 `mcheck()` 和 `mprobe()`
          • 1.3 `MALLOC_CHECK_` 环境变量
        • 2. 第三方 `malloc` 调试库
          • 2.1 Electric Fence
          • 2.2 dmalloc
          • 2.3 Valgrind
          • 2.4 Insure++
        • 3. 控制和监测 `malloc` 函数包
          • 3.1 `mallopt()`
          • 3.2 `mallinfo()`
        • 4. 总结
      • 5. 在堆上分配内存的其他方法
        • 1. `calloc()`
        • 2. `realloc()`
        • 3. `memalign()` 和 `posix_memalign()`
          • 3.1 `memalign()`
          • 3.2 `posix_memalign()`
        • 4. 总结
      • 7.2 在堆栈上分配内存:`alloca()`
        • 1. `alloca()` 的基本用法
        • 2. `alloca()` 的优势
          • 2.1 自动释放内存
          • 2.2 适用于信号处理程序
          • 2.3 快速分配
        • 3. `alloca()` 的局限性
          • 3.1 堆栈溢出风险
          • 3.2 不能在函数参数列表中调用 `alloca()`
        • 4. `alloca()` 的可移植性
        • 5. 总结

7.1 在堆上分配内存

当程序需要更多的内存时,它可以通过增加堆的大小来实现。堆是一段长度可变的连续虚拟内存,位于进程的未初始化数据段末尾之后。堆的增长是通过移动所谓的“program break”来实现的,这是堆的当前边界。

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

brk()sbrk() 是两个系统调用,它们允许程序直接调整 program break 的位置,从而改变堆的大小。虽然现代的 C 语言程序更倾向于使用 malloc() 等高级内存分配函数,但了解 brk()sbrk() 可以帮助我们理解底层的内存管理机制。

  • brk(void *addr)

    • 作用:将 program break 设置为参数 addr 所指定的位置。
    • 返回值:成功时返回 0,失败时返回 -1。
    • 注意事项:由于虚拟内存是以页为单位分配的,addr 实际会被四舍五入到下一个内存页的边界。如果尝试将 program break 设置为低于初始值的位置,可能会导致未定义行为,例如分段错误(SIGSEGV)。
  • sbrk(intptr_t increment)

    • 作用:将 program break 在原有地址上增加 increment 字节。如果 increment 为正数,则堆会增长;如果为负数,则堆会收缩。
    • 返回值:成功时返回调整前的 program break 地址,失败时返回 (void *)-1
    • 特殊情况:如果 increment 为 0,sbrk(0) 会返回当前的 program break 地址,而不做任何改变。这在调试或监控内存分配时非常有用。
内存分配的工作过程

当程序调用 sbrk()brk() 来增加 program break 时,内核并不会立即为这些新增的虚拟地址分配物理内存页。相反,内核会在进程首次访问这些地址时,通过页面错误(page fault)机制自动分配物理内存页。这种方式称为按需分页(demand paging),它可以提高内存的使用效率,因为只有当程序真正需要这些内存时,才会实际分配物理页。

限制和注意事项
  • 资源限制:program break 的最大值受到进程资源限制的影响,特别是 RLIMIT_DATA,它限制了数据段的最大大小。此外,内存映射区域、共享内存段和共享库的位置也会影响 program break 的上限。

  • 不可逆性:虽然可以使用 sbrk() 减少 program break,但在某些系统上,减少后的内存可能不会立即返回给操作系统,而是保留在进程中,供后续的内存分配使用。

标准化状态

在 POSIX 标准中,brk()sbrk() 被标记为 Legacy(传统),意味着它们在较新的标准中已经被废弃。尽管如此,它们仍然在许多 Unix-like 系统(如 Linux)中可用,并且在一些低级别的内存管理场景中仍然有应用。

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

在 C 语言中,malloc()free() 是用于动态内存分配和释放的函数。它们比底层的 brk()sbrk() 更加高级、易用,并且更适合现代编程的需求。以下是关于这两个函数的详细介绍:

malloc() 函数

malloc() 函数用于在堆上分配指定大小的内存块,并返回指向这块内存的指针。

  • 原型
    #include <stdlib.h>
    void *malloc(size_t size);
    
  • 作用:在堆上分配 size 字节的内存,并返回指向这块内存起始位置的指针。
  • 返回值
    • 成功时返回一个指向已分配内存的指针,类型为 void *,可以赋值给任意类型的指针。
    • 如果无法分配内存(例如因为内存不足),则返回 NULL,并且设置 errno 以指示错误。
  • 对齐方式malloc() 返回的内存块总是按照适当的边界对齐,以便高效访问任何 C 语言数据结构。通常,这意味着内存块会按照 8 字节或 16 字节的边界对齐。
  • malloc(0) 的行为:根据 POSIX 标准(SUSv3),malloc(0) 可以返回 NULL 或者返回一个小的、可以被 free() 释放的内存块。在 Linux 中,malloc(0) 通常返回一个非空指针,这个指针可以安全地传递给 free()
free() 函数

free() 函数用于释放之前由 malloc() 或其他堆分配函数(如 calloc()realloc())分配的内存。

  • 原型
    #include <stdlib.h>
    void free(void *ptr);
    
  • 作用:释放 ptr 指向的内存块。ptr 必须是之前由 malloc()calloc()realloc() 分配的内存块的地址。
  • 行为
    • 如果 ptrNULLfree() 不做任何操作,这是合法的调用。
    • 释放内存后,ptr 指向的内存不再有效,继续使用它会导致未定义行为(例如段错误)。
    • free() 通常不会立即减少程序的 program break,而是将这块内存添加到空闲内存列表中,供后续的 malloc() 调用重用。
  • 为什么 free() 不降低 program break
    • 内存位置:被释放的内存块通常位于堆的中间,而不是堆的顶部。因此,直接降低 program break 是不可能的。
    • 性能优化:频繁调用 sbrk() 系统调用来调整 program break 会带来较大的开销,因此 free() 通常会尽量避免这样做。
    • 内存碎片:如果频繁释放和重新分配小块内存,可能会导致内存碎片化。通过将释放的内存块保留在空闲列表中,malloc() 可以更好地管理这些碎片,提高内存利用率。
malloc()free() 的优点

相比 brk()sbrk()malloc()free() 具有以下优点:

  1. 标准库支持malloc()free() 是 C 语言标准库的一部分,具有广泛的支持和兼容性。
  2. 多线程友好:它们可以在多线程环境中安全使用,而 brk()sbrk() 可能会导致竞态条件。
  3. 灵活的内存管理malloc() 可以分配任意大小的内存块,而 brk()sbrk() 只能调整整个堆的大小。
  4. 自动内存回收free() 会将释放的内存块添加到空闲列表中,供后续的 malloc() 调用重用,从而减少系统调用的频率和内存碎片。
示例程序:free()program break 的影响

下面是一个示例程序,展示了 free() 如何影响 program break。该程序分配了多个内存块,然后根据命令行参数释放部分或全部内存,并观察 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;

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

    // 解析命令行参数
    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");
    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;
    freeMin = (argc > 4) ? getInt(argv[4], GN_GT_0, "min") : 1;
    freeMax = (argc > 5) ? getInt(argv[5], GN_GT_0, "max") : numAllocs;

    if (freeMax > numAllocs) {
        cmdLineErr("free-max > num-allocs\n");
    }

    // 分配内存
    printf("Allocating %d * %d bytes\n", numAllocs, blockSize);
    for (j = 0; j < numAllocs; j++) {
        ptr[j] = 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);
    for (j = freeMin - 1; j < freeMax; j += freeStep) {
        free(ptr[j]);
    }

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

    exit(EXIT_SUCCESS);
}
运行结果分析
  1. 释放所有内存块

    • 当程序释放所有内存块后,program break 的位置仍然保持在分配后的高位。这是因为 free() 并没有立即调用 sbrk() 来减少 program break,而是将这些内存块添加到空闲列表中,供后续的 malloc() 调用重用。
    $ ./free_and_sbrk 1000 10240 2
    Initial program break: 0x804a6bc
    Allocating 1000 * 10240 bytes
    Program break is now: 0x8a13000
    Freeing blocks from 1 to 1000 in steps of 2
    After free(), program break is: 0x8a13000
    
  2. 释放除最后一块外的所有内存块

    • 即使释放了大部分内存块,program break 仍然保持在高位。这是因为最后一块内存仍然被占用,free() 无法将 program break 降低到这块内存之前。
    $ ./free_and_sbrk 1000 10240 1 1 999
    Initial program break: 0x804a6bc
    Allocating 1000 * 10240 bytes
    Program break is now: 0x8a13000
    Freeing blocks from 1 to 999 in steps of 1
    After free(), program break is: 0x8a13000
    
  3. 释放堆顶部的连续内存块

    • 当释放的是堆顶部的连续内存块时,free() 会检测到这一情况,并调用 sbrk() 来降低 program break。这是因为这些内存块已经不再被使用,且它们位于堆的顶部,可以直接缩小堆的大小。
    $ ./free_and_sbrk 1000 10240 1 500 1000
    Initial program break: 0x804a6bc
    Allocating 1000 * 10240 bytes
    Program break is now: 0x8a13000
    Freeing blocks from 500 to 1000 in steps of 1
    After free(), program break is: 0x852b000
    
总结
  • malloc()free() 是 C 语言中常用的动态内存分配和释放函数,提供了比 brk()sbrk() 更高级、更灵活的接口。
  • malloc() 分配的内存块会被对齐,以便高效访问各种数据结构。
  • free() 通常不会立即减少 program break,而是将释放的内存块添加到空闲列表中,供后续的 malloc() 调用重用。只有当释放的内存块位于堆的顶部且足够大时,free() 才会调用 sbrk() 来降低 program break
  • 通过合理的内存管理和释放策略,malloc()free() 可以有效地减少内存碎片,提高内存利用率。

7.1.3 malloc()free() 的实现

malloc()free() 是 C 语言中用于动态内存管理的核心函数。它们的实现涉及复杂的内存分配和回收机制,理解这些实现细节有助于避免常见的编程错误,并优化内存使用。以下是关于 malloc()free() 实现的详细说明。

1. malloc() 的实现

malloc() 的主要任务是在堆上分配指定大小的内存块。它的实现通常包括以下几个步骤:

  1. 扫描空闲内存列表

    • malloc() 首先会检查之前由 free() 释放的空闲内存块列表(也称为“空闲链表”),寻找一个足够大的空闲内存块。
    • 扫描策略:不同的实现可能会采用不同的扫描策略来选择合适的内存块。常见的策略包括:
      • First-fit:从头开始扫描空闲链表,找到第一个足够大的空闲块。
      • Best-fit:遍历整个空闲链表,选择最接近所需大小的空闲块。
      • Worst-fit:选择最大的空闲块,虽然这种策略较少使用。
      • Next-fit:从上次分配的位置继续扫描,直到找到合适的空间。
  2. 分割大块内存

    • 如果找到的空闲块比所需的内存稍大,malloc() 会将这块内存分割成两部分:一部分返回给调用者,另一部分保留在空闲链表中,供后续的 malloc() 调用使用。
    • 最小块大小:为了减少内存碎片,malloc() 通常不会分配非常小的内存块。它会确保每个分配的内存块至少有一个最小的固定大小(例如 16 字节或 32 字节)。
  3. 扩展堆

    • 如果在空闲链表中找不到足够大的空闲块,malloc() 会调用 sbrk() 系统调用来扩展堆,增加更多的可用内存。
    • 批量分配:为了减少对 sbrk() 的频繁调用,malloc() 通常会一次性申请比实际需要更多的内存(通常是虚拟内存页大小的倍数)。多余的内存会被放入空闲链表中,供后续的 malloc() 调用使用。
  4. 记录内存块大小

    • malloc() 在分配内存时,会在实际返回给用户的内存块之前额外分配几个字节,用于存储该内存块的大小信息。这个大小信息通常位于内存块的起始位置,而用户实际获得的指针则指向这个大小信息之后的位置。
    • 这种设计使得 free() 可以在释放内存时知道这块内存的实际大小,从而正确地将其放回空闲链表中。
2. free() 的实现

free() 的主要任务是将已分配的内存块归还给系统或放入空闲链表中,以便后续的 malloc() 调用可以重用这些内存。它的实现包括以下几个步骤:

  1. 获取内存块大小

    • free() 接收到一个指针时,它会通过指针减去一个小的偏移量(通常是 8 字节或 16 字节),找到内存块的起始位置,并从中读取之前由 malloc() 存储的内存块大小信息。
    • 这样,free() 就能准确地知道这块内存的大小,并将其正确地放回空闲链表中。
  2. 合并相邻的空闲块

    • free() 会检查新释放的内存块是否与空闲链表中的其他空闲块相邻。如果是相邻的,free() 会将这些相邻的空闲块合并成一个更大的空闲块,以减少内存碎片。
    • 双向链表:空闲链表通常是一个双向链表,每个空闲块都包含指向前一个和后一个空闲块的指针。这样可以方便地进行合并操作。
  3. 调整 program break

    • 如果释放的内存块位于堆的顶部,并且空闲链表中有足够大的连续空闲块,free() 可能会调用 sbrk() 来减少 program break,从而释放不再使用的内存回到操作系统。
    • 阈值free() 并不会每次释放内存时都调用 sbrk(),而是只有当空闲的内存块足够大(通常是 128KB 或更大)时才会这样做。这减少了对 sbrk() 的频繁调用,提高了性能。
3. 内存管理和常见错误

尽管 malloc()free() 提供了相对简单的接口,但在使用时仍然容易犯下各种编程错误。理解它们的实现可以帮助我们避免这些错误。以下是一些常见的错误及其原因:

  1. 越界访问

    • 问题:程序可能会错误地访问分配的内存块之外的区域,导致覆盖内存块的大小信息或其他关键数据结构。
    • 原因:错误的指针运算、循环更新内存内容时的“off-by-one”错误等。
    • 解决方法:始终确保只在分配的内存范围内进行操作,避免越界访问。
  2. 重复释放

    • 问题:程序可能会多次释放同一块内存,导致不可预知的行为,甚至引发段错误(SIGSEGV)。
    • 原因:程序员可能不小心多次调用 free(),或者在多线程环境中多个线程同时释放同一块内存。
    • 解决方法:确保每块内存只被释放一次。可以在释放后将指针设置为 NULL,以防止意外的重复释放。
  3. 释放未分配的内存

    • 问题:程序可能会尝试释放从未分配过的内存,或者使用非 malloc() 系列函数分配的内存。
    • 原因:程序员可能误用了 free(),传递了无效的指针。
    • 解决方法:确保只释放由 malloc()calloc()realloc() 分配的内存块。
  4. 内存泄漏

    • 问题:程序可能会分配内存但忘记释放,导致堆不断增长,最终耗尽可用的虚拟内存。
    • 原因:程序员没有正确管理内存生命周期,特别是在长时间运行的程序(如 shell 或网络守护进程)中。
    • 解决方法:确保在不再需要内存时及时调用 free(),并定期检查是否存在内存泄漏。
  5. 悬空指针

    • 问题:程序可能会在释放内存后继续使用已经无效的指针,导致不可预知的行为。
    • 原因:程序员在释放内存后没有将指针设置为 NULL,或者在多线程环境中共享指针。
    • 解决方法:在释放内存后立即将指针设置为 NULL,并在使用指针前进行检查。
4. 总结
  • malloc()free() 的实现涉及到复杂的内存管理和优化策略,旨在提高内存分配的效率并减少碎片。
  • malloc() 通过扫描空闲链表、分割大块内存和扩展堆来满足内存分配请求,而 free() 则通过合并相邻的空闲块和调整 program break 来回收内存。
  • 为了避免常见的编程错误,开发者应遵守以下规则:
    • 不要越界访问分配的内存。
    • 不要重复释放同一块内存。
    • 不要释放未分配的内存。
    • 及时释放不再需要的内存,避免内存泄漏。
    • 在释放内存后将指针设置为 NULL,防止悬空指针问题。

通过理解 malloc()free() 的内部工作原理,开发者可以编写更高效、更可靠的代码,并避免潜在的内存管理问题。

4. malloc 调试工具和库

在开发过程中,内存管理错误(如越界访问、重复释放、内存泄漏等)可能会导致难以调试的问题。为了帮助开发者发现和修复这些问题,许多工具和库提供了对 mallocfree 的调试功能。以下是 glibc 提供的调试工具以及一些常见的第三方调试库。

1. glibc 提供的 malloc 调试工具

glibc 提供了多种内置的调试工具,可以帮助开发者检测和诊断内存管理问题。这些工具包括:

1.1 mtrace()muntrace()
  • 功能mtrace()muntrace() 函数用于跟踪程序中的内存分配调用。它们与环境变量 MALLOC_TRACE 配合使用,后者定义了一个文件名,用于记录所有对 malloc 函数包中函数的调用。

  • 使用方法

    • 在程序中调用 mtrace() 来启用跟踪,调用 muntrace() 来关闭跟踪。
    • 设置环境变量 MALLOC_TRACE,指定跟踪信息的输出文件。
    • 使用 mtrace 脚本来分析生成的跟踪文件,生成易于理解的报告。
    export MALLOC_TRACE=/path/to/tracefile
    ./your_program
    mtrace ./your_program /path/to/tracefile
    
  • 注意事项

    • mtrace() 会在程序启动时检查 MALLOC_TRACE 环境变量,并尝试打开指定的文件进行写入。
    • 设置用户 ID 和设置组 ID 的程序会忽略对 mtrace() 的调用,出于安全原因。
1.2 mcheck()mprobe()
  • 功能mcheck()mprobe() 函数允许程序对已分配的内存块进行一致性检查。它们可以在程序运行时捕获内存管理错误,例如越界写操作。

  • 使用方法

    • 在程序启动时调用 mcheck() 来启用一致性检查。
    • 使用 mprobe() 检查特定的内存块是否有效。
    #include <mcheck.h>
    
    int main() {
        mcheck(NULL);  // 启用一致性检查
        void *ptr = malloc(100);
        mprobe(ptr);   // 检查 ptr 是否有效
        free(ptr);
        return 0;
    }
    
  • 链接选项:使用 mcheck 库时,必须在编译时链接 -lmcheck 选项。

1.3 MALLOC_CHECK_ 环境变量
  • 功能MALLOC_CHECK_ 环境变量提供了类似于 mcheck()mprobe() 的功能,但无需修改或重新编译程序。通过设置不同的值,可以控制程序对内存分配错误的响应方式。

  • 设置选项

    • 0:忽略错误。
    • 1:在标准错误输出(stderr)中打印诊断信息。
    • 2:调用 abort() 终止程序。
    export MALLOC_CHECK_=2
    ./your_program
    
  • 优点:快速、易用,且不需要修改代码。

  • 局限性:并非所有的内存分配和释放错误都能被 MALLOC_CHECK_ 捕获,它主要检测常见错误。

2. 第三方 malloc 调试库

除了 glibc 提供的工具外,还有一些第三方库提供了更强大的内存调试功能。这些库通常会替代标准的 malloc 实现,并在运行时捕获各种内存管理错误。以下是一些常用的第三方调试库:

2.1 Electric Fence
  • 功能:Electric Fence 是一个简单的内存调试库,它通过将每个分配的内存块放在独立的页面边界上,来捕获越界访问错误。当程序试图访问未分配的内存时,Electric Fence 会触发段错误(SIGSEGV),从而帮助定位问题。

  • 使用方法

    • 编译时链接 libefence.so
    • 运行程序时设置环境变量 EFENCE 来控制其行为。
    gcc -g -o your_program your_program.c -lefence
    export EFENCE=1
    ./your_program
    
2.2 dmalloc
  • 功能:dmalloc 是一个功能丰富的内存调试库,提供了详细的内存分配和释放跟踪功能。它可以检测多种内存管理错误,包括越界访问、重复释放、内存泄漏等。

  • 使用方法

    • 编译时链接 libdmalloc.so
    • 运行程序时设置环境变量 DMALLOC_OPTIONS 来控制其行为。
    gcc -g -o your_program your_program.c -ldmalloc
    export DMALLOC_OPTIONS=debug=1,log=stdout
    ./your_program
    
2.3 Valgrind
  • 功能:Valgrind 是一个功能强大的内存调试和性能分析工具。它不仅可以检测内存管理错误,还可以发现其他类型的错误,如未初始化的内存访问、条件竞争等。Valgrind 通过模拟 CPU 执行程序,因此可以提供非常详细的错误报告。

  • 使用方法

    • 无需修改代码,直接运行程序。
    valgrind --leak-check=full ./your_program
    
  • 优点:能够发现多种类型的错误,不仅限于 mallocfree

  • 缺点:运行速度较慢,适合开发和调试阶段,不适合生产环境。

2.4 Insure++
  • 功能:Insure++ 是一个商业化的内存调试工具,提供了类似 Valgrind 的功能,但具有更好的性能和更多的特性。它可以检测内存泄漏、越界访问、未初始化的内存访问等问题。

  • 使用方法

    • 无需修改代码,直接运行程序。
    insure ./your_program
    
  • 优点:性能优于 Valgrind,适合大型项目。

  • 缺点:需要购买许可证。

3. 控制和监测 malloc 函数包

glibc 还提供了一些非标准的函数,用于控制和监测 malloc 函数包的行为。这些函数虽然不具有良好的可移植性,但在某些情况下仍然非常有用。

3.1 mallopt()
  • 功能mallopt() 函数用于修改 malloc 的内部参数,以控制其行为。例如,可以通过 mallopt() 设置在调用 sbrk() 收缩堆之前,空闲列表中必须保留的最小可释放内存空间。

  • 使用方法

    • mallopt(int param, int value),其中 param 是要修改的参数,value 是新的值。

    常见的参数包括:

    • M_TRIM_THRESHOLD:设置在调用 sbrk() 收缩堆之前,空闲列表中必须保留的最小可释放内存空间。
    • M_TOP_PAD:设置从堆中分配的内存块大小的上限,超出上限的内存块将使用 mmap() 系统调用分配。
3.2 mallinfo()
  • 功能mallinfo() 函数返回一个结构体,其中包含 malloc 分配内存的各种统计数据。这些数据可以帮助开发者了解程序的内存使用情况。

  • 使用方法

    #include <malloc.h>
    
    struct mallinfo mi = mallinfo();
    printf("Total non-mmapped bytes (arena): %d\n", mi.arena);
    printf("Number of free chunks (ordblks): %d\n", mi.ordblks);
    
  • 注意事项mallinfo() 的接口和返回值可能因不同的 glibc 版本而有所不同,因此在使用时需要注意版本兼容性。

4. 总结
  • glibc 内置工具mtrace()mcheck()MALLOC_CHECK_ 等工具可以帮助开发者快速发现常见的内存管理错误,适合简单的调试场景。
  • 第三方调试库:如 Electric Fence、dmalloc、Valgrind 和 Insure++ 提供了更强大的功能,能够发现更多类型的错误,适合复杂的调试需求。
  • 控制和监测函数mallopt()mallinfo() 可以帮助开发者控制 malloc 的行为并监控内存使用情况,但需要注意其可移植性问题。

通过使用这些工具和库,开发者可以更有效地发现和修复内存管理问题,确保程序的稳定性和可靠性。

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

除了 malloc(),C 标准库还提供了其他几个用于在堆上分配内存的函数,每个函数都有其特定的用途和行为。以下是这些函数的详细介绍:

1. calloc()

calloc() 函数用于为一组相同类型的对象分配内存,并将分配的内存初始化为零。

  • 原型

    #include <stdlib.h>
    void *calloc(size_t num, size_t size);
    
  • 作用:为 num 个大小为 size 的对象分配内存,并将分配的内存初始化为零。

  • 返回值

    • 成功时返回指向已分配内存块的指针。
    • 如果无法分配内存,则返回 NULL
  • 特点

    • calloc()malloc() 类似,但会自动将分配的内存初始化为零。
    • 适用于需要初始化为零的数组或结构体。
  • 示例

    int *arr = (int *)calloc(10, sizeof(int));
    if (arr == NULL) {
        // 处理内存分配失败的情况
    }
    // arr 现在是一个包含 10 个整数的数组,所有元素都初始化为 0
    free(arr);
    
2. realloc()

realloc() 函数用于调整已分配内存块的大小。它可以增加或减少内存块的大小,甚至移动内存块的位置。

  • 原型

    #include <stdlib.h>
    void *realloc(void *ptr, size_t size);
    
  • 作用:调整由 ptr 指向的内存块的大小为 size 字节。ptr 必须是之前由 malloc()calloc()realloc() 分配的内存块的指针。

  • 返回值

    • 成功时返回指向调整后内存块的指针。如果内存块被移动,返回的指针可能与原来的 ptr 不同。
    • 如果无法调整内存大小,则返回 NULL,而原始的 ptr 指针仍然有效。
  • 特点

    • 如果 ptrNULLrealloc(NULL, size) 等效于 malloc(size)
    • 如果 size 为 0,realloc(ptr, 0) 等效于 free(ptr) 后调用 malloc(0)
    • realloc() 可能会移动内存块,因此必须使用返回的指针来访问调整后的内存。
    • 如果 realloc() 增加了内存块的大小,新增的部分不会被初始化,内容是未定义的。
  • 示例

    int *arr = (int *)malloc(5 * sizeof(int));
    if (arr == NULL) {
        // 处理内存分配失败的情况
    }
    
    // 尝试将数组大小增加到 10 个元素
    int *new_arr = realloc(arr, 10 * sizeof(int));
    if (new_arr == NULL) {
        // 处理内存调整失败的情况
        free(arr);
    } else {
        arr = new_arr;
        // arr 现在是一个包含 10 个整数的数组
    }
    
    free(arr);
    
  • 注意事项

    • realloc() 可能会移动内存块,因此任何指向原内存块内部的指针在调用 realloc() 后都可能失效。
    • 应尽量避免频繁调用 realloc(),因为它可能会导致性能问题,尤其是在内存块位于堆中部且需要复制数据的情况下。
3. memalign()posix_memalign()

memalign()posix_memalign() 用于分配对齐的内存,即内存块的起始地址是对齐到指定边界的整数倍。这对某些应用程序(如多线程编程、硬件加速等)非常有用。

3.1 memalign()
  • 原型

    #include <malloc.h>
    void *memalign(size_t boundary, size_t size);
    
  • 作用:分配 size 字节的内存,起始地址是对 boundary 的整数倍对齐。boundary 必须是 2 的幂次方。

  • 返回值

    • 成功时返回指向已分配内存块的指针。
    • 如果无法分配内存,则返回 NULL
  • 特点

    • memalign() 并非在所有 UNIX 实现中都存在,某些系统可能需要包含 <malloc.h> 而不是 <stdlib.h>
    • memalign() 返回的内存块应该使用 free() 来释放。
3.2 posix_memalign()
  • 原型

    #include <stdlib.h>
    int posix_memalign(void **memptr, size_t alignment, size_t size);
    
  • 作用:分配 size 字节的内存,起始地址是对 alignment 的整数倍对齐。alignment 必须是 sizeof(void*) 的倍数,并且是 2 的幂次方。

  • 返回值

    • 成功时返回 0,并通过 memptr 参数返回指向已分配内存块的指针。
    • 如果无法分配内存或参数无效,则返回一个错误码(如 EINVALENOMEM)。
  • 特点

    • posix_memalign() 是 POSIX 标准的一部分,具有更好的可移植性。
    • posix_memalign() 返回的内存块也应该使用 free() 来释放。
  • 示例

    void *ptr;
    int ret = posix_memalign(&ptr, 4096, 65536);  // 分配 65536 字节的内存,并与 4096 字节边界对齐
    if (ret != 0) {
        // 处理内存分配失败的情况
    }
    // 使用 ptr
    free(ptr);
    
  • 注意事项

    • posix_memalign() 的返回值是错误码,而不是指针,因此需要通过 memptr 参数获取分配的内存地址。
    • alignment 必须是 sizeof(void*) 的倍数,并且是 2 的幂次方。在大多数硬件架构上,sizeof(void*) 为 4 或 8 字节。
4. 总结
  • calloc():用于为一组相同类型的对象分配内存,并将内存初始化为零。适用于需要初始化为零的数组或结构体。
  • realloc():用于调整已分配内存块的大小。可以增加或减少内存块的大小,甚至移动内存块的位置。需要注意的是,realloc() 可能会移动内存块,因此必须使用返回的指针来访问调整后的内存。
  • memalign()posix_memalign():用于分配对齐的内存,适用于需要特定对齐要求的应用程序。posix_memalign() 是 POSIX 标准的一部分,具有更好的可移植性,推荐优先使用。

通过合理使用这些函数,开发者可以根据具体需求选择合适的内存分配方式,确保程序的高效性和稳定性。

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

alloca() 是一个用于在堆栈上动态分配内存的函数,与 malloc() 不同的是,它不是从堆中分配内存,而是通过扩展当前函数的栈帧来分配内存。由于栈帧位于堆栈的顶部,因此可以通过调整堆栈指针来实现内存分配。alloca() 分配的内存具有自动释放的特性,当调用它的函数返回时,分配的内存会随着栈帧的移除而自动释放。

1. alloca() 的基本用法
  • 原型

    #include <alloca.h>
    void *alloca(size_t size);
    
  • 作用:在堆栈上分配 size 字节的内存,并返回指向已分配内存块的指针。

  • 特点

    • 自动释放:由 alloca() 分配的内存会在调用它的函数返回时自动释放,无需手动调用 free()
    • 不可调整大小:不能使用 realloc() 来调整由 alloca() 分配的内存大小。
    • 快速分配alloca() 的实现通常被编译器优化为内联代码,直接通过调整堆栈指针来分配内存,因此速度比 malloc() 更快。
    • 不需要维护空闲列表alloca() 不需要像 malloc() 那样维护空闲内存块列表,减少了管理开销。
  • 示例

    #include <stdio.h>
    #include <alloca.h>
    
    void example_function(int n) {
        // 使用 alloca() 分配 n 个整数的内存
        int *arr = (int *)alloca(n * sizeof(int));
    
        for (int i = 0; i < n; i++) {
            arr[i] = i;
        }
    
        // 打印数组内容
        for (int i = 0; i < n; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
    }
    
    int main() {
        example_function(5);  // 调用 example_function,分配的内存会在函数返回时自动释放
        return 0;
    }
    
2. alloca() 的优势
2.1 自动释放内存

alloca() 分配的内存会在调用它的函数返回时自动释放,这使得编写代码更加简单,尤其是在函数有多个返回路径的情况下。开发者不需要担心在每个返回路径中都调用 free() 来释放内存,从而减少了内存泄漏的风险。

2.2 适用于信号处理程序

在信号处理程序中调用 longjmp()siglongjmp() 以执行非局部跳转时,alloca() 的自动释放特性非常有用。如果使用 malloc() 分配内存,可能会导致内存泄漏,因为 longjmp() 会跳过正常的函数返回路径,导致 free() 没有机会被调用。而 alloca() 分配的内存会随着栈帧的移除而自动释放,避免了这一问题。

2.3 快速分配

alloca() 的实现通常被编译器优化为内联代码,直接通过调整堆栈指针来分配内存,因此速度比 malloc() 更快。对于频繁分配和释放小块内存的情况,alloca() 可能是一个更好的选择。

3. alloca() 的局限性
3.1 堆栈溢出风险

alloca() 分配的内存来自堆栈,而不是堆。堆栈的大小通常是有限的,因此如果分配的内存过大,可能会导致堆栈溢出。堆栈溢出会导致程序行为不可预知,甚至可能触发段错误(SIGSEGV)。因此,使用 alloca() 时应谨慎,确保分配的内存大小适中。

  • 示例
    void dangerous_function() {
        // 分配过大的内存可能导致堆栈溢出
        char *large_buffer = (char *)alloca(1024 * 1024 * 1024);  // 1GB
        // 程序可能会崩溃或行为异常
    }
    
3.2 不能在函数参数列表中调用 alloca()

alloca() 不能在函数的参数列表中调用,因为这样会导致 alloca() 分配的堆栈空间出现在当前函数参数的空间内,而函数参数是位于栈帧内的固定位置。相反,应该将 alloca() 的调用放在函数体内部。

  • 错误示例

    void bad_function(int n, void *ptr = alloca(n)) {
        // 错误:不能在函数参数列表中调用 alloca()
    }
    
  • 正确示例

    void good_function(int n) {
        void *ptr = alloca(n);  // 正确:在函数体内部调用 alloca()
        // 使用 ptr
    }
    
4. alloca() 的可移植性

虽然 alloca() 不是 POSIX 标准(SUSv3)的一部分,但大多数 UNIX 实现都提供了该函数,因此它在实际应用中具有较好的可移植性。不过,不同系统对 alloca() 的声明头文件可能有所不同:

  • glibc 和其他一些 UNIX 实现:需要包含 <stdlib.h><alloca.h>

  • BSD 衍生版本:通常需要包含 <alloca.h>

  • 示例

    #include <stdlib.h>  // 或者 #include <alloca.h>
    void *ptr = alloca(100);  // 分配 100 字节的内存
    
5. 总结
  • alloca():用于在堆栈上动态分配内存,分配的内存会在调用它的函数返回时自动释放。
  • 优点
    • 自动释放内存,简化代码编写。
    • 适用于信号处理程序中的非局部跳转,避免内存泄漏。
    • 分配速度快,适合频繁分配和释放小块内存。
  • 局限性
    • 存在堆栈溢出的风险,应谨慎使用。
    • 不能在函数参数列表中调用 alloca()
    • 不是 POSIX 标准的一部分,但在大多数 UNIX 实现中可用。

通过合理使用 alloca(),开发者可以在某些场景下获得更高效的内存分配和更简洁的代码结构。然而,由于其潜在的堆栈溢出风险,建议在使用时保持谨慎,确保分配的内存大小适中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2271880.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《Rust权威指南》学习笔记(五)

高级特性 1.在Rust中&#xff0c;unsafe是一种允许绕过Rust的安全性保证的机制&#xff0c;用于执行一些Rust默认情况下不允许的操作。unsafe存在的原因是&#xff1a;unsafe 允许执行某些可能被 Rust 的安全性检查阻止的操作&#xff0c;从而可以进行性能优化&#xff0c;如手…

使用R语言绘制标准的中国地图和世界地图

在日常的学习和生活中&#xff0c;有时我们常常需要制作带有国界线的地图。这个时候绘制标准的国家地图就显得很重要。目前国家标准地图服务系统向全社会公布的标准中国地图数据&#xff0c;是最权威的地图数据。 今天介绍的R包“ggmapcn”&#xff0c;就是基于最新公布的地图…

Flutter踩坑记-第三方SDK不兼容Gradle 8.0,需适配namespace

最近需要集成Flutter作为Module&#xff0c;Flutter依赖了第三方库&#xff0c;Gradle是8.0版本。 编译报错&#xff1a; 解决办法是在.android根目录下的build.gradle下新增一行代码&#xff1a; buildscript {ext.kotlin_version "1.8.22"repositories {google()…

golang 编程规范 - 项目目录结构

原文&#xff1a;https://makeoptim.com/golang/standards/project-layout 目录结构 Go 目录 cmdinternalpkgvendor 服务端应用程序目录 api Web 应用程序目录 web 通用应用程序目录 buildconfigsdeploymentsinitscriptstest 其他目录 assetsdocsexamplesgithooksthird_par…

蓝桥杯备赛:C++基础,顺序表和vector(STL)

目录 一.C基础 1.第一个C程序&#xff1a; 2.头文件&#xff1a; 3.cin和cout初识&#xff1a; 4.命名空间&#xff1a; 二.顺序表和vector&#xff08;STL&#xff09; 1.顺序表的基本操作&#xff1a; 2.封装静态顺序表&#xff1a; 3.动态顺序表--vector&#xff1a;…

node.js之---事件循环机制

事件循环机制 Node.js 事件循环机制&#xff08;Event Loop&#xff09;是其核心特性之一&#xff0c;它使得 Node.js 能够高效地处理大量并发的 I/O 操作。Node.js 基于 非阻塞 I/O&#xff0c;使用事件驱动的模型来实现异步编程。事件循环是 Node.js 实现异步编程的基础&…

如何在 Ubuntu 22.04 上部署 Nginx 并优化以应对高流量网站教程

简介 本教程将教你如何优化 Nginx&#xff0c;使其能够高效地处理高流量网站。 Nginx 是一个强大且高性能的 Web 服务器&#xff0c;以其高效处理大量并发连接的能力而闻名&#xff0c;这使得它成为高流量网站的流行选择。 正确优化 Nginx 可以显著提高服务器的性能&#xff0…

AIRemoveBackground:用 AI 技术轻松去除背景图的前端程序

在当今数字化时代&#xff0c;图像处理技术不断发展&#xff0c;其中 AI 去除背景图的功能备受关注。本文将介绍一款名为 AIRemoveBackground 的前端程序&#xff0c;它利用人工智能技术&#xff0c;为用户提供便捷、高效的背景去除解决方案。 一、简介 随着互联网的普及和多媒…

【踩坑指南2.0 2025最新】Scala中如何在命令行传入参数以运行主函数

这个地方基本没有任何文档记录&#xff0c;在学习的过程中屡屡碰壁&#xff0c;因此记录一下这部分的内容&#xff0c;懒得看可以直接跳到总结看结论。 踩坑步骤 首先来看看书上让我们怎么写&#xff1a; //main.scala object Start {def main(args:Array[String]) {try {v…

Excel VBA 自动填充空白并合并相同值的解决方案

文章目录 Excel VBA: 自动填充空白并合并相同值的解决方案问题背景解决方案1. VBA代码实现2. 代码说明3. 使用方法4. 注意事项 扩展优化总结 Excel VBA: 自动填充空白并合并相同值的解决方案 问题背景 在Excel中经常会遇到这样的数据处理需求&#xff1a;一列数据中存在多个空…

SpringSecurity中的过滤器链与自定义过滤器

关于 Spring Security 框架中的过滤器的使用方法,系列文章: 《SpringSecurity中的过滤器链与自定义过滤器》 《SpringSecurity使用过滤器实现图形验证码》 1、Spring Security 中的过滤器链 Spring Security 中的过滤器链(Filter Chain)是一个核心的概念,它定义了一系列过…

【STC库函数】Compare比较器的使用

如果我们需要比较两个点的电压&#xff0c;当A点高于B点的时候我们做一个操作&#xff0c;当B点高于A点的时候做另一个操作。 我们除了加一个运放或者比较器&#xff0c;还可以直接使用STC内部的一个比较器。 正极输入端可以是P37、P50、P51&#xff0c;或者从ADC的十六个通道…

Postgresql 命令还原数据库

因为PgAdmin打不开&#xff0c;但是数据库已经安装成功了&#xff0c;这里借助Pg命令来还原数据库 C:\Program Files\PostgreSQL\15\bin\psql.exe #链接数据库 psql -U postgres -p 5432#创建数据库 CREATE DATABASE "数据库名称"WITHOWNER postgresENCODING UTF8…

Backend - C# 的日志 NLog日志

目录 一、注入依赖和使用 logger 二、配置记录文件 1.安装插件 NLog 2.创建 nlog.config 配置文件 3. Programs配置日志信息 4. 设置 appsettings.json 的 LogLevel 5. 日志设定文件和日志级别的优先级 &#xff08;1&#xff09;常见的日志级别优先级 &#xff08;2&…

急需升级,D-Link 路由器漏洞被僵尸网络广泛用于 DDoS 攻击

僵尸网络活动增加 &#xff1a;新的“FICORA”和“CAPSAICIN”僵尸网络&#xff08;Mirai 和 Kaiten 的变体&#xff09;的活动激增。 被利用的漏洞 &#xff1a;攻击者利用已知的 D-Link 路由器漏洞&#xff08;例如 CVE-2015-2051、CVE-2024-33112&#xff09;来执行恶意命…

[ubuntu-22.04]ubuntu不识别rtl8153 usb转网口

问题描述 ubuntu22.04插入rtl8153 usb转网口不识别 解决方案 安装依赖包 sudo apt-get install libelf-dev build-essential linux-headers-uname -r sudo apt-get install gcc-12 下载源码 Realtek USB FE / GBE / 2.5G / 5G Ethernet Family Controller Softwarehttps:/…

WinForm开发-自定义组件-1. 工具栏: UcompToolStrip

这里写自定义目录标题 1. 工具栏: UcompToolStrip1.1 展示效果1.2 代码UcompToolStrip.csUcompToolStrip.Designer.cs 1. 工具栏: UcompToolStrip 自定义一些Winform组件 1.1 展示效果 1&#xff09;使用效果 2&#xff09;控件事件 1.2 代码 设计 编码 UcompToolStrip.…

Hypium纯血鸿蒙系统 HarmonyOS NEXT自动化测试框架

1、什么是Hypium Hypium是华为官方为鸿蒙操作系统开发的一款以python为语言的自动化测试框架。 引用华为官网介绍如下&#xff1a; DevEco Testing Hypium(以下简称Hypium)是HarmonyOS平台的UI自动化测试框架&#xff0c;支持开发者使用python语言为应用编写UI自动化测试脚本…

基于Spring Boot微信小程序电影管理系统

一、系统背景与意义 随着移动互联网的普及和用户对个性化娱乐需求的不断增长&#xff0c;电影行业迎来了新的发展机遇。然而&#xff0c;传统的电影管理方式存在信息不对称、购票流程繁琐、用户体验不佳等问题。因此&#xff0c;开发一个基于Spring Boot微信小程序的电影管理系…

软件工程实验-实验2 结构化分析与设计-总体设计和数据库设计

一、实验内容 1. 绘制工资支付系统的功能结构图和数据库 在系统设计阶段&#xff0c;要设计软件体系结构&#xff0c;即是确定软件系统中每个程序是由哪些模块组成的&#xff0c;以及这些模块相互间的关系。同时把模块组织成良好的层次系统&#xff1a;顶层模块通过调用它的下层…