Linux 基础七 内存

在操作系统中,进程的内存布局通常分为几个区域,包括代码段、已初始化的数据段、未初始化的数据段(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(),开发者可以在某些场景下获得更高效的内存分配和更简洁的代码结构。然而,由于其潜在的堆栈溢出风险,建议在使用时保持谨慎,确保分配的内存大小适中。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

庐山派K230学习日记4 PWM控制

1 本节介绍​ &#x1f4dd;本节您将学习如何通过将K230开发板的GPIO引脚复用为PWM功能并输出PWM信号&#xff1b;实现输出PWM信号及控制板载无源蜂鸣器发出声音。 &#x1f3c6;学习目标 1️⃣如何将GPIO引脚配置为PWM模式&#xff0c;通过40Pin排针中的部分引脚来输出PWM信号…

c语言的文件操作与文件缓冲区

目录 C语言文件操作函数汇总 简单介绍文件 为什么使用文件 什么是文件 文件名 二进制文件和文本文件 流和标准流 流 标准流 文件指针 文件的打开和关闭 文件的顺序读写 顺序读写函数介绍 文件的随机读写 fseek ftell rewind 文件读取结束的判定 文件缓冲区 缓…

嵌入式linux中socket控制与实现

一、概述 1、首先网络,一看到这个词,我们就会想到IP地址和端口号,那IP地址和端口各有什么作用呢? (1)IP地址如身份证一样,是标识的电脑的,一台电脑只有一个IP地址。 (2)端口提供了一种访问通道,服务器一般都是通过知名端口号来识别某个服务。例如,对于每个TCP/IP实…

Nginx:动静分离

什么是动静分离? 动静分离 是指将网站中的静态资源(如图片、样式表、脚本等)和动态内容(如 PHP、Python、Node.js 等后端生成的内容)分开部署和处理。这样做的好处是可以利用不同的服务器或缓存策略来优化不同类型的资源。 动静分离的好处 提高性能:静态资源可以直接从…

PADS Layout 差分线设计规则及其设计规则约束的详细过程步骤

一般我们的电路板有很多的差分线,有90欧姆的差分线,也有100欧姆的差分线,90欧姆的差分线主要是针对USB的差分线,特别是对于USB HUB的板子,那么我们就要设置差分线。一般我们设置差分线,一般要切换到Router里面来设置,如下所示: 那么设置差分对,一般要对原理图和Router…

计算机网络--路由表的更新

一、方法 【计算机网络习题-RIP路由表更新-哔哩哔哩】 二、举个例子 例1 例2

概述(讲讲python基本语法和第三方库)

我是北子&#xff0c;这是我自己写的python教程&#xff0c;主要是记录自己的学习成果方便自己日后复习&#xff0c; 我先学了C/C&#xff0c;所以这套教程中可能会将很多概念和C/C去对比&#xff0c;所以该教程大概不适合零基础的人。 it seems that python nowadays 只在人工…

redux用法总结

redux用法总结 目录 基本概念工作原理核心概念基本使用异步操作 Redux ThunkRedux Saga React 集成Redux Toolkit最佳实践 基本概念 什么是 Redux&#xff1f; Redux 是一个可预测的状态容器&#xff0c;用于管理 JavaScript 应用的状态。它遵循三个基本原则&#xff1a; …

Gitee上传项目代码教程(详细)

工具必备&#xff1a;Git Bash 上传步骤 1.在Gitee创建项目仓库 2.进入本地项目目录 右键打开Git Bash here 3.配置用户名和邮箱 如果之前给git配置过用户名和邮箱可跳过 查看Git是否配置成功&#xff1a;git config --list git config --global user.name "xxx"…

ARM CCA机密计算安全模型之安全生命周期管理

安全之安全(security)博客目录导读 目录 一、固件启用的调试 二、CCA系统安全生命周期 三、重新供应 四、可信子系统与CCA HES 启用 CCA&#xff08;机密计算架构&#xff09;的安全系统是指 CCA 平台的实现处于可信状态。 由于多种原因&#xff0c;CCA 启用系统可能处于不…

计算机视觉CV期末总复习

1.计算机视觉基础 数字图像表示 二值图像 仅包含黑白两种颜色的图像&#xff0c;只使用1个比特为&#xff08;0黑或1白&#xff09;表示 彩色图像&#xff1a;分不同的颜色空间 gray灰度图像 每个像素只有一个采样颜色&#xff0c;取值范围0--255&#xff0c;为8比特位&a…

web安全常用靶场

这里写自定义目录标题 phpstydy2018pikachuxss-labs phpstydy2018 网盘地址 提取码: nxnw ‌phpStudy是一款专为PHP开发者设计的集成环境工具&#xff0c;主要用于简化PHP开发环境的搭建过程。‌ 它集成了Apache、MySQL、PHP等核心组件&#xff0c;用户只需进行一次性安装&a…

每天40分玩转Django:Django实战 - 在线打印服务系统

Django实战 - 在线打印服务系统 一、系统功能概览表 模块主要功能技术要点文件上传PDF/Word文件上传、文件验证文件处理、MIME类型验证异步处理文件转换、打印队列Celery、Redis通知邮件打印状态通知、订单确认SMTP、邮件模板 二、系统架构设计 2.1 模型设计 # models.py …

WPS计算机二级•数据查找分析

听说这里是目录哦 通配符&#x1f30c;问号&#xff08;?&#xff09;星号&#xff08;*&#xff09;波形符&#xff08;~&#xff09; 排序&#x1f320;数字按大小排序以当前选定区域排序以扩展选定区域排序 文字按首字母排序 快速筛选分类数据☄️文字筛选数字筛选颜色筛选…

基于海思soc的智能产品开发(camera sensor的两种接口)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 对于嵌入式开发设备来说&#xff0c;除了图像显示&#xff0c;图像输入也是很重要的一部分。说到图像输入&#xff0c;就不得不提到camera。目前ca…

网安入门之MySQL后端基础

数据库 (Database) 数据库是指长期存储在计算机中的&#xff0c;有组织、可共享的数据集合。它通过表、列、行等结构来组织数据&#xff0c;目的是使数据可以高效存储、检索和管理。数据库通常包括多个表&#xff0c;每个表存储与特定主题或对象相关的数据 数据库管理系统 (D…

概率基本概念 --- 离散型随机变量实例

条件概率&独立事件 随机变量 - 离散型随机变量 - 非离散型随机变量 连续型随机变量奇异性型随机变量 概率表示 概率分布函数概率密度函数概率质量函数全概率公式贝叶斯公式 概率计算 数学期望方差协方差 计算实例 假设有两个离散型随机变量X和Y&#xff0c;它们代…

w139华强北商城二手手机管理系统

&#x1f64a;作者简介&#xff1a;多年一线开发工作经验&#xff0c;原创团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339;赠送计算机毕业设计600个选题excel文…

LLM大语言模型中RAG切片阶段改进策略

切片方法的特点和示例&#xff1a; Token 切片 适合对 Token 数量有严格要求的场景&#xff0c;比如使用上下文长度较小的模型时。 示例文本&#xff1a; “LlamaIndex是一个强大的RAG框架。它提供了多种文档处理方式。用可以根据需选择合适的方法。” 使用Token切片(chunk…

Earth靶场

打开靶机后使用 arp-scan -l 查询靶机 ip 我们使用 nmap 进行 dns 解析 把这两条解析添加到hosts文件中去&#xff0c;这样我们才可以访问页面 这样网站就可以正常打开 扫描ip时候我们发现443是打开的&#xff0c;扫描第二个dns解析的443端口能扫描出来一个 txt 文件 dirsear…