A02 —— 堆的内部结构:arena、bin 和 chunk
金句:malloc 管理的堆,是一个由 bin 和 chunk 组成的精密系统。
malloc_chunk 的结构
malloc 把内存分成 chunk(块)来管理:
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; // 前一个 chunk 的大小(如果前一个空闲)
INTERNAL_SIZE_T size; // 本 chunk 大小 + 标志位
malloc_chunk *fd; // 同 bin 的下一个 chunk
malloc_chunk *bk; // 同 bin 的上一个 chunk
// ... (用户数据从这里开始)
};
prev_size 的特殊用法:
- 如果前一个 chunk 是空闲的:prev_size = 前一个 chunk 的大小
- 如果前一个 chunk 是已分配的:prev_size 可以存储用户数据(复用)
size 的低 3 bits 是标志位:
- PREV_INUSE (0x1):前一个 chunk 已被分配
- IS_MMAPPED (0x2):chunk 通过 mmap 分配
- NON_MAIN_ARENA (0x4):chunk 不属于 main arena
最小 chunk(32-bit):32 bytes
- prev_size:4 bytes
- size:4 bytes
- fd:4 bytes
- bk:4 bytes
- 合计 16 bytes... 但需要对齐到 8 字节,所以实际 32 bytesBin 的结构
Bin = 空闲 chunk 的链表
glibc 有多种 bin:
1. Fast bins(fastbin):
- 大小:16, 24, 32, 40, 48, 56, 64 bytes(共 7 个)
- 单向链表(fd only,无 bk)
- LIFO(后进先出)
- 不会被合并(保持大小)
- 分配 O(1),释放 O(1)
2. Small bins:
- 大小:32, 40, 48, ... ~ 1024 bytes(每 8 bytes 一个,共 62 个)
- 双向循环链表(fd + bk)
- 按大小排序
- 合并相邻空闲块
3. Large bins:
- 大小:>= 1024 bytes
- 双向循环链表
- 按大小排序(递减)
- 每个 bin 管理一个大小范围内的 chunk
4. Unsorted bin:
- 大小:任意(刚释放的块在这里)
- 双向循环链表
- 分配时会先检查 unsorted bin(可能返回小块)Bin 的数据结构:
struct malloc_state {
// fastbins
mfastbinptr fastbinsY[NFASTBINS]; // 7 个 fastbin 头
// smallbins + largebins
malloc_chunk *bins[NBINS * 2 - 2]; // 124 个 bin 头(双向链表)
// unsorted bin
malloc_chunk *bins[2]; // unsorted bin 头
};
bins[i] 是链表的头节点:
- bins[2*(i-NFASTBINS)] = fd(前向指针)
- bins[2*(i-NFASTBINS)+1] = bk(后向指针)分配和释放的流程
malloc(size) 分配流程:
1. 计算实际分配大小(size + overhead + 对齐)
2. 进入对应 size 的 fastbin 或 smallbin
3. 如果 bin 不空,取出第一个 chunk(O(1))
4. 如果 bin 空,检查 unsorted bin
5. 如果还空,合并 fastbin 到 smallbin
6. 如果仍然空,向 top chunk 要空间
7. top chunk 不够,调用 brk 扩展堆
示例:malloc(32)
32 -> 48 bytes(对齐到 8)
→ fastbin[5](32-byte chunk 的 fastbin)
→ 如果 fastbin[5] 非空,分配
---
free(ptr) 释放流程:
1. 检查 ptr 是否在 fastbin 范围(<= 64 bytes)
2. 如果是,插入 fastbin(O(1))
3. 如果不是,合并相邻的空闲 chunk
4. 放入 unsorted bin 或对应 size 的 bin
合并(consolidate):
- 检查下一个 chunk 是否空闲 → 合并
- 检查上一个 chunk 是否空闲(如果 PREV_INUSE=0)→ 合并
- 合并后放入 unsorted bin
注意:top chunk 不会和相邻 chunk 合并
- top chunk 是堆顶
- 只有 top chunk 可以 shrink(通过 brk)Top Chunk 和 Remainder
Top chunk 的特殊性:
top chunk = 堆顶的剩余空间
- 不属于任何 bin
- 是最后的后备空间
分配流程:
1. 所有 bin 都空
2. 向 top chunk 要空间
3. 如果 top chunk >= 需要的大小:
- 分配出去
- 剩余部分成为新的 top chunk
4. 如果 top chunk 不够:
- 调用 brk() 扩展堆
- 新的 top chunk = old top + new space
remainder(剩余):
假设:
top chunk = 1024 bytes
malloc(100) = 需要 128 bytes(含 overhead)
分配后:
分配出去:128 bytes
top chunk 剩余:1024 - 128 = 896 bytes
→ 896 bytes 成为新的 top chunk
如果 top chunk 太小(< 128 bytes):
- 调用 brk 扩展
- 不产生 remainder/proc/self/maps 查看堆结构
$ cat /proc/self/maps | grep heap
00...-00... rw-p [heap]
$ cat /proc/self/1/smaps | grep -A 20 "[heap]"
00...-00...
Rss: 132 kB
Pss: 132 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 12 kB
Private_Dirty: 120 kB
Ref 1
heap 区域是 brk 管理的区域。
$ cat /proc/self/smaps | grep -E "Anon|Heap"
00007f... rw-p [heap]
AnonHugePages: 0 kB
Anonymous: 8192 kB ← 堆使用的匿名页
对比 mmap 区域:
00007f... rw-p [anon]
Anonymous: 16384 kB ← mmap 匿名映射使用的页总结
- malloc_chunk:prev_size + size(低3位是标志)+ fd + bk + 用户数据
- Bin 类型:fastbin(7个,单向LIFO)/ smallbin(62个,双向)/ largebin(63个)/ unsorted(合并过渡)
- 分配流程:fastbin → smallbin → unsorted → top chunk → brk 扩展
- 释放流程:free → 检查合并 → 放入 fastbin 或 unsorted
- Top chunk:最后的后备,分配后产生 remainder,不会和相邻块合并
下篇预告(A03):为什么多线程程序中频繁 malloc/free 会变慢?arena 竞争和 per-thread arena 的设计。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论