K12 —— 内存碎片化和 compaction:为什么大块内存分配失败
金句:碎片化是内存的慢性病——平时不疼,发作时可能要了命。
什么是内存碎片化
内存碎片化是指内存中大量的”小空洞”,无法满足大块内存的分配请求。
碎片化的两种类型:
1. 外部碎片(External Fragmentation):
- 空闲内存分散在多处
- 总空闲足够,但连续空间不够
- Buddy System 可以解决(合并相邻块)
- 但大块分配(order > 4)仍然可能失败
2. 内部碎片(Internal Fragmentation):
- 分配的内存比请求的大
- 例如:分配 100 bytes,实际分配 128 bytes(SLAB)
- 浪费的 28 bytes 是内部碎片
- 通过更精细的对象池减少
外部碎片的例子:
物理内存(256MB)分配后状态:
| 64MB | 32MB | 64MB | 16MB | 80MB |
已用 空闲
申请 128MB:
→ 总空闲 192MB(>128MB)
→ 但最大连续只有 80MB(不够)
→ 分配失败!
虽然有足够的总内存,但碎片化导致无法满足大块请求compaction(内存规整)
Linux 的 compaction 机制:
当大块分配(order >= 1)失败时,内核启动 compaction:
1. compaction 的原理:
- 扫描内存,把已分配的页面移动到一侧
- 把空闲页面收集到另一侧
- 最终产生连续的大块空闲区域
2. compaction 的触发:
- alloc_pages 失败(order >= 1)
- watermark 检查失败
- 调用 compact_zone(order)
3. compaction 的过程:
- sync:暂停其他操作,扫描 zone
- isolate:把可移动的页面隔离出来
- migrate:把页面移动到新位置
- 更新页表(remap)
4. compaction 的代价:
- 页面迁移需要复制内容
- 需要刷新 TLB
- 涉及大量锁操作
- 暂停时间可能很长compaction 的关键函数:
compact_zone(order):
→ compact_zone_order(order)
→ isolate_migratepages()
→ 扫描 LRU,找可移动的页面
→ 可移动 = anonymous(swap)或 file-backed(可以写回)
→ 脏文件页写回后才能移动
migrate_pages():
→ 对每个页面:
→ 分配新页面
→ 复制内容
→ 更新所有进程的页表(remap)
→ 释放旧页面
失败原因:
- 有不能移动的页面(locked、hwpoisoned)
- 页面正在 I/O(写回中)
- 锁竞争导致迁移超时内存碎片化的实际影响
大块内存分配失败的场景:
1. 大页(Huge Page)分配:
- 申请 2MB(或 1GB)大页
- compaction 失败
- /proc/sys/vm/nr_hugepages 显示分配失败
$ echo 16 > /proc/sys/vm/nr_hugepages
$ cat /proc/sys/vm/nr_hugepages
0 // 失败!
2. vmalloc 分配(不连续物理页):
- vmalloc 需要连续的虚拟地址
- 但不要求物理连续
- compaction 只影响物理连续性
3. Direct I/O:
- 需要物理连续的 buffer
- fragmentation 严重时可能失败
碎片化的时间线:
- 程序运行初期:碎片少
- 运行几小时后:开始碎片化
- 运行几天后:碎片严重,大块分配失败
- 服务器重启才能解决(长期运行的问题)缓解碎片化的策略
1. 预留大块内存:
$ echo 1024 > /proc/sys/vm/nr_hugepages
# 启动时预留 2GB 大页
GRUB_CMDLINE_LINUX="hugepages=1024"
大页的优点:
- 减少 TLB miss
- 避免碎片化问题
- 分配时不需要 compaction
2. 降低 swappiness(避免 anonymous 碎片化):
$ sysctl vm.swappiness=10
理由:
- file-backed 可以被回收(干净页)
- anonymous 需要 swapout(保留)
- 低 swappiness 减少 anonymous 占用
3. 使用内存池(memory pool):
- 预先分配大块内存
- 程序从池中分配
- 避免运行时碎片化
4. 定期重载(restart):
- 服务器定期重启
- 内存碎片化是长期问题
- 重启是终极解决方案
5. compaction 手动触发:
$ echo 1 > /proc/sys/vm/compact_memory
当内存碎片化时,手动触发 compaction
(适合批量任务之前)总结
- 外部碎片:总空闲够但连续性不够,buddy 合并不了碎片(跨 order)
- 内部碎片:分配 > 请求,SLAB 的固定大小导致
- compaction:页面迁移,把空闲页收集到一侧,需要复制+TLB 刷新
- 大块分配失败:order >= 1 的分配失败,huge page 分配失败
- 缓解:预留 hugepages / 低 swappiness / 定期 compaction / 定期重启
K 层总结:K01~K12 覆盖了 SLAB 分配器 → VMA/地址空间 → COW/fork → OOM Killer → Page Cache → mmap/demand paging → malloc/ptmalloc → shm 共享内存 → 泄漏 → NUMA → alloc_pages 路径 → 碎片化/compaction 的完整内核层链路。
下篇预告(A01):用户空间怎么和内核协作分配内存的?malloc 是怎么调用 brk/mmap 的,以及 glibc 如何管理堆。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论