K12 —— 内存碎片化和 compaction:为什么大块内存分配失败

金句:碎片化是内存的慢性病——平时不疼,发作时可能要了命。


什么是内存碎片化

内存碎片化是指内存中大量的”小空洞”,无法满足大块内存的分配请求。

Bash
碎片化的两种类型:

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(内存规整)

Bash
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
   - 涉及大量锁操作
   - 暂停时间可能很长

Bash
compaction 的关键函数:

compact_zone(order):
  → compact_zone_order(order)
      → isolate_migratepages()
          → 扫描 LRU,找可移动的页面
          → 可移动 = anonymous(swap)或 file-backed(可以写回)
          → 脏文件页写回后才能移动

migrate_pages():
  → 对每个页面:
      → 分配新页面
      → 复制内容
      → 更新所有进程的页表(remap)
      → 释放旧页面

失败原因:
  - 有不能移动的页面(locked、hwpoisoned)
  - 页面正在 I/O(写回中)
  - 锁竞争导致迁移超时

内存碎片化的实际影响

Bash
大块内存分配失败的场景:

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 严重时可能失败

碎片化的时间线:
  - 程序运行初期:碎片少
  - 运行几小时后:开始碎片化
  - 运行几天后:碎片严重,大块分配失败
  - 服务器重启才能解决(长期运行的问题)

缓解碎片化的策略

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

最后修改: 2024年8月24日

作者

评论

发表评论

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