入门:先建立 ptmalloc 的整体模型¶
Note
这页的目标不是展开源码细节,而是先建立一套稳定的整体模型:
- 什么是堆
- 什么是
chunk malloc时一块内存大概会怎么被找到free时一块内存大概会去哪里- 后面读源码时,应该把哪些函数当成哪一步来看
总览¶
ptmalloc 本质上只做两件事:
- 尽量复用已经存在的空闲
chunk - 如果现有空间不够,再向系统要新的内存
所以后面看到一堆 tcache、fastbin、smallbin、largebin、top chunk、mmap chunk,不要先被名字吓住。
它们本质上都只是这两个目标下的不同“存放方式”和“处理策略”。
先回答最基础的问题:什么是堆¶
从程序运行的角度看,进程拿到的是一整片虚拟地址空间。
这片空间里会放很多东西,例如:
| Text Only | |
|---|---|
这里先只关心两个最常见的区域:
- 栈:函数调用时自动管理,通常随着函数进入和返回自动增长、回退
- 堆:程序主动申请、主动释放,用来放生命周期更灵活的数据
例如:
- 局部变量通常更接近“放在栈上”
malloc出来的内存通常更接近“放在堆上”
堆最重要的特点不是“大”,而是:
- 它的使用时机不固定
- 它的大小需求不固定
- 谁先申请、谁后释放也不固定
这就带来一个问题:
ptmalloc 做的就是这件事。
它并不是“堆本身”,而是“管理堆中这些内存块的一套分配器”。
所以后面读到 tcache、bin、top chunk 这些名词时,可以先把它们理解成:
- 堆并不是一整块无差别的空间
- 分配器会把堆里的空闲块分门别类地管理起来
malloc/free就是在这些分类结构之间搬运和复用内存块
再换一个角度:ptmalloc 本身就是一套“多级缓存”¶
你可以把 ptmalloc 理解成一套分层缓存系统,而不是“每次都临时去系统要内存”的分配器。
它大概像这样:
| Text Only | |
|---|---|
这张图要表达的不是严格执行顺序,而是一个核心思想:
- 越靠上,越像“已经准备好、希望立刻复用的缓存”
- 越靠下,越像“必须付出更大成本才能拿到的新内存”
把这个视角记住之后,很多设计都会突然顺起来:
- 为什么
free不急着把普通 chunk 还给系统 - 为什么
tcache/fastbin这种“先挂起来再说”的结构会存在 - 为什么
unsorted bin不急着一开始就做最细分类 - 为什么
top chunk会被当成“最后的现成库存”
这套结构的核心可以概括成一句话:
ptmalloc 尽量把后续可能继续使用的 chunk 留在用户态维护的缓存和空闲结构里,而不是频繁重新进入系统调用。
第一层模型:什么是 chunk¶
我们平时写:
| C | |
|---|---|
你以为自己拿到的是“20h 字节内存”,但 ptmalloc 真正管理的是一个更大的对象,叫 chunk。
可以先把它想成这样:
| Text Only | |
|---|---|
也就是说:
- ptmalloc 管理的是整个
chunk - 程序真正拿到的只是
chunk里“用户数据区”的起点 free(p)时,ptmalloc 会把这个用户指针再换算回所属的chunk
Tip
后面读源码时一定要时刻分清两件事:
chunk指针:分配器内部视角- 用户指针:程序拿到的
malloc返回值
第二层模型:一个 chunk 在“使用中”和“空闲”时长得不一样¶
1. 使用中的 chunk¶
可以先粗暴地理解成:
| Text Only | |
|---|---|
这时 ptmalloc 只需要知道:
- 这块多大
- 前一块/当前块的一些状态信息
- 这块现在是给用户用的,还是已经空闲
2. 空闲后的 chunk¶
一旦这块被 free,用户数据区就不再属于你了,ptmalloc 会把其中一部分拿来串链表、做缓存、记录下一个空闲块是谁。
可以把它理解成:
| Text Only | |
|---|---|
这里通常会产生两个直接的问题:
- 为什么
free之后原来的内容会被“破坏” - 为什么利用里总说“free 后用户区会被写入指针”
原因很简单:
这块内存在 free 之后,已经从“你的缓冲区”变回了“分配器要管理的空闲块”。
第三层模型:先认识几个最重要的角色¶
读 ptmalloc,最先要认识的不是代码,而是“角色分工”。
| 名词 | 先把它想成什么 | 作用 |
|---|---|---|
arena |
一个分配现场 / 一个管理区域 | 管着一批 heap 和 bin |
chunk |
一块被 ptmalloc 管理的内存单元 | 分配和释放的基本对象 |
tcache |
线程本地的小缓存 | 为了更快地复用小块 |
fastbin |
一组只追求快的小块空闲链 | free 后先挂进去,通常不立刻合并 |
unsorted bin |
普通空闲块的“中转站” | 刚释放/合并后的块先来这里 |
smallbin |
固定大小的小块仓库 | 同尺寸块放一起 |
largebin |
较大块仓库 | 按大小组织,便于找合适块 |
top chunk |
堆顶还没切出去的那一大块 | 现成仓库不够时,直接从这里切 |
mmap chunk |
单独向系统映射的大块 | 通常不走普通 bin 体系 |
Note
从整体上看,最重要的不是先背出每个 bin 的精确定义,而是先区分它们各自承担的角色:
tcache/fastbin:偏“快”unsorted bin:偏“中转”smallbin/largebin:偏“正规仓库”top chunk:偏“还没分出去的原料”mmap chunk:偏“单独订单”
arena、heap、chunk 到底是什么关系¶
这三个词特别容易混。
可以先把它们拆成三层:
| Text Only | |
|---|---|
如果要更口语一点:
arena更像“仓库管理员”heap更像“仓库的场地”chunk更像“场地上被切出来的一件件货物”
对主线程和其他线程,再粗分一次¶
按常见情况粗分,可以先写成:
| Text Only | |
|---|---|
这不是说主线程绝不会碰 mmap,也不是说其他线程完全不碰普通 chunk。
它只是先给出一个整体上的方向区分:
main arena:更像“主仓库”non-main arena:更像“分仓”
Note
后面读源码时会反复看到两个判断:
- 当前是不是
main_arena - 当前这块 chunk 是不是
NON_MAIN_ARENA
它背后其实就是在问:这块内存来自主仓库,还是分仓。
各空闲结构的大致定位¶
这几类结构不需要一开始就分别背细。
先知道它们各自负责什么,模型就已经够用了。
| 结构 | 可以先理解成什么 | 整体特点 |
|---|---|---|
tcache |
线程本地的小缓存 | 最近释放的小块,优先快速复用 |
fastbin |
分配器内部的小块快缓存 | 很快,但通常不立刻合并 |
unsorted bin |
普通空闲块的中转站 | 先放这里,再看是否直接复用或继续分类 |
smallbin |
小块正式仓库 | 同尺寸块集中管理 |
largebin |
大块正式仓库 | 更强调按大小找合适块 |
top chunk |
堆顶剩余原料 | 仓库不够时直接从这里切 |
如果只看顺序感,可以先记住下面这几个点:
tcache、fastbin更偏向“最近释放的先拿出来”,也就是更接近LIFOsmallbin更偏向稳定复用,同尺寸块整体更接近FIFO/LRUlargebin更关注“大小合不合适”unsorted bin不是最终归宿,而是一次过渡和再利用机会
从“仓库”视角看分配¶
可以把 ptmalloc 想成一个仓库管理员。
你来要一块空间,它会按下面这个大方向去找:
| Text Only | |
|---|---|
第一步:先“规格化”请求¶
用户写的是:
| C | |
|---|---|
但 ptmalloc 不会真的按 0x23 这么离散的大小去管理。
它会先做几件事:
- 加上管理开销
- 按对齐规则向上取整
- 得到真正要处理的
chunk size
所以你申请 0x23、0x24、0x25,最后很可能都落到同一个大小档里。
这一步很重要,因为后面很多分支判断看的都不是“原始请求大小”,而是“规格化之后的 chunk 大小”。
第二步:优先复用最近释放过的块¶
如果有现成空闲块,最便宜的办法当然不是去问系统再要,而是直接复用。
最先被看的通常是这些地方:
tcachefastbinsmallbinunsorted binlargebin
Note
这里描述的是全局路径,不是说每一次 malloc 都会把这几站完整走一遍。
实际源码会根据请求大小、当前缓存状态、bin 命中情况走不同分支。
不要把它理解成“五个平等的容器”,更好的理解是:
tcache:离线程最近,拿起来最顺手fastbin:很快,但管理比较粗糙unsorted bin:刚回收的大多数普通块会先在这里停一下smallbin/largebin:更正式、更稳定的仓库
按请求规模和命中情况拆开后,常见路径可以归纳为四类¶
路径 1:小块,而且刚好在线程本地缓存里¶
这是最轻、最快、最适合高频小申请的一条路。
路径 2:小块,tcache 没命中,但仓库里有现成货¶
| Text Only | |
|---|---|
这条路的关键词是“复用库存”。
路径 3:仓库没有现成货,但堆顶还有原料¶
这条路的关键词是“现切”。
路径 4:连 top chunk 都不够¶
这条路的关键词是“进货”。
第三步:仓库都没有,就从 top chunk 现切¶
如果现成的空闲块都不合适,ptmalloc 还会看 top chunk。
你可以把它想成:
| Text Only | |
|---|---|
当请求到来时,如果 top chunk 足够大,就直接从前面切一块出去:
| Text Only | |
|---|---|
这一步非常关键,因为它解释了很多源码里的核心判断:
- “有没有现成 free chunk”
- “没有的话 top chunk 够不够”
- “还不够才去
sysmalloc”
第四步:top chunk 也不够,就向系统申请¶
这时才轮到真正的“进货”。
大方向可以先理解成两种:
- 给普通堆继续扩空间
- 直接为某个大请求单独做一块映射
也就是:
两个特殊 chunk¶
ptmalloc 里有几类块并不属于普通 bin 的稳定流转体系,其中最重要的是下面两个:
1. top chunk¶
它不是 bin 里的空闲块,而是“堆顶剩下的那一整块原料”。
你可以把它理解成:
- 还没有正式放进 bin 分类体系
- 但分配器知道它在这儿
- 只要足够大,就可以直接切
2. mmap chunk¶
它也不是 bin 体系里的一员。
它更像“专门为某次较大需求单独开的一单”。
所以它的典型命运是:
这也是为什么这里要单独把它提出来:
mmap chunk通常不进入普通 binfree它时更像“直接退货给系统”
从“仓库”视角看 free¶
free 不是“把内存直接还给操作系统”。
更准确地说,free 做的是:
- 先把这块内存交还给 ptmalloc
- 看它应该先进缓存、先进 bin、还是直接释放给系统
它的大方向可以先记成下面这张图:
| Text Only | |
|---|---|
Note
这同样是一条抽象主线。真实执行时,某些块会很早就在 tcache 或 fastbin 停住,某些块则会直接落入合并与 unsorted bin 逻辑,不是每次都把所有步骤走完。
按来源和大小拆开后,free 的典型路径可以归纳为四类¶
路径 1:小块,先回 tcache¶
这条路径的重点是线程局部缓存优先。
路径 2:再往下一层,先回 fastbin¶
这条路径仍然偏向“先快速挂起”,而不是立刻彻底整理。
路径 3:普通块,走合并与整理¶
| Text Only | |
|---|---|
这条路径会进入更完整的空闲管理流程。
路径 4:本来就是大块单独映射¶
这条路径不进入普通 bin 体系。
普通 free 的主线其实只要先把下面三件事想清楚就够了:
- 小块可能先进入
tcache或fastbin - 普通块通常会看能不能和前后空闲块合并
- 合并后的块常先去
unsorted bin,再决定是否继续分类;如果一路并到顶端,还可能触发trim
这三件事基本就覆盖了后面源码里大多数关键判断的出发点。
一张图把分配和释放串起来¶
把分配和释放放在同一张总图里,可以更容易看出它们之间的闭环关系:
再加一张“单个 chunk 的生命周期图”¶
如果上面那张是“整个仓库视角”,这张可以帮助你从“单个 chunk 的命运”去理解。
| Text Only | |
|---|---|
这张图最想表达的是:
chunk的命运不是“一次性”的- 一块 chunk 可能在“使用中”和“空闲体系”之间来回流转很多次
- ptmalloc 的工作重点就是让这种流转尽量高效
不同类型 chunk 的典型命运¶
| 类型 | 分配时怎么来 | free 后通常去哪里 | 整体定位 |
|---|---|---|---|
tcache chunk |
常从线程本地缓存直接拿 | 优先回线程本地缓存 | 最近刚用过的小块 |
fastbin chunk |
快速复用小块 | 先进 fastbin,稍后再整理 |
为了快,先不急着合并 |
smallbin chunk |
从固定大小仓库里拿 | 合适时会进入 smallbin | 同尺寸块整齐摆放 |
largebin chunk |
从较大空闲块里找 | 合适时会进入 largebin | 需要按大小挑选 |
unsorted chunk |
常作为中转被重新利用 | 普通 free/合并后先到这里 | 临时中转站 |
top chunk |
不够时直接从堆顶切 | 和顶端合并后可能变大 | 还没分出去的原料 |
mmap chunk |
对较大申请单独映射 | 通常直接 munmap |
不走普通 bin 体系 |
几个容易混淆的问题¶
1. 为什么 free 不等于“立刻还给系统”?¶
因为 ptmalloc 更希望复用。
频繁“还给系统再重新申请”通常更慢,碎片控制也未必更好。
2. 为什么有的 free 会合并,有的不会?¶
因为不同路径目标不同:
tcache/fastbin更偏“快”- 普通 free 更偏“整理与合并”
3. 为什么会先有 unsorted bin,再有 smallbin / largebin?¶
因为 ptmalloc 想先给这些新回收的块一个“快速被再次利用”的机会,而不是一上来就做最重的分类工作。
4. 为什么读源码时总觉得一个 chunk 一会儿是数据,一会儿又变成链表节点?¶
因为“用户数据区”在 free 之后已经不再是用户数据区,而是分配器拿来管理空闲块的工作空间。
读源码时,怎么把这个模型对上函数¶
下面这张表可以作为后续精读时的“函数地图”:
| 你脑子里的步骤 | 源码里常看到的函数 |
|---|---|
进入 malloc |
__libc_malloc |
| 真正的主分配流程 | _int_malloc |
| 现有空间不够,向系统申请 | sysmalloc |
直接走大块 mmap 分配 |
sysmalloc_mmap |
进入 free |
__libc_free |
| 普通释放主流程 | _int_free |
| fastbin 后续统一整理 | malloc_consolidate |
释放 mmap 大块 |
munmap_chunk |
| main arena 尝试缩堆 | systrim |
| 线程 heap 尝试收缩 | heap_trim |
Tip
后面读源码时,不要要求自己“每个分支第一次就全懂”。
更有效的方法是:
- 先判断当前在做的是“分配”还是“释放”
- 再判断它是在“复用空闲块”还是“向系统申请/归还”
- 最后再看这一条具体落在了
tcache、fastbin、unsorted、top chunk还是mmap
归纳¶
- ptmalloc 不是直接管理“用户指针”,而是管理
chunk。 malloc的核心是“先复用,不够再申请”。free的核心是“先交还给分配器,再决定缓存、合并还是归还系统”。tcache/fastbin偏快,unsorted偏中转,smallbin/largebin偏正式管理。top chunk是还没切出去的堆顶原料,mmap chunk是单独订单。- 读源码时,先认清“这块 chunk 现在处于哪一种命运里”,很多分支就不会乱。
下一步怎么读¶
如果这页已经看顺了,接下来比较自然的顺序是:
这样读时,你会更容易知道每一段代码在整个模型里的位置,而不是一开始就陷在宏、结构体和分支里。