linux系统编程(十七):内存管理基础

内存管理基础 ------ 进程内存布局与 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 不会出现。

相关推荐
严同学正在努力2 小时前
PostgreSQL 数据库安装部署教程(Linux 多发行版 + Windows + Docker 全覆盖)
linux·数据库·postgresql
2601_965449082 小时前
AI视频制作:做文物视频,青铜器瓷器的图难找,AI怎么自动扒文物素材?
人工智能·音视频
艾芯微科技3 小时前
【音频线路驱动方案分享】圣邦微 SGM8903YTS14G,无电容立体声线路驱动器,解决 POP 声痛点
音视频
EasyCVR3 小时前
应急指挥怕延迟?聊聊视频汇聚/视频融合/视频分析EasyCVR平台WebRTC低延迟方案
音视频·webrtc·easycvr
蒲公英内测分发3 小时前
智能家居厂商有多个产品线,包装上的 App 下载入口该共用还是分开?
android·人工智能·数码相机·机器人·智能家居·智能硬件
开开心心就好3 小时前
电脑小说阅读器,支持朗读和自定义皮肤
服务器·开发语言·javascript·算法·pdf·c#·音视频
oyzz1203 小时前
CentOS Stream 9 配置静态 IP 口令是多少
linux·tcp/ip·centos
hunterandroid3 小时前
[Android 从零到一] Dagger2 依赖注入深度解析:编译时注解处理与依赖图生成
android
信可维3 小时前
用无头 Chrome 逐屏截图 + ffmpeg 拼竖版视频:1080×1920 全流程与 4 个坑
chrome·ffmpeg·音视频