第 7 章 内存分配 (Memory Allocation)

      +

      核心结论

      • 堆分配三层次:底层 brk/sbrk 系统调用直接调整 program break;中间层 malloc/free 库函数管理空闲块链表;上层 calloc/realloc/posix_memalign 提供便利接口。

      • malloc 不一定调 sbrk:实现可能用 mmap 分配大块(典型阈值 128KB+),不增加 program break;好处是 free 时可立即归还内核。

      • malloc 实现机制:每个块头部存大小;空闲块用双向链表维护;分配时搜索合适的块(first-fit/best-fit);释放时合并相邻空闲块。

      • 典型陷阱:重复 free(double free)、free 非 malloc 返回的指针、访问已 free 的内存(use-after-free)、超出分配范围写入(buffer overrun)。

      • calloc/realloccalloc(n, size) 分配并清零;realloc(ptr, new_size) 调整大小(可能移动);失败返回 NULL;ptr 失效后不能再用。

      • 栈分配 alloca:在栈帧上分配;函数返回时自动释放;不能跨函数返回使用;不适合大分配(栈溢出)。

      • 调试工具MALLOC_CHECK_ 环境变量、mtrace() 函数、valgrind、AddressSanitizer (gcc/clang -fsanitize=address)。

      本章主旨

      本章介绍 Linux 进程内存分配的两条路径:(1) heap(堆)——通过 malloc/free 家族动态分配;(2) stack(栈)——通过 alloca 在栈帧内分配。理解 malloc 的底层机制是排查内存 bug(泄漏、double free、use-after-free)的前提。本章不展开 mmap/munmap(详见第 49 章)、内存锁(详见第 50 章)、共享内存(详见第 48 章)。

      一、核心概念

      本章围绕 6 个核心概念展开:从「堆内存管理」到「malloc 内部实现」再到「栈分配与调试」。

      概念 定义 + 重要性 实现提示

      program break 与 brk/sbrk

      program break 是堆的上界;brk(addr) 设置,sbrk(incr) 增量调整;底层机制。

      §7.1.1;SUSv3 已移除这两个函数;现代 malloc 内部使用。

      malloc 与 free

      标准 C 库函数;维护空闲链表;分配可被多次复用;free 通常不归还内存给内核(除非大块)。

      §7.1.2;void *malloc(size_t size)void free(void *ptr)

      malloc 实现机制

      每个块头部存大小;空闲链表(双向);分配时搜索合适块;释放时合并相邻空闲块;大块(典型 128KB+)用 mmap。

      §7.1.3;glibc 的 ptmalloc2 实现细节。

      calloc / realloc / aligned_alloc

      calloc(n, size) 分配并清零;realloc(ptr, size) 调整大小;aligned_alloc(align, size) 对齐分配。

      §7.2.1-§7.2.3;C11 引入 aligned_alloc;POSIX 的 posix_memalign 旧一些。

      栈分配 alloca

      在调用函数的栈帧上分配;函数返回时自动释放;不能跨函数返回;不能 free。

      §7.3;优势:自动释放,无泄漏;劣势:栈溢出风险。

      malloc 调试与诊断

      MALLOC_CHECK_ 环境变量、mtrace() 函数、valgrind、AddressSanitizer。

      §7.4;调试版本 malloc 在边界加保护字节;检测 double free、buffer overrun。

      二、详细笔记

      7.1 堆与 program break

      What:heap(堆)是位于 bss 段之上、可动态扩展的内存区域;program break 是堆的当前上界;brk/sbrk 系统调用调整 program break。

      Why:理解 program break 是理解 malloc 底层机制的前提;某些高级场景(如自定义内存分配器)需要直接用 brk/sbrk。

      How

      // 摘自《The Linux Programming Interface》第 7 章
      #include <unistd.h>
      
      int brk(void *end_data_segment);
      // 设置 program break 到 end_data_segment;返回 0 成功,-1 失败
      
      void *sbrk(intptr_t increment);
      // 增加 program break(可负数);返回旧 program break 地址

      调整 program break 后,内核自动分配新页(首次访问时);program break 通常向上调整,但向下调整可能失败。

      约束(§7.1.1):

      • program break 低于初始值(&end 之下)→ 段错误。

      • program break 上限受 RLIMIT_DATA 资源限制、mmap 区域、共享库位置影响。

      malloc(0) 在 Linux 上返回有效指针(指向「0 字节块」,但可 free)。

      When

      • 现代程序几乎不直接用 brk/sbrk——glibc malloc 内部使用。

      • 嵌入式或自定义内存分配器——直接用 brk/sbrk。

      • 想知道当前 program break——sbrk(0)

      Example

      // 摘自《The Linux Programming Interface》第 7 章
      // 查询 program break
      void *current_brk = sbrk(0);
      printf("Current program break: %p\n", current_brk);

      7.2 malloc 与 free

      Whatmalloc(size) 从堆分配 size 字节;free(ptr) 释放先前分配的内存。两者是 C 程序动态内存的基本接口。

      Why:malloc/free 是几乎所有 C 程序都会用到的接口;理解其行为(什么时候调 brk、什么时候用 mmap、什么时候合并空闲块)才能写出无内存 bug 的程序。

      How

      // 摘自《The Linux Programming Interface》第 7 章
      #include <stdlib.h>
      
      void *malloc(size_t size);
      // 分配 size 字节;返回对齐的指针(8/16 字节对齐);失败返回 NULL
      // 内存未初始化
      
      void free(void *ptr);
      // 释放 ptr 指向的内存;ptr 必须是 malloc/calloc/realloc 返回值
      // free(NULL) 无操作
      // 不要重复 free;不要 free 非 malloc 返回的指针

      行为细节(§7.1.2):

      • malloc 不降低 program break——free 通常把块加入空闲链表,不立即归还内核。

      • 重复 free → 通常触发 SIGSEGV(glibc 检测)。

      • 越界写入 → 破坏其他块的头部,可能导致后续 malloc 返回坏指针。

      • 大块分配(典型 128KB+)→ glibc 用 mmap 分配;free 时调用 munmap 立即归还。

      When

      • 任何需要动态分配的对象——malloc(sizeof(T))malloc(n * sizeof(T))

      • 释放后置指针为 NULL——避免重复 free:free(p); p = NULL;

      • 长生命周期程序(守护进程)——避免累积内存泄漏。

      Example

      // 摘自《The Linux Programming Interface》第 7 章
      char *p = malloc(1024);
      if (p == NULL) errExit("malloc");
      strcpy(p, "hello");
      // ... 使用 p ...
      free(p);
      p = NULL;  // 避免 double free

      7.3 malloc 内部实现

      What:glibc 的 ptmalloc2 维护「空闲链表」管理已释放的块;分配时搜索链表,必要时合并相邻空闲块;大块用 mmap。

      Why:理解 malloc 内部机制是理解「为什么 free 不立即调 sbrk」「为什么小块分配比大块频繁的 syscall 少」的关键。

      How:块结构(§7.1.3 Figure 7-1):

      已分配块:
      [L |-------- 内存空间 --------]
       ^
       块头部:长度 L
       ↑ 返回给用户的指针
      
      空闲链表块:
      [L | P_prev | P_next |... 剩余空闲空间 ...]
            ^           ^
            双向链表指针
      • 块头部存长度 L(含头)。

      • 已分配块:返回「头之后」的地址给用户。

      • 空闲块:头 + 双向链表指针 + 剩余空间。

      分配策略:

      • first-fit:从链表头开始,找到第一个大小 ≥ 要求的块;返回;剩余部分留在链表中(可能分裂)。

      • best-fit:找到最小的「足够大」的块;减少碎片但搜索慢。

      • glibc:fastbin(小块)+ smallbin + largebin + unsorted bin 多种策略。

      free 行为:

      • 把块加入空闲链表(标记为 free)。

      • 如果与相邻块都是 free → 合并(coalesce),减少碎片。

      • 如果是大块(mmap 分配)→ 直接 munmap 归还内核。

      • glibc 仅在「足够大」(典型 128KB)的 free 块在堆顶时才调 sbrk 降低 program break。

      When

      • 频繁分配/释放小块——使用对象池或 slab allocator 减少 malloc 开销。

      • 大量分配/释放大块——考虑直接 mmap/munmap。

      • 调试内存问题——用 valgrind 或 ASan 跟踪 malloc 行为。

      Example:free 与 program break(Listing 7-1 输出):

      $ ./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 is now:    0x8a13000   # program break 没变
                                          # 空闲块在链表中等待复用

      7.4 calloc / realloc / 对齐分配

      What

      • calloc(n, size):分配 n*size 字节并清零;失败返回 NULL。

      • realloc(ptr, size):调整 ptr 指向块的大小;可能移动;返回新指针(可能等于也可能不等于 ptr);失败返回 NULL(ptr 仍有效)。

      • posix_memalign(&ptr, align, size):分配 size 字节、按 align 字节对齐;返回 0 成功。

      • aligned_alloc(align, size)(C11):同上。

      Why:calloc 适合需要清零的数组(如矩阵);realloc 适合动态增长的缓冲区;posix_memalign 适合需要特定对齐(如 SSE/AVX、页对齐 DMA)的场景。

      How

      // 摘自《The Linux Programming Interface》第 7 章
      #include <stdlib.h>
      
      void *calloc(size_t nmemb, size_t size);
      // 分配 nmemb*size 字节并清零;溢出检查;失败返回 NULL
      
      void *realloc(void *ptr, size_t size);
      // 调整大小;可能移动;旧 ptr 失效;失败返回 NULL(不修改原 ptr)
      
      int posix_memalign(void **memptr, size_t alignment, size_t size);
      // 分配按 alignment 对齐的内存;alignment 必须是 2 的幂且 sizeof(void*) 的倍数
      // 成功返回 0;错误返回错误码(不设置 errno)

      realloc 行为:

      • size == 0:等价于 free(ptr)(旧实现可能返回 NULL 或小指针)。

      • ptr == NULL:等价于 malloc(size)。

      • 新 size > 旧 size:可能就地扩展或分配新块并复制;旧块 free。

      • 新 size < 旧 size:可能就地缩小。

      When

      • 数组初始化:calloc(n, sizeof(T)) 优于 malloc + memset

      • 动态缓冲区:realloc 增长;ptr = realloc(ptr, new_size); if (!ptr) errExit();

      • SIMD/硬件要求:posix_memalign(&p, 32, size)(AVX 需要 32 字节对齐)。

      • 大页:posix_memalign(&p, 2*1024*1024, size)(2MB 对齐)。

      Example

      // 摘自《The Linux Programming Interface》第 7 章
      // 动态字符串缓冲区
      char *buf = NULL;
      size_t cap = 0;
      for (int i = 0; i < 100; i++) {
          cap += 16;
          char *new_buf = realloc(buf, cap);
          if (new_buf == NULL) errExit("realloc");
          buf = new_buf;
          // ... 使用 buf ...
      }
      free(buf);
      
      // 对齐分配(SIMD)
      void *p;
      if (posix_memalign(&p, 32, 1024) != 0) errExit("posix_memalign");
      // p 现在是 32 字节对齐
      free(p);

      7.5 栈分配:alloca

      Whatalloca(size) 在调用函数的栈帧上分配 size 字节;函数返回时自动释放(无需 free)。

      Why:alloca 比 malloc 快(不需要堆管理);自动释放避免泄漏。但有限制:不能跨函数返回,栈溢出风险。

      How

      // 摘自《The Linux Programming Interface》第 7 章
      #include <alloca.h>
      
      void *alloca(size_t size);
      // 在栈帧上分配 size 字节;函数返回时自动释放
      // 失败无明确错误(栈溢出 → SIGSEGV)
      // 不要对 alloca 返回的指针调用 free

      行为:

      • alloca 调整当前函数的栈帧指针(向下扩展)。

      • 函数返回时——栈帧自动收缩,内存自动「释放」。

      • 不能跨函数返回——返回后栈帧无效,悬空指针。

      • 嵌套函数调用 alloca——内层函数返回后内存也无效(alloca 作用于调用者栈帧)。

      When

      • 临时缓冲区——在函数内分配,使用完毕自动释放。

      • 避免 malloc/free 开销——allocamalloc 快约一个数量级。

      • 不适合大分配——可能栈溢出;典型栈大小 8MB。

      Example

      // 摘自《The Linux Programming Interface》第 7 章
      void process(int n) {
          char *buf = alloca(n);  // n 字节在栈上
          // ... 使用 buf ...
          // 函数返回时自动释放,无须 free
      }

      7.6 内存调试

      What:检测 malloc 相关的 bug(内存泄漏、double free、buffer overrun、use-after-free)需要专门的工具。

      Why:这些 bug 在生产环境往往表现为「运行 3 天后崩溃」或「偶发性数据错误」——调试版本 malloc 和外部工具能在开发期捕获。

      How:调试方法:

      工具 用途

      MALLOC_CHECK_=3 环境变量

      glibc 启用严格检查;输出到 stderr;退出码 1/2/3

      mtrace()

      glibc 跟踪所有 malloc/free;输出到文件;用 mtrace 命令分析

      MALLOC_PERTURB_

      分配时填充特定字节;free 后填充特定字节;检测 use-after-free

      valgrind --tool=memcheck

      检测未初始化内存、越界访问、double free、内存泄漏

      AddressSanitizer (-fsanitize=address)

      gcc/clang 内置;检测越界、use-after-free、double free

      Electric Fence / DUMA

      分配页面对齐;越界立即触发 SIGSEGV

      When

      • 开发期——启用 ASan 或 valgrind 跑测试。

      • 怀疑内存泄漏——valgrind --leak-check=full ./prog

      • 多线程程序——valgrind --tool=helgrind 检测锁问题。

      Example

      # ASan 编译
      $ gcc -g -O0 -fsanitize=address -o prog prog.c
      $ ./prog
      # 越界写入立即报告
      
      # valgrind 检测泄漏
      $ valgrind --leak-check=full ./prog
      # 报告: "definitely lost: 1024 bytes in 1 blocks"
      
      # MALLOC_CHECK_
      $ MALLOC_CHECK_=3 ./prog
      # double free 时输出错误到 stderr

      三、关键图表

      非可视化条目(malloc 家族速查)
      函数 用途

      brk/sbrk

      底层系统调用;调整 program break;几乎不直接用

      malloc(size)

      分配 size 字节;返回对齐指针;失败 NULL

      calloc(n, size)

      分配 n*size 字节并清零;溢出检查

      realloc(ptr, size)

      调整大小;可能移动;旧 ptr 失效

      free(ptr)

      释放;ptr 必须来自 malloc 家族;free(NULL) 无操作

      posix_memalign(&p, align, size)

      对齐分配;返回 0/errno

      aligned_alloc(align, size)

      C11 对齐分配;size 必须是 align 的倍数

      alloca(size)

      栈分配;自动释放;不能 free

      调试

      MALLOC_CHECK_ / mtrace / valgrind / ASan

      四、思维导图

      mindmap
        root((第 7 章 内存分配))
          底层 brk sbrk
            program break
            调整堆上界
            现代几乎不直接用
          malloc 家族
            malloc
            calloc 清零
            realloc 调整
            free
            posix_memalign
          malloc 实现
            块头存大小
            空闲链表
            first fit
            合并空闲块
            大块用 mmap
          调试
            MALLOC_CHECK_
            mtrace
            valgrind
            AddressSanitizer
            double free
            use after free
            buffer overrun
          栈分配
            alloca
            自动释放
            不能 free
            栈溢出风险
          进程终止
            自动释放
            内存泄漏避免

      五、重点与易错点

      1. malloc(0) 返回有效指针(Linux):可 free;不要假设返回 NULL。

      2. free 不一定调 sbrk:小块 free 后留在空闲链表等待复用;只有堆顶大块 free 才调 sbrk 降低 program break。

      3. free(NULL) 安全:显式 free(NULL) 无操作——可以在 free 后置 NULL 避免 double free。

      4. realloc 返回 NULL 时旧指针仍有效:必须保留原指针以便 free。

      5. realloc 后旧指针失效:可能移动到新地址;不要继续用旧指针。

      6. posix_memalign alignment 必须是 2 的幂:且 ≥ sizeof(void*);典型 16/32/64 字节对齐。

      7. alloca 不能 free:函数返回时自动释放;不要跨函数返回(悬空指针)。

      8. alloca 栈溢出:典型栈 8MB;大分配可能栈溢出;不确定时用 malloc。

      9. 分配后不初始化:malloc 返回的内存内容是「垃圾」;calloc 或 memset(0) 才清零。

      10. 越界写入不立即报错:malloc 不加边界检查;越界破坏相邻块头部;后续 free 时崩溃。

      11. 跨章衔接:第 6 章讲解进程内存布局;本章讲解 heap 管理;第 13 章讲解 buffer cache;第 48 章讲解共享内存;第 49 章讲解 mmap;第 50 章讲解内存锁。