A12 —— 内存系统的完整旅程:从物理到虚拟到应用
金句:理解内存的唯一方法,是走一遍数据从硬件到应用的完整旅程。
内存系统的五层全景
内存系统的分层结构:
[应用层] malloc / new / mmap
│
▼
[用户态分配器] ptmalloc / tcmalloc / jemalloc
│
▼
[内核页表] VMA / COW / page fault
│
▼
[内核物理管理] Buddy / SLAB / SLUB / Page Cache
│
▼
[硬件层] DRAM / Cache / MMU / CPU每层的作用:
硬件层(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:映射文件或分配大块一次内存分配的完整旅程
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(首次访问,含物理分配)数据写入后发生的事
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 nsfork 时的内存复制
fork() 的内存复制路径:
1. copy_page_range() 被调用
- 遍历父进程的每个 VMA
- 复制页表(PTE)
- 共享物理页面(引用计数 +1)
- 设置只读位
2. COW 设置:
- 父子共享同一物理页
- 页面标记为只读
- 写入触发 page fault
3. 写入时的 COW 处理:
- Page fault → 检查 PTE
- 发现只读 + 共享 → 分配新页
- 复制旧页内容
- 更新 PTE 为新页 + 可写
- 继续执行写入内存泄漏检测的完整链路
检测内存泄漏的工具对应关系:
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 系统中的内存分配
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
文件读取的 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不着急」,回复”资料”获取内存学习路线图。
评论