☰
linux系统编程(十七):内存管理基础
2026/10/27 10:17:53 网站建设 项目流程

内存管理基础 —— 进程内存布局与 malloc 全家桶

C 程序员吃过最多的亏都是内存:栈溢出、堆泄漏、free 后再用、double free、悬空指针。这篇按"进程内存布局 → 栈 vs 堆 → malloc/free 怎么工作 → 常见内存错误 → 内存分配器选型"五步讲清楚,读完后再写 C,对每一个变量 / 每一次 malloc 心里有数。


0. 引言:这个 bug 在哪?

char*get_name(void){charbuf[64];strcpy(buf,"alice");returnbuf;// ⛔ 返回栈上指针}intmain(void){char*name=get_name();printf("%s\n",name);// 可能打印对,可能崩,看运气return0;}

类似的还有:

char*p=malloc(100);free(p);p[0]='X';// ⛔ use-after-freechar*q=malloc(100);free(q);free(q);// ⛔ double free

这些坑都和"内存到底在哪"有关。下面把 Linux 进程内存布局拆开看。


1. 进程内存布局

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

各区对应(结合 19 篇 ELF):

区对应 ELF 节谁分配谁释放
.text.textOS 加载进程退出
.rodata.rodataOS 加载进程退出
.data.dataOS 加载并初始化进程退出
.bss.bssOS 加载并清零进程退出
Heap—malloc / brk / mmapfree / munmap
Stack—编译器自动函数返回自动
共享库 mmap各 .so 段ld.so进程退出

2. 栈(Stack)

2.1 行为

特性说明
每个线程一个栈pthread 默认 8MB(详见第 1 篇)
函数调用压栈帧帧内含参数、返回地址、局部变量
函数返回弹栈帧自动恢复 SP,局部变量随之失效
栈向下生长高地址 → 低地址

2.2 局部变量在栈上

voidf(intn){intlocal_a=10;// 栈charbuf[1024];// 栈structpointp={1,2};// 栈}// f 返回后,local_a / buf / p 全部失效

2.3 栈的优势

优势说明
零开销分配编译期算好偏移,函数入口一次性SP -= N完成
自动释放函数返回SP += N,无需手动free
缓存友好连续地址,CPU cache 命中率高

2.4 栈的局限

局限说明
生命周期受限函数返回就没了
大小有限默认 8MB,超了SIGSEGV(栈溢出)
递归危险每次调用一帧,深递归爆栈

2.5 栈大小调整

# 查看ulimit-s# 8192 (KB)# 临时改ulimit-s16384# 16MB# 永久改 /etc/security/limits.conf

线程级也能在 pthread_create 前设置:

pthread_attr_tattr;pthread_attr_init(&attr);pthread_attr_setstacksize(&attr,16*1024*1024);// 16MBpthread_create(&tid,&attr,fn,NULL);

2.6 栈溢出表现

voidrecur(intn){charbig[1024];recur(n+1);}intmain(void){recur(0);}// Segmentation fault (core dumped)

或者大局部数组:

voidf(void){charbuf[10*1024*1024];// 10MB > 8MB → 栈溢出}

经验法则:单函数局部变量总和 ≤ 几 KB;大数据用 malloc 走堆。


3. 堆(Heap)

3.1 malloc / free / realloc / calloc

#include<stdlib.h>void*malloc(size_tsize);// 分配,未初始化void*calloc(size_tnmemb,size_tsize);// 分配 + 清零void*realloc(void*ptr,size_tsize);// 重新分配voidfree(void*ptr);// 释放
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 行为

#include<malloc.h>malloc_stats();// 把统计信息打到 stderr

或者mallinfo2()拿到结构体编程使用。

3.5 阈值控制

#include<malloc.h>mallopt(M_MMAP_THRESHOLD,64*1024);// 改成 ≥64KB 走 mmapmallopt(M_TRIM_THRESHOLD,128*1024);// free 后超过这值才 trim 还 OS

4. 常见内存错误

4.1 内存泄漏(Memory Leak)

void*p=malloc(100);// 忘记 freereturn;

少量泄漏看不出来,长跑服务 RSS 持续上涨 → OOM。

抓泄漏:valgrind / AddressSanitizer / jemalloc heap profile。详见 29 篇。

4.2 use-after-free

char*p=malloc(100);free(p);strcpy(p,"hi");// ⛔ p 已经无效

最阴险的 bug:有时正常有时崩,因为内存可能被 malloc 重用。

防御:

free(p);p=NULL;// 立即置空// 之后 p[0] 访问 → SIGSEGV,立刻发现

4.3 double free

char*p=malloc(100);free(p);free(p);// ⛔ 崩或破坏堆// glibc 报: free(): double free detected

4.4 悬空指针(Dangling Pointer)

char*create(void){charbuf[100];// 栈strcpy(buf,"hi");returnbuf;// ⛔ 返回时 buf 已失效}

或者:

char*p=malloc(100);char*alias=p;free(p);strcpy(alias,"hi");// ⛔ alias 也是悬空

4.5 堆缓冲区溢出

char*p=malloc(10);strcpy(p,"this is a long string");// ⛔ 溢出

破坏堆元数据,下次 malloc 可能崩。

4.6 栈缓冲区溢出

voidf(constchar*input){charbuf[10];strcpy(buf,input);// ⛔ input 太长 → 栈溢出,可能改返回地址}

经典安全漏洞。Linux 现代防御:栈金丝雀(canary)+ NX bit + ASLR。

4.7 未初始化读取

charbuf[100];printf("%s\n",buf);// ⛔ buf 是栈上垃圾值

或:

char*p=malloc(100);// malloc 不清零printf("%s\n",p);// ⛔ p 内容是垃圾

要清零用calloc或memset(p, 0, 100)。


5. malloc 的安全 wrapper

工程实践常包一层:

void*xmalloc(size_tsize){void*p=malloc(size);if(!p){fprintf(stderr,"out of memory\n");abort();}returnp;}void*xcalloc(size_tn,size_ts){void*p=calloc(n,s);if(!p)abort();returnp;}void*xrealloc(void*p,size_tsize){void*q=realloc(p,size);if(!q&&size)abort();returnq;}

理由:

出发点论据
简化代码把"出错检查"统一做掉,主流程代码更干净
场景合理大多数程序内存不足就该退出,没法优雅恢复
行业先例vim、git、tmux 都有自己的xmalloc

6. 内存分配器选型

glibc 默认是 ptmalloc2,多线程场景碎片严重。生产服务常换:

分配器特点用法
ptmalloc2glibc 默认,通用默认
jemallocFacebook 出品,多线程友好LD_PRELOAD=/usr/lib/libjemalloc.so ./prog
tcmallocGoogle 出品,速度快LD_PRELOAD=/usr/lib/libtcmalloc.so ./prog
mimalloc微软新秀,性能极佳LD_PRELOAD=/usr/lib/libmimalloc.so ./prog

切换很简单:

# 安装 jemallocsudoaptinstalllibjemalloc-dev# 用 LD_PRELOAD 替换LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so ./myapp

或者编译时链接:

gcc main.c-omyapp-ljemalloc

实测:高频 malloc/free 的多线程服务,换 jemalloc 性能常提升 10-30%,内存碎片显著降低。Redis、Nginx、Rust runtime 都支持选择分配器。


7. 自己写对象池 / 内存池

频繁 malloc 同尺寸对象时,对象池比 malloc 快 10-100 倍:

typedefstructchunk{structchunk*next;chardata[64];}chunk_t;staticchunk_t*freelist=NULL;chunk_t*pool_alloc(void){if(freelist){chunk_t*c=freelist;freelist=c->next;returnc;}returnmalloc(sizeof(chunk_t));}voidpool_free(chunk_t*c){c->next=freelist;freelist=c;}

进阶:arena 分配器(一次性分配大块,像栈一样递增分配):

typedefstruct{char*base;size_tsize,used;}arena_t;void*arena_alloc(arena_t*a,size_tn){if(a->used+n>a->size)returnNULL;void*p=a->base+a->used;a->used+=n;returnp;}// 不能单独 free,整个 arena 一锅释放

适合"批量分配 + 一次性释放"场景:HTTP 请求处理、编译器 AST 等。Postgres、Apache、nginx 都内置这类 arena。


8. 内存对齐

C 结构体不是简单"字段大小相加":

structs{chara;// 1intb;// 4,要 4 字节对齐charc;// 1};// sizeof(struct s) = 12,不是 6

布局:

项内容
偏移 0a (1)
偏移 1-3padding
偏移 4b (4)
偏移 8c (1)
偏移 9-11tail padding (对齐 sizeof 到 4)

调整字段顺序节省内存:

structs2{intb;// 4chara;// 1charc;// 1};// sizeof(struct s2) = 8

强制不对齐(极少用):

structs__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 不会出现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询