A12 —— 内存系统的完整旅程:从物理到虚拟到应用

金句:理解内存的唯一方法,是走一遍数据从硬件到应用的完整旅程。


内存系统的五层全景

Bash
内存系统的分层结构:

  [应用层]          malloc / new / mmap
      │
      ▼
  [用户态分配器]    ptmalloc / tcmalloc / jemalloc
      │
      ▼
  [内核页表]        VMA / COW / page fault
      │
      ▼
  [内核物理管理]    Buddy / SLAB / SLUB / Page Cache
      │
      ▼
  [硬件层]          DRAM / Cache / MMU / CPU

Bash
每层的作用:

硬件层(S/H):
  - DRAM:实际的存储介质
  - Cache(L1/L2/L3):减少 DRAM 访问延迟
  - MMU:VA → PA 翻译
  - 地址总线:CPU 到 DRAM 的通信

内核物理管理(K):
  - Buddy System:分配 4KB+ 的物理页
  - SLAB/SLUB:把页切成固定大小的对象
  - Page Cache:缓存文件数据
  - kswapd:回收页面(LRU)

内核页表(B):
  - VMA:进程虚拟地址空间的管理
  - COW:fork 时的延迟复制
  - page fault:按需分配物理页
  - mmap:建立文件或匿名映射

用户态分配器(A):
  - ptmalloc:glibc 的 malloc(arena + bin)
  - tcmalloc:Google 的高效分配器
  - jemalloc:Facebook 的高效分配器

应用层:
  - malloc / new:申请内存
  - free / delete:释放内存
  - mmap:映射文件或分配大块

一次内存分配的完整旅程

Bash
malloc(256) 的完整路径:

1. ptmalloc 接收请求
   - 大小 256 → 找对应 size class(可能是 256)
   - 检查 thread cache(tcache)
   - tcache 有 → 直接返回(无锁,O(1))

2. tcache 空 → 从 arena 的 bin 分配
   - 检查 smallbin 或 largebin
   - bin 有 → 取出 chunk(需要锁)
   - bin 空 → 从 top chunk 取

3. top chunk 不够 → 调用 brk()
   - brk() 系统调用
   - 内核分配新的虚拟地址空间(vma)
   - 物理页在第一次访问时分配(demand paging)
   - 更新 top chunk

4. 物理页分配(page fault):
   - MMU 发现 PTE 无效
   - OS 分配物理页(alloc_pages → buddy)
   - 建立页表映射
   - 填充页面(清零或读文件)
   - 返回,继续执行

5. 返回虚拟地址给用户程序

总耗时:
  - tcache 命中:~10 ns(用户态)
  - bin 分配:~50 ns(含锁)
  - brk:~500 ns(含系统调用)
  - page fault:~10000 ns(首次访问,含物理分配)

数据写入后发生的事

Bash
memcpy(buf, data, 256) 的过程:

1. 用户态 memcpy
   - 把 data 复制到 buf(CPU copy)

2. 如果 buf 之前没访问过(第一次 touch):
   - page fault
   - OS 分配物理页(alloc_pages → buddy)
   - 建立页表映射

3. 如果多个进程映射同一文件(MAP_SHARED):
   - 修改直接反映到 Page Cache
   - 其他进程立即看到

4. 后续访问:
   - MMU 查页表 → PA
   - L1 Cache hit → ~1 ns
   - L1 Cache miss → L2 hit → ~10 ns
   - L2 miss → L3 hit → ~30 ns
   - L3 miss → DRAM → ~60 ns

fork 时的内存复制

Bash
fork() 的内存复制路径:

1. copy_page_range() 被调用
   - 遍历父进程的每个 VMA
   - 复制页表(PTE)
   - 共享物理页面(引用计数 +1)
   - 设置只读位

2. COW 设置:
   - 父子共享同一物理页
   - 页面标记为只读
   - 写入触发 page fault

3. 写入时的 COW 处理:
   - Page fault → 检查 PTE
   - 发现只读 + 共享 → 分配新页
   - 复制旧页内容
   - 更新 PTE 为新页 + 可写
   - 继续执行写入

内存泄漏检测的完整链路

Bash
检测内存泄漏的工具对应关系:

1. /proc/status(运行时监控):
   - RSS 持续增长 → 可能有泄漏
   - VmPeak 持续增长 → 确认泄漏

2. mtrace(glibc 内置):
   - 记录所有 malloc/free
   - 找出未配对的分配

3. valgrind(全面检测):
   - 跟踪所有 malloc/free/realloc
   - 报告泄漏的堆块
   - 报告 use-after-free

4. ASan(运行时检测):
   - 编译时插桩
   - 检测越界访问
   - 检测 UAF
   - 比 valgrind 快 2~5x

5. massif(堆分析):
   - 跟踪堆的使用
   - 找出峰值和趋势
   - 火焰图可视化

NUMA 系统中的内存分配

Bash
NUMA 系统中 malloc 的路径:

1. glibc 分配请求
   - 线程缓存 → arena

2. arena 分配物理页:
   - first touch:在线程当前运行的节点分配
   - mbind:可以绑定到特定节点

3. alloc_pages_node(node, ...):
   - 在指定节点从 buddy 分配
   - 如果节点内存不足,回退到其他节点

4. 页面迁移(numa_balancing):
   - 如果页面和访问线程不在同一节点
   - kernel 自动迁移页面
   - 减少 NUMA 延迟

numa 感知的分配:
  #include <numa.h>
  numa_bind(&numa_all_nodes);
  // 后续分配在绑定的节点

Page Cache 和文件 I/O

Bash
文件读取的 Page Cache 路径:

1. read(fd, buf, 4096)
   - 查找 Page Cache(radix tree)

2. Cache miss:
   - 从 buddy 分配物理页
   - 从磁盘读取到 Page Cache
   - 复制到用户 Buffer

3. Cache hit:
   - 直接从 Page Cache 复制
   - 无磁盘 I/O

文件写入的 Page Cache 路径:

1. write(fd, buf, 4096)
   - 写入 Page Cache(标记为 dirty)
   - 立即返回(写回异步)

2. 定期 flush(pdflush):
   - 把 dirty 页面写回磁盘
   - 更新 Page Cache 状态

总结

  • 五层结构:硬件(S/H)→ 内核物理(B/K)→ 内核页表(B)→ 用户态分配器(A)→ 应用
  • malloc 路径:tcache → bin → top chunk → brk → page fault → buddy
  • 数据写入:用户 memcpy → COW → PTE 更新 → Cache 命中或 DRAM 访问
  • NUMA:first touch → 节点分配 → 页面迁移(numa_balancing)
  • Page Cache:radix tree 索引 → LRU 管理 → 定期 flush
  • 泄漏检测:/proc/status → mtrace → valgrind → ASan → massif

内存专题总结:从 S 层的 DRAM Cell 和 DDR5,到 H 层的 DIMM 和双通道,到 B 层的 boot/mmu/buddy,到 K 层的 SLAB/kswapd/PageCache,到 A 层的 malloc/brk 和泄漏检测——55 篇内容覆盖了内存的完整生命周期。


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年1月12日

作者

评论

发表评论

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