子系列 D 开篇。虚拟内存(VM)是一套给进程假象的机制:每个进程以为自己独占整块大地址空间,实际由硬件 MMU + 内核页表管理把 VA 映射到 PA,或根本没映射。这套抽象支撑了隔离、保护、swap、mmap、COW、共享库。本文讲它的整体模型。
一、先看全景
flowchart LR
CODE[进程代码<br/>访问 VA] --> MMU[MMU/TLB]
MMU -->|命中| PA[物理地址]
MMU -->|miss| WALK[page walk<br/>读页表]
WALK --> PT[多级页表 CR3/TTBR]
PT -->|present| PA
PT -->|!present| PF[缺页 #PF]
PF --> KMM[内核 mm<br/>handle_mm_fault]
KMM -->|anon/file/swap/cow| FILLPT[填 PTE]
FILLPT --> MMU
classDef sw fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef hw fill:#3fb95022,stroke:#3fb950,color:#adbac7;
classDef err fill:#f0883e22,stroke:#f0883e,color:#adbac7;
class CODE,KMM,FILLPT sw
class MMU,PT,PA hw
class PF err
二、VM 给了什么
- 进程隔离:A 进程看不见 B 进程的内存
- 保护:内核空间、只读段、NX 段受 MMU 强制
- 大地址空间:64 位下有 128TB+ 用户空间,远大于物理 RAM
- mmap 语义:文件映射、匿名映射、共享/私有
- COW:fork 不复制物理页,写时再分离
- Demand paging:不访问的地址根本不分物理页
- Swap:物理压力大时把冷页换到磁盘
三、代价
- 硬件成本:MMU、TLB、多级页表;每次访问要翻译
- 软件成本:缺页处理、TLB shootdown、页表维护
- 内存碎片:虚拟连续不代表物理连续,大页需要 compaction
- 安全副作用:Meltdown/Spectre 利用地址翻译的微架构侧信道
四、地址空间布局(x86_64 Linux)
0x0000_0000_0000_0000 ┌─────────────────┐
│ NULL guard │
├─────────────────┤
│ text (.text) │ 可执行代码
├─────────────────┤
│ data/bss │ 全局/静态
├─────────────────┤
│ heap (brk) │ 向上生长
│ ↓ │
│ │
│ mmap 区 │ 向下生长(glibc)
│ ↑ │
│ │
├─────────────────┤
│ stack │ 向下生长
0x0000_7fff_ffff_ffff ├─────────────────┤
│ 非规范空洞 │ 47 位有效
0xffff_8000_0000_0000 ├─────────────────┤
│ 内核空间 │
0xffff_ffff_ffff_ffff └─────────────────┘
5 级分页(LA57)扩展到 57 位,上限到 128PB,但默认不开启。
查看实际布局:cat /proc/$$/maps。
五、mmap 的四个象限
shared (MAP_SHARED) private (MAP_PRIVATE)
file-backed (fd) ┌──────────────────────┬──────────────────────┐
│ 对文件可见的共享内存 │ COW 私有映射(如 .so)│
│ 如 DB data file │ 读共享,写分家 │
├──────────────────────┼──────────────────────┤
anonymous │ POSIX shm / │ 普通 malloc 大块 │
(MAP_ANONYMOUS) │ 父子 fork 后仍共享 │ 堆扩展 │
└──────────────────────┴──────────────────────┘
关键
flag:MAP_POPULATE(提前分配)、MAP_LOCKED(mlock)、MAP_HUGETLB、MAP_FIXED_NOREPLACE。
六、page 的状态机
一个物理页可能处于:
- free:buddy 空闲列表里
- anon:匿名映射(malloc/stack/heap)
- file:page cache 里的文件页
- slab:内核对象分配器
- reserved:固件/硬件保留
- PageDirty:需回写
- PageWriteback:正在 I/O
/proc/meminfo
给宏观分类;page_owner、page-types
给细粒度。
七、overcommit
Linux 允许”超额承诺”:malloc 拿到 VA 不立刻分物理页。策略:
vm.overcommit_memory
= 0 启发式(默认):大分配拒绝,小的允许
= 1 从不拒绝(所有 malloc 都成功)
= 2 严格:按 CommitLimit = swap + RAM*ratio
vm.overcommit_ratio 默认 50
争议:
- 0 = 启发式:不可预测
- 1:OOM Killer 来救场,容器推荐
- 2:不会 OOM,但 malloc 会失败;金融/HFT 选这个
八、COW:fork 的灵魂
fork() 复制 mm_struct + VMA,但不复制物理页——所有页标只读,任一方写触发 #PF → 分一份私有页,把写的这页 COW 出来。
parent: [VA=0x1000] -----> PA=X (RO)
child: [VA=0x1000] -----> PA=X (RO)
// child 写 → #PF → 分配 PA=Y,copy X→Y,child PTE 改成 PA=Y (RW)fork+exec 热路径里 99% 的页根本不会被写(马上 exec 就释放了),COW 把 fork 从”O(工作集大小)“降到”O(VMA 个数)“。
九、保护与特权
PTE 里的控制位:
- R/W:可写
- U/S:用户/内核
- NX/XD:不可执行
- PKU(x86)、MTE(ARM):内存着色
- G(global):跨进程保留 TLB
内核靠这些 bit 实现:
- 用户态不能读内核页(Meltdown 前提下的缓解:KPTI)
- 只读段真只读(崩溃而不是静默损坏)
- NX bit 阻挡栈上执行 shellcode
- pkey 可做 per-thread 内存隔离(glibc 的 memprotect_key)
十、mmap 工程要点
- 大文件逐段处理:mmap + madvise(SEQUENTIAL) 比 read 省上下文切换
- MAP_POPULATE 避免第一次访问大批 #PF 抖动(但增加启动时间)
- madvise(DONTNEED) 把脏匿名页直接丢(注意匿名页丢了内容就没了)
- mlock 防换出,要求 CAP_IPC_LOCK 或 RLIMIT_MEMLOCK
- shared mmap + msync 的持久化语义不强,DB 不能靠 msync 当 fsync 用
十一、观察与诊断
cat /proc/$$/maps # VMA 列表
cat /proc/$$/smaps # 每 VMA 的 RSS/PSS/Swap/...
cat /proc/$$/status | grep Vm # VmPeak/VmRSS/VmSwap
pmap -x $$ # 友好版
vmtouch /path/file # 看文件缓存状态观察 overcommit:
grep -E "Commit(Limit|Allocated|ed_AS)" /proc/meminfo
十二、小结
- VM = 假象 + 保护 + 按需分配
- mmap 四象限覆盖绝大多数用法
- overcommit 是 Linux 特色,选哪档看业务对 OOM 的容忍度
- COW 是 fork/exec 能工作的前提
- 一切从 PTE bit 和 mm_struct 开始;后续子系列 D 的每一篇都是在放大其中一角
参考文献
- Gorman, M. “Understanding the Linux Virtual Memory Manager.” (第 3-7 章基础仍有效)
- Bovet & Cesati, “Understanding the Linux Kernel” 3rd §2/8/9
Documentation/admin-guide/mm/、Documentation/vm/- Corbet, J. “The (updated) anatomy of address spaces.” LWN.net 2021
工具
/proc/$$/maps、/proc/$$/smapspmap、vmtouch、numastatperf mem、page-typesbpftrace -e 'tracepoint:exceptions:page_fault_user { @[comm]=count(); }'
上一篇:cpufreq governors:频率调节 下一篇:x86_64 多级页表
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】缺页处理
缺页(page fault)是 VM 的核心事件——PTE 不 present 就触发 #PF。本文讲 major/minor/COW/swap/userfault 五种缺页控制流、do_page_fault 调用链、hugefault、userfaultfd 在 CRIU/Live Migration 的角色。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。