第 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/realloc:
calloc(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 是堆的上界; |
§7.1.1;SUSv3 已移除这两个函数;现代 malloc 内部使用。 |
malloc 与 free |
标准 C 库函数;维护空闲链表;分配可被多次复用;free 通常不归还内存给内核(除非大块)。 |
§7.1.2; |
malloc 实现机制 |
每个块头部存大小;空闲链表(双向);分配时搜索合适块;释放时合并相邻空闲块;大块(典型 128KB+)用 mmap。 |
§7.1.3;glibc 的 ptmalloc2 实现细节。 |
calloc / realloc / aligned_alloc |
|
§7.2.1-§7.2.3;C11 引入 aligned_alloc;POSIX 的 posix_memalign 旧一些。 |
栈分配 alloca |
在调用函数的栈帧上分配;函数返回时自动释放;不能跨函数返回;不能 free。 |
§7.3;优势:自动释放,无泄漏;劣势:栈溢出风险。 |
malloc 调试与诊断 |
|
§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
What:malloc(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
What:alloca(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 开销——
alloca比malloc快约一个数量级。 -
不适合大分配——可能栈溢出;典型栈大小 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:调试方法:
| 工具 | 用途 |
|---|---|
|
glibc 启用严格检查;输出到 stderr;退出码 1/2/3 |
|
glibc 跟踪所有 malloc/free;输出到文件;用 |
|
分配时填充特定字节;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 家族速查)
|
四、思维导图
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
栈溢出风险
进程终止
自动释放
内存泄漏避免
五、重点与易错点
-
malloc(0) 返回有效指针(Linux):可 free;不要假设返回 NULL。
-
free 不一定调 sbrk:小块 free 后留在空闲链表等待复用;只有堆顶大块 free 才调 sbrk 降低 program break。
-
free(NULL) 安全:显式 free(NULL) 无操作——可以在 free 后置 NULL 避免 double free。
-
realloc 返回 NULL 时旧指针仍有效:必须保留原指针以便 free。
-
realloc 后旧指针失效:可能移动到新地址;不要继续用旧指针。
-
posix_memalign alignment 必须是 2 的幂:且 ≥
sizeof(void*);典型 16/32/64 字节对齐。 -
alloca 不能 free:函数返回时自动释放;不要跨函数返回(悬空指针)。
-
alloca 栈溢出:典型栈 8MB;大分配可能栈溢出;不确定时用 malloc。
-
分配后不初始化:malloc 返回的内存内容是「垃圾」;calloc 或
memset(0)才清零。 -
越界写入不立即报错:malloc 不加边界检查;越界破坏相邻块头部;后续 free 时崩溃。
-
跨章衔接:第 6 章讲解进程内存布局;本章讲解 heap 管理;第 13 章讲解 buffer cache;第 48 章讲解共享内存;第 49 章讲解 mmap;第 50 章讲解内存锁。