A02 —— 堆的内部结构:arena、bin 和 chunk

金句:malloc 管理的堆,是一个由 bin 和 chunk 组成的精密系统。


malloc_chunk 的结构

Bash
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 bytes

Bin 的结构

Bash
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(可能返回小块)

Bash
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(后向指针)

分配和释放的流程

Bash
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

Bash
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 查看堆结构

Bash
$ 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不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年8月10日

作者

评论

发表评论

您的邮箱地址不会被公开。