内存管理基础 ------ 进程内存布局与 malloc 全家桶
C 程序员吃过最多的亏都是内存:栈溢出、堆泄漏、free 后再用、double free、悬空指针。这篇按"进程内存布局 → 栈 vs 堆 → malloc/free 怎么工作 → 常见内存错误 → 内存分配器选型"五步讲清楚,读完后再写 C,对每一个变量 / 每一次 malloc 心里有数。
0. 引言:这个 bug 在哪?
c
char *get_name(void) {
char buf[64];
strcpy(buf, "alice");
return buf; // ⛔ 返回栈上指针
}
int main(void) {
char *name = get_name();
printf("%s\n", name); // 可能打印对,可能崩,看运气
return 0;
}
类似的还有:
c
char *p = malloc(100);
free(p);
p[0] = 'X'; // ⛔ use-after-free
char *q = malloc(100);
free(q);
free(q); // ⛔ double free
这些坑都和"内存到底在哪"有关。下面把 Linux 进程内存布局拆开看。
1. 进程内存布局

一个 C 程序运行时,进程地址空间从低到高大致:

各区对应(结合 19 篇 ELF):
| 区 | 对应 ELF 节 | 谁分配 | 谁释放 |
|---|---|---|---|
| .text | .text |
OS 加载 | 进程退出 |
| .rodata | .rodata |
OS 加载 | 进程退出 |
| .data | .data |
OS 加载并初始化 | 进程退出 |
| .bss | .bss |
OS 加载并清零 | 进程退出 |
| Heap | --- | malloc / brk / mmap |
free / munmap |
| Stack | --- | 编译器自动 | 函数返回自动 |
| 共享库 mmap | 各 .so 段 | ld.so |
进程退出 |
2. 栈(Stack)
2.1 行为
| 特性 | 说明 |
|---|---|
| 每个线程一个栈 | pthread 默认 8MB(详见第 1 篇) |
| 函数调用压栈帧 | 帧内含参数、返回地址、局部变量 |
| 函数返回弹栈帧 | 自动恢复 SP,局部变量随之失效 |
| 栈向下生长 | 高地址 → 低地址 |
2.2 局部变量在栈上
c
void f(int n) {
int local_a = 10; // 栈
char buf[1024]; // 栈
struct point p = {1, 2}; // 栈
}
// f 返回后,local_a / buf / p 全部失效
2.3 栈的优势
| 优势 | 说明 |
|---|---|
| 零开销分配 | 编译期算好偏移,函数入口一次性 SP -= N 完成 |
| 自动释放 | 函数返回 SP += N,无需手动 free |
| 缓存友好 | 连续地址,CPU cache 命中率高 |
2.4 栈的局限
| 局限 | 说明 |
|---|---|
| 生命周期受限 | 函数返回就没了 |
| 大小有限 | 默认 8MB,超了 SIGSEGV(栈溢出) |
| 递归危险 | 每次调用一帧,深递归爆栈 |
2.5 栈大小调整
bash
# 查看
ulimit -s # 8192 (KB)
# 临时改
ulimit -s 16384 # 16MB
# 永久改 /etc/security/limits.conf
线程级也能在 pthread_create 前设置:
c
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 16 * 1024 * 1024); // 16MB
pthread_create(&tid, &attr, fn, NULL);
2.6 栈溢出表现
c
void recur(int n) {
char big[1024];
recur(n + 1);
}
int main(void) { recur(0); }
// Segmentation fault (core dumped)
或者大局部数组:
c
void f(void) {
char buf[10*1024*1024]; // 10MB > 8MB → 栈溢出
}
经验法则:单函数局部变量总和 ≤ 几 KB;大数据用 malloc 走堆。
3. 堆(Heap)
3.1 malloc / free / realloc / calloc
c
#include <stdlib.h>
void *malloc(size_t size); // 分配,未初始化
void *calloc(size_t nmemb, size_t size); // 分配 + 清零
void *realloc(void *ptr, size_t size); // 重新分配
void free(void *ptr); // 释放
c
int *arr = malloc(100 * sizeof(int));
if (!arr) { /* 失败处理 */ }
arr[0] = 1;
free(arr);
// 清零版(适合数组初始化)
int *zarr = calloc(100, sizeof(int)); // 全 0
// 扩容
arr = realloc(arr, 200 * sizeof(int)); // 可能换新地址
3.2 必背规则
| 规则 | 说明 |
|---|---|
malloc 必检查返回值 |
内存不足返回 NULL |
free(NULL) 安全 |
标准保证什么都不做 |
同一指针只 free 一次 |
重复 free → double free 崩 |
free 后立即 ptr = NULL |
防 use-after-free |
realloc(p, 0) 行为依实现 |
可能返回 NULL,可能等于 free |
realloc 失败原指针仍有效 |
旧块不丢 |
3.3 malloc 内部 ------ brk / mmap 双轨
malloc 不直接调系统调用------每次 malloc(8) 都不会去内核要内存,有内存池。底层实际用:
| 块大小 | 分配方式 | 说明 |
|---|---|---|
| 小块(默认 ≤ 128KB) | brk() 调整堆顶 |
heap 区从 .bss 之上往上长 |
| 大块(> 128KB) | 直接 mmap 一段匿名内存 |
独立映射,不进堆 |
用户层 free 把内存还给 malloc 池,不一定立即归还给 OS 。所以 top 显示进程 RSS 居高不下,但 valgrind 又没找到泄漏 ------ 碎片 + 不归还给 OS。
3.4 看 malloc 行为
c
#include <malloc.h>
malloc_stats(); // 把统计信息打到 stderr
或者 mallinfo2() 拿到结构体编程使用。
3.5 阈值控制
c
#include <malloc.h>
mallopt(M_MMAP_THRESHOLD, 64 * 1024); // 改成 ≥64KB 走 mmap
mallopt(M_TRIM_THRESHOLD, 128 * 1024); // free 后超过这值才 trim 还 OS
4. 常见内存错误

