K07 — The Story Behind malloc: brk, mmap, and the ptmalloc Allocator
Key Insight: The malloc you call hides a complete allocator system behind it. It is more complex than you think.
malloc Is Not a System Call
Many people think malloc is a system call. In fact, malloc is a glibc library function. It may ultimately call brk or mmap, or it may not call the system at all.
malloc 的调用路径:
用户代码:
char *p = malloc(1024);
glibc 实现(ptmalloc):
if (size <= 128KB) {
// 小于 128KB:用 brk/sbrk 扩展堆
// 从 arena 的 free list 分配
} else {
// 大于 128KB:用 mmap 直接映射
// 直接从内核分配整块
}
系统调用(可能触发):
brk():调整堆顶(heap top)
mmap():创建匿名映射
munmap():释放映射
关键点:
- 小分配:brk + 用户态分配器(无系统调用)
- 大分配:mmap + munmap(系统调用)ptmalloc: glibc’s Memory Allocator
ptmalloc 的核心概念:
Arena(内存竞技场):
- 每个线程有自己的 arena(避免锁竞争)
- arena 管理一堆 bin(空闲链表)
- 主线程用 main_arena,其他线程用 per-thread arena
Bin(空闲链表):
- fast bin:< 64 bytes,先进后出(不能合并)
- small bin:< 1024 bytes,先进先出
- large bin:>= 1024 bytes,按大小排序
- unsorted bin:刚释放的块(可能合并)
分配策略:
1. 检查 thread local cache(tcache)
2. 从对应 size 的 bin 取
3. 如果 bin 空,从 arena 的 top chunk 取
4. 如果 top chunk 不够,调用 brk/mmap
Free 策略:
1. 放入 thread local cache(tcache)
2. 如果 cache 太大,批量放回 bin
3. 相邻的 free chunk 可以合并(top chunk 不合并)malloc_chunk Real Layout (64-bit)
64-bit 系统上,一个已分配的 malloc_chunk 长这样:
+--------+───────────────+
│ prev_size (8B) │ 前一个 chunk 的大小(如果前一个 chunk 空闲) │
+----------------+----------------+
│ size (8B) │ 本 chunk 大小(低 3 bit 是标志位) │
+----------------+----------------+
│ fd (8B) │ 下一个同 bin 的 chunk 指针(已分配时用户可用) │
+----------------+----------------+
│ bk (8B) │ 上一个同 bin 的 chunk 指针(已分配时用户可用) │
+----------------+----------------+
│ 用户数据区域 │
│ (实际可用的内存) │
+---------------------------------+
size 的低 3 bit 标志位(flag bits):
bit 0 = PREV_INUSE :前一个 chunk 处于已分配状态
bit 1 = IS_MMAPPED :本 chunk 由 mmap 分配
bit 2 = NONMAIN_ARENA :不属于主 arena
注意:
- 最小 chunk:32 bytes(32-bit)/ 48 bytes(64-bit,含头部)
- 如果前一个 chunk 已分配,prev_size 区域可以被当前 chunk 用来存数据
- 这就是为什么 malloc(16) 实际可能占用 32+ bytes
空闲 chunk 和已分配 chunk 的布局是一样的,
区别是空闲时 fd/bk 用来串链表,已分配时 fd/bk 是用户的"免费"存储空间。Fastbin LIFO Behavior
fastbin 为什么不能合并?
fastbin 是 small bin 的一种,专门处理 <= 64 bytes 的小块。
设计决策:性能优先,安全次之。
- 正常 free chunk 合并:检查相邻 chunk,更新 size,插入 unsorted bin
- fastbin 不合并:直接插入对应大小的链表头部
LIFO(后进先出)示例:
malloc(32) -> chunk A
malloc(32) -> chunk B
free(B) -> fastbin[32]: B
free(A) -> fastbin[32]: A -> B (A 在头部)
再分配 32 bytes 时,先取 A(最新的)。
为什么不合并?
- 合并需要遍历相邻块,开销大
- fastbin 设计的本意是"小对象用完就还",合并反而慢
危险:
- fastbin 可以被 double-free 攻击
- glibc 对 fastbin 做了检测,但逻辑复杂时仍可能绕过的
tcache(thread local cache,glibc 2.26+):
tcache 是 per-thread 的缓存,比 fastbin 更激进:
- 每个线程有自己的 tcache(类似 tcmalloc 的 thread local area)
- 分配:先查 tcache,有则直接返回(无锁,O(1))
- tcache 满了才走 arena/bin
- free:先放 tcache,tcache 满了才放回 binThe Boundary Between brk and mmap
为什么是 128KB?
临界值(MMAP_THRESHOLD):
- 默认 128KB(可通过 mallopt(M_MMAP_THRESHOLD, N) 调整)
- <= 128KB:用 brk 扩展堆
- > 128KB:用 mmap 匿名映射
brk 的优点:
- 分配/释放快(用户态操作,不需要系统调用)
- 内存连续(相对于 mmap)
mmap 的优点:
- 释放容易(munmap 整个块)
- 不产生碎片(相对于 brk 扩展堆)
性能对比(分配 + 释放):
1. malloc(100) + free():~10 ns(brk,用户态)
2. mmap(2MB) + munmap():~500 ns(系统调用)
这就是为什么大对象用 mmap 更好:
- 大对象释放不需要合并到堆
- 整块 munmap 效率高mallopt in Practice: Adjusting the mmap Threshold
#include <malloc.h>
#include <stdio.h>
int main() {
// 默认 128KB,改成 4KB
// 所有 > 4KB 的分配都走 mmap
mallopt(M_MMAP_THRESHOLD, 4096);
char *p = malloc(8192); // 8KB > 4KB,走 mmap
printf("mmap'd: %pn", p);
free(p); // munmap 整块,无碎片
// 改回默认
mallopt(M_MMAP_THRESHOLD, 128 * 1024);
return 0;
}Mallopt 可调参数:
M_MMAP_THRESHOLD :mmap 阈值(默认 128KB)
M_MMAP_MAX :最大 mmap 次数(默认 65536)
M_TRIM_THRESHOLD :调用 munmap 收缩堆的阈值(默认 128KB)
M_TOP_PAD :brk 扩展时多要的"余量"(默认 0)
典型调优场景:
- 高频分配大缓存(>1MB):设置 M_MMAP_THRESHOLD=1MB 减少碎片
- 内存敏感服务:设 M_TRIM_THRESHOLD=0 禁止收缩(减少 munmap 系统调用)Why Frequent malloc/free Slows Down
内存分配器的性能问题:
1. 碎片化:
- 多次分配释放后,堆碎片化
- 大块请求可能找不到连续空间
- 需要调用 brk 调整堆顶
2. 锁竞争(多线程):
- 每个 arena 有一把锁
- 线程 A 释放时,线程 B 可能正在分配
- 锁竞争导致上下文切换
3. Top chunk 合并问题:
- free 的 chunk 放入 bin,不会和 top chunk 合并
- 除非调用 malloc_consolidate,否则碎片不会消失
4. 缓存失效:
- free 后,chunk 放入 bin
- 再次分配同一大小,可能不在 CPU cache 中
解决:tcmalloc / jemalloc / mimalloc
tcmalloc(Google):
- 每个线程有自己的 thread cache
- 无锁分配(thread local)
- 分配 O(1)
- 适合多线程程序
jemalloc(Facebook):
- 类似的 thread cache 设计
- 更好的碎片管理
- 被 Firefox 等使用
mimalloc(Microsoft):
- 更紧凑的布局
- 更好的安全属性(隔离)
- 微软云服务使用
多线程 malloc 锁竞争实测(定性):
2 线程:~5% 时间在等锁
8 线程:~30% 时间在等锁(取决于分配频率)
32 线程:锁竞争成为主要瓶颈
切到 tcmalloc/jemalloc 后,8 线程 QPS 可提升 3~5 倍。Summary
- malloc is a Library Function: Not a system call; may internally call brk/mmap or only operate the user-space allocator
- ptmalloc: glibc’s allocator; arena + bin (free list) + cache
- malloc_chunk: 64-bit: prev_size (8B) + size (8B) + fd/bk pointers; lower 3 bits of size are flags
- fastbin: LIFO, no merging; optimized for small objects. tcache is a more aggressive per-thread cache
- Allocation Strategy: Small objects (<=128KB) use brk + bin; large objects (>128KB) use mmap
- mallopt: Adjustable mmap threshold/count/trim parameters; practical tuning tool
- Fragmentation: Heap fragments after repeated alloc/free; causes performance degradation
- Alternatives: tcmalloc/jemalloc/mimalloc; use thread-local caching to reduce lock contention
Next (K08): How do processes share memory? shmget/shmat, POSIX shm_open, and different mmap(MAP_SHARED) implementations.
Comments