malloc 底层:brk vs mmap
一句话:你调
malloc(100)申请 100 字节——glibc 不会傻到每次都找内核要。它自己先管着一片内存池,小块的从池里划,大块(>128KB)才找内核单独要。划的方式有两种:brk(推堆顶)和 mmap(独立开辟)。
一、进程的内存长啥样?
你的程序跑起来之后,它在内存里的布局是这样的:graph TD A[代码段 .text] --> B[数据段 .data] B --> C[堆 Heap<br/>malloc在这拿内存] C --> D[共享库 printf等] D --> E[栈 Stack<br/>局部变量] E --> F[内核空间]
好了,你现在知道——堆(Heap)就是你 malloc 拿内存的地方。
堆的顶端叫 program break(程序断点)。
堆的大小 = 当前 program break 的位置 - 堆的起始位置。
类比:堆就像一个可以拉长的大抽屉。
你拉出多长(program break 推多高),你就有多少空间。二、glibc 到底怎么从内核拿内存?
两种方法:brk 和 mmap。是重点内容。brk——推堆顶
graph TD A[代码段] --> B[数据段] B --> C[堆已分配区] C --> D[堆空闲区] D --> E[堆顶] E --> F[未映射内存]
brk 怎么工作的:
堆的顶端有个指针叫 program break。
你需要内存时,把这个指针往上推——推出来的空间就是新的内存。
不需要时,把指针降下来——还给进程。
⚠️ 注意:brk 也是系统调用,也要切内核态。
但它比 mmap 轻量——它只是在已有的虚拟内存区域(VMA)上调整边界,
不像 mmap 要创建一个全新的 VMA。
类比:brk = 把围墙往外挪一挪。mmap = 在旁边另盖一栋房子。brk 的特点:
✅ 快——系统调用里只改一个边界值,工作轻
✅ 连续——分出来的内存都在堆上,地址连着
✅ 小内存友好——碎片可以复用
❌ 释放不干净——中间空了块,堆顶也降不下来
OS 以为你还占着那块地
❌ 碎片多了堆越来越大——假性内存泄漏mmap——另起炉灶
graph TD A[代码段] --> B[数据段] B --> C[堆 brk管] C --> D[mmap区 多块独立区域] D --> E[栈] E --> F[内核空间]
mmap 本来是"把文件映射到内存"用的。
但也可以匿名映射——纯粹要一块内存,不和文件挂钩。
你每次 mmap,内核就在你的地址空间里单独开一块新区。
这块区和堆无关,和其他 mmap 区域也无关——各自独立。
当你 free 这块内存时,munmap 直接把它删干净,归还 OS。
就像临时租仓库——用完就退,不占你主店的地方。mmap 的特点:
✅ 释放即归还——free 后 OS 立刻收回,不假性泄漏
✅ 大块友好——几 MB 单独维护,不和堆上的小块掺和
✅ 互不影响——碎片只发生在自己块内,不影响别人
❌ 慢——每次 mmap 都是系统调用,而且内核要建 VMA
❌ 最少一页(4KB)——你只要 1 字节也得给你一整页
❌ 地址不连续——每次映射的位置不一定连着三、brk 的碎片问题
场景:你把货架上放了三堆货
堆布局(从低到高):
[p1=100B] [p2=100B] [p3=100B] [空闲] ← 堆顶
你取走了 p2(free(p2)):
[p1=100B] [空闲100B] [p3=100B] [空闲] ← 堆顶还在原地
↑ 堆顶降不了——p3 在 p2 上面挡着
又来一个新客人要 200B:
找遍空闲块——最大只有 100B(p2 的坑)
不够 → 只能从堆顶继续推
[p1=100B] [空闲100B] [p3=100B] [p4=200B] ← 堆顶推上去了
那个 100B 的空洞就永远留在那了——这叫"碎片"
堆越来越大,实际利用率越来越低brk 碎片 = 堆里出现了"瑞士奶酪"——到处是洞,但堆顶降不下来。
mmap 没这问题:
每块 mmap 是独立的一整块,free 了整块消失。
但每块至少 4KB——小块用 mmap 浪费空间。
这是空间换干净的交易。总结:malloc 底层 = 批零分离。brk 管零售(小内存、快、有瑞士奶酪问题),mmap 管批发(大内存、慢但干净)。128KB 是 glibc 的分界线。
速记卡(面试闪卡)
Q1:一句话讲清「malloc 底层:brk vs mmap」到底是什么? A:malloc 不是每次都找内核:小块从内存池划,大块(>128KB)才用 brk 推堆顶或 mmap 另开。
Q2:一、进程的内存布局 —— 怎么理解? A:堆(Heap)是你 malloc 拿内存的地方,顶端叫 program break。类比:堆像能拉长的大抽屉——你拉出多长(program break 推多高)就有多少空间。
Q3:二、brk——推堆顶 —— 怎么理解? A:brk 把堆顶指针往上推就得到新内存,只是改已有 VMA 的边界,比 mmap 轻量、地址连续、小内存碎片可复用;但中间空一块堆顶降不下来,碎片多了堆假性膨胀(假性内存泄漏)。类比:brk=把围墙往外挪。
Q4:二、mmap——另起炉灶 —— 怎么理解? A:mmap 在内核地址空间单独开一块独立新区(可匿名映射),free 后 munmap 立刻归还 OS、不假性泄漏,大块友好;但每次都是系统调用+建 VMA、慢,且最少一页(4KB)、地址不连续。类比:mmap=在旁边另盖一栋仓库,用完就退。
Q5:三、brk 的碎片问题与分界线 —— 怎么理解? A:brk 碎片像”瑞士奶酪”:释放中间块堆顶降不下来,洞永久留着,堆越撑越大。mmap 没这问题但小块浪费(每块≥4KB),是”空间换干净”。结论:malloc=批零分离,brk 管零售、mmap 管批发,128KB 是 glibc 的分界线。
Q6:核心速记主线有哪些?
- 堆顶 program break,brk 推堆顶、mmap 另开独立区
- brk:轻量连续、小内存友好,但碎片会让堆假性膨胀
- mmap:释放即归还、大块友好,但慢且最少 4KB、不连续
- 批零分离:小块走 brk 池,大块(>128KB)走 mmap
- 瑞士奶酪碎片 vs 空间换干净
口诀 A:malloc 不每次求内核,小池划来大另开 brk 推墙快却藏奶酪,mmap 盖房慢但干净来 一百二八是分界,批零分离最自在 碎在堆中顶难降,用罢即还无公害