4.1 内存泄漏(Memory Leak)
c
void *p = malloc(100);
// 忘记 free
return;
少量泄漏看不出来,长跑服务 RSS 持续上涨 → OOM。
抓泄漏:valgrind / AddressSanitizer / jemalloc heap profile。详见 29 篇。
4.2 use-after-free
c
char *p = malloc(100);
free(p);
strcpy(p, "hi"); // ⛔ p 已经无效
最阴险的 bug:有时正常有时崩,因为内存可能被 malloc 重用。
防御:
c
free(p);
p = NULL; // 立即置空
// 之后 p[0] 访问 → SIGSEGV,立刻发现
4.3 double free
c
char *p = malloc(100);
free(p);
free(p); // ⛔ 崩或破坏堆
// glibc 报: free(): double free detected
4.4 悬空指针(Dangling Pointer)
c
char *create(void) {
char buf[100]; // 栈
strcpy(buf, "hi");
return buf; // ⛔ 返回时 buf 已失效
}
或者:
c
char *p = malloc(100);
char *alias = p;
free(p);
strcpy(alias, "hi"); // ⛔ alias 也是悬空
4.5 堆缓冲区溢出
c
char *p = malloc(10);
strcpy(p, "this is a long string"); // ⛔ 溢出
破坏堆元数据,下次 malloc 可能崩。
4.6 栈缓冲区溢出
c
void f(const char *input) {
char buf[10];
strcpy(buf, input); // ⛔ input 太长 → 栈溢出,可能改返回地址
}
经典安全漏洞。Linux 现代防御:栈金丝雀(canary)+ NX bit + ASLR。
4.7 未初始化读取
c
char buf[100];
printf("%s\n", buf); // ⛔ buf 是栈上垃圾值
或:
c
char *p = malloc(100); // malloc 不清零
printf("%s\n", p); // ⛔ p 内容是垃圾
要清零用 calloc 或 memset(p, 0, 100)。
5. malloc 的安全 wrapper
工程实践常包一层:
c
void *xmalloc(size_t size) {
void *p = malloc(size);
if (!p) {
fprintf(stderr, "out of memory\n");
abort();
}
return p;
}
void *xcalloc(size_t n, size_t s) {
void *p = calloc(n, s);
if (!p) abort();
return p;
}
void *xrealloc(void *p, size_t size) {
void *q = realloc(p, size);
if (!q && size) abort();
return q;
}
理由:
| 出发点 | 论据 |
|---|---|
| 简化代码 | 把"出错检查"统一做掉,主流程代码更干净 |
| 场景合理 | 大多数程序内存不足就该退出,没法优雅恢复 |
| 行业先例 | vim、git、tmux 都有自己的 xmalloc |
6. 内存分配器选型
glibc 默认是 ptmalloc2,多线程场景碎片严重。生产服务常换:
| 分配器 | 特点 | 用法 |
|---|---|---|
| ptmalloc2 | glibc 默认,通用 | 默认 |
| jemalloc | Facebook 出品,多线程友好 | LD_PRELOAD=/usr/lib/libjemalloc.so ./prog |
| tcmalloc | Google 出品,速度快 | LD_PRELOAD=/usr/lib/libtcmalloc.so ./prog |
| mimalloc | 微软新秀,性能极佳 | LD_PRELOAD=/usr/lib/libmimalloc.so ./prog |
切换很简单:
bash
# 安装 jemalloc
sudo apt install libjemalloc-dev
# 用 LD_PRELOAD 替换
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so ./myapp
或者编译时链接:
bash
gcc main.c -o myapp -ljemalloc
实测:高频 malloc/free 的多线程服务,换 jemalloc 性能常提升 10-30%,内存碎片显著降低。Redis、Nginx、Rust runtime 都支持选择分配器。
7. 自己写对象池 / 内存池
频繁 malloc 同尺寸对象时,对象池比 malloc 快 10-100 倍:
c
typedef struct chunk {
struct chunk *next;
char data[64];
} chunk_t;
static chunk_t *freelist = NULL;
chunk_t *pool_alloc(void) {
if (freelist) {
chunk_t *c = freelist;
freelist = c->next;
return c;
}
return malloc(sizeof(chunk_t));
}
void pool_free(chunk_t *c) {
c->next = freelist;
freelist = c;
}
进阶:arena 分配器(一次性分配大块,像栈一样递增分配):
c
typedef struct {
char *base;
size_t size, used;
} arena_t;
void *arena_alloc(arena_t *a, size_t n) {
if (a->used + n > a->size) return NULL;
void *p = a->base + a->used;
a->used += n;
return p;
}
// 不能单独 free,整个 arena 一锅释放
适合"批量分配 + 一次性释放"场景:HTTP 请求处理、编译器 AST 等。Postgres、Apache、nginx 都内置这类 arena。
8. 内存对齐
C 结构体不是简单"字段大小相加":
c
struct s {
char a; // 1
int b; // 4,要 4 字节对齐
char c; // 1
}; // sizeof(struct s) = 12,不是 6
布局:
| 项 | 内容 |
|---|---|
| 偏移 0 | a (1) |
| 偏移 1-3 | padding |
| 偏移 4 | b (4) |
| 偏移 8 | c (1) |
| 偏移 9-11 | tail padding (对齐 sizeof 到 4) |
调整字段顺序节省内存:
c
struct s2 {
int b; // 4
char a; // 1
char c; // 1
}; // sizeof(struct s2) = 8
强制不对齐(极少用):
c
struct s __attribute__((packed)) { ... }; // sizeof = 6
注意:packed 结构访问慢(CPU 不喜欢未对齐),跨平台未必兼容。
详见 20 篇 C 语言常见坑。
9. valgrind 抓内存错误
终极武器:valgrind --tool=memcheck --leak-check=full ./myapp
10. 总结
进程内存布局速记:

API 速记:
| API | 用途 |
|---|---|
malloc(n) |
分配 n 字节,未初始化 |
calloc(n, sz) |
分配 + 清零 |
realloc(p, n) |
改大小 |
free(p) |
释放,free(NULL) 安全 |
避坑口诀:
- malloc 必检查返回值
- free 后立即置 NULL
- 同一指针只 free 一次
- 局部 char 数组别返回
- 大对象走 堆,不要堆栈
- malloc + memset 就是 calloc
- 怀疑内存问题 valgrind / ASan
金句:内存是 C 程序的命门,搞懂"在哪 / 谁分配 / 谁释放"三件事,半数 bug 不会出现。