Theory · OS · Virtual Memory

虚拟内存与分页

每个进程的 128TB 假象:地址翻译 · 多级页表 · TLB · 缺页 —— OS 最精巧的一层间接

三层价值

隔离(进程间互不可见)、扩展(地址空间可大于物理内存)、灵活(按需加载 / 共享 / 写时复制)

翻译管线

VA → MMU 查页表(TLB 加速)→ PA;多级页表按需分配,4KB 页 + 9×4 位索引是 x86-64 的标准答案

缺页即机制

惰性分配、COW、mmap 文件、swap——四种"故意缺页"撑起了现代内存管理的全部弹性

定位:OS 系列第七篇,内存三部曲之首。主线:"缺页不是异常而是机制"——COW/fork、demand paging、mmap、swap 全部复用同一条缺页通路。

Why Virtual Memory

虚拟内存的三层价值:隔离、扩展、灵活

① 隔离(安全与稳定)

每个进程有独立虚拟地址空间:A 进程的 0x400000 和 B 进程的 0x400000 指向不同物理页。没有页表校验,进程之间互相踩内存、内核也无处藏身——多进程模型的前提。

② 扩展(地址空间 > 物理内存)

程序可用地址空间不再受物理内存限制:暂时不用的页换到磁盘(swap),需要时再调入——用磁盘扩展 RAM,配合局部性原理,"小内存跑大程序"成立。

③ 灵活(机制复用)

同一套页表机制支撑:按需加载(demand paging,不碰不装)、写时复制(fork 秒级)、共享库映射(多个进程映射同一份 libc 物理页)、mmap 文件 I/O、内存整理——全是"页映射关系"的花样。

对比物理寻址(无 VM)虚拟寻址(有 VM)
地址含义直接是内存芯片位置逻辑空间,经页表翻译
进程隔离无,互相踩踏天然隔离
内存超卖不可能swap + 按需加载
重定位编译时定死(换机器失效)链接到固定 VA 即可
面试金句:"虚拟内存是进程视角的一层谎言:你以为自己在连续私有的大内存里,实际是 MMU 每次访存都在替你查表——谎言换来隔离与弹性。"
数字锚点:x86-64 进程可寻址 128TB 用户空间(4 级页表 48 位地址),远超物理内存——这个"富余"就是扩展与惰性分配的舞台。
三层价值按"安全→容量→机制复用"讲。扩展价值要配局部性原理(下一册页面置换的主角)。重定位价值常被忽略:同一份二进制在各进程同一 VA 运行。

VA → PA Translation Pipeline

一次访存的真实旅程:MMU · 页表 · TLB

虚拟地址到物理地址的翻译管线 翻译管线图:CPU 发出虚拟地址,虚拟地址分为页号和页内偏移两部分;MMU 先查 TLB 缓存,命中则直接得到物理页号,拼接偏移形成物理地址;未命中则走页表逐级查询;若页表项标记不在内存则触发缺页异常,由内核调入页面或分配物理页。 CPU 执行 load/store 虚拟地址 0x00007f3a…5678 = 页号(47:12) + 偏移(11:0) TLB 页表项的硬件缓存 命中:1 个时钟级访问 MMU 页表遍历 PGD→PUD→PMD→PTE 4 次内存访问(miss 时) PTE 标记不在内存? → 缺页异常 → 内核接管 调入页 / COW / 惰性分配 物理内存 物理页 frame PA = PFN + 偏移 miss hit:直接得 PFN(跨过页表) PTE present=0 walk 完成 性能账本:TLB 命中 → 1 cycle 级 · TLB miss → 硬件页表遍历 +4 次访存 · major 缺页 → 磁盘 I/O(万倍级惩罚) 所以:局部性(时间/空间)决定了 TLB 与 cache 的命中率,进而决定了程序的真实访存速度 —— "分页把访存变慢了,局部性又把它救回来"
管线图是本篇的心脏:VA 拆页号/偏移 → TLB → 页表遍历 → PA;present=0 走缺页。性能三档(命中/遍历/major fault)差距悬殊,是后面 TLB、大页、页面置换三页的动机。

Paging · Multi-level Page Table

多级页表:用"按需分配"驯服页表大小

单级页表为什么不可行

x86-64 虚拟地址 48 位、4KB 页:每个进程要 2^36 个页表项 × 8B = 512GB 页表——比大多数机器内存还大。而进程实际只碰地址空间的一小角,98% 的页表项是浪费。

多级页表 = 稀疏树

把页号切成 4 段 9 位做四级索引(PGD→PUD→PMD→PTE,最后 12 位是页内偏移):顶级页表常驻,下级节点用到才分配。稀疏地址空间只挂几个节点,页表开销从 512GB 降到 KB 级。

页表项(PTE)里有什么

物理页框号 + 控制位:present(在不在内存)、RW(可写?)、US(用户态可访问?)、A(访问过,置换算法用)、D(脏,写回用)——后面缺页、置换、COW 全靠这些位

5 级页表

地址不够用了:Linux 4.17 起 x86-64 支持 5 级页表(57 位虚拟地址,共 128 PiB,用户/内核各 64 PiB),有 la57 硬件才启用,默认仍 4 级。

// x86-64 4 级页表的地址切分(48 位 VA)
// [47:39] PGD  ← 9 位:顶级索引
// [38:30] PUD  ← 9 位
// [29:21] PMD  ← 9 位
// [20:12] PTE  ← 9 位
// [11:0 ] offset ← 12 位 = 4KB 页内偏移

// 每级页表 4KB = 512 × 8B 项
// 2^9 = 512 → 每段正好 9 位
空间换时间的反例:多级页表省空间但费时间——4 次内存访问才能拿一个 PA。TLB 正是为弥补这个代价而生的缓存;没有 TLB,分页根本跑不快。
面试金句:"多级页表的本质是为稀疏而生的树:用'下级节点按需分配'把页表大小从地址空间规模(512GB)压到实际使用规模(KB 级),代价是遍历变慢——由 TLB 买单。"
必背数字链:48 位 VA = 4×9 + 12;页表 4KB=512 项;单级要 512GB、多级 KB 级。PTE 的 A/D/present 位为后续缺页与置换埋伏笔。5 级页表注明 4.17+/la57 硬件条件。

TLB · PCID · Huge Pages

TLB 与大页:给翻译管线提速

TLB 是什么

MMU 内部的页表项缓存(VA→PA 的映射快表):小而快(L1 各几十项、L2 上千项)。命中省掉整个页表遍历——没有 TLB,每次访存都要多付 4 次内存访问。

为什么依赖局部性

TLB 只能缓存少量映射,靠程序的时间/空间局部性维持高命中率:顺序访问数组轻松 ~100%;指针追逐(链表、随机跳转)频繁 miss——这就是"缓存友好代码"的 TLB 侧解释。

上下文切换与 TLB

换页表后 TLB 里的旧映射全部作废——进程切换贵就贵在这。缓解:PCID/ASID 给 TLB 项打上进程标签,切换不刷(现代 x86 默认开启);内核单独一套全局映射(global bit)。

大页 Huge Page

页从 4KB 变 2MB/1GB:同样 TLB 项数覆盖地址范围扩大 512/262144 倍,miss 骤降。数据库/大内存服务(Redis、MySQL Buffer Pool)常开 THP 或 hugetlbfs。代价:分配需连续物理块、易碎片化、COW 粒度变粗(fork 后写放大 512 倍)——THP 对 Redis 的内存膨胀恶名正是后者。

手段原理代价
TLB(硬件)缓存映射,命中免遍历容量小,靠局部性
PCID/ASIDTLB 项带进程标签,切换不刷几乎免费,必开
2MB 大页 (THP)一项覆盖 512 倍范围碎片 + COW 放大
1GB 大页 (hugetlbfs)数据库/大页专用预留需预留,管理僵硬
prefetch / 布局优化提升局部性补命中率改代码/数据结构
实战连接(Redis):Redis 官方建议 THP=never——fork + COW 时 2MB 大页让"写一个字节复制 2MB"的放大效应制造内存膨胀与延迟尖刺(Redis 持久化 deck 联动)。
面试金句:"TLB 解决'翻译贵',大页解决'TLB 小',PCID 解决'切换刷 TLB'——三招各堵一个洞,洞的源头都是多级页表的遍历成本。"
TLB 页三件事:是什么(映射缓存)、靠什么(局部性)、切换怎么办(PCID)。大页的双面性(TLB 优化 vs COW 放大)配 Redis THP=never 实战背书。

Segmentation vs Paging

分段 vs 分页:历史的两种切法

维度分段分页
切分单位逻辑单元(代码段/数据段/栈),大小可变固定大小(4KB 页)
对程序员可见、有语义(段 = 模块)透明,纯实现细节
碎片类型外部碎片(段间空隙难利用)内部碎片(最后一页填不满)
地址形式段号 + 段内偏移(查段表得基址+限长)页号 + 页内偏移(查页表得框号)
共享/保护按逻辑段共享,粒度自然按页共享(也是现代共享库的方式)
现状基本退场(x86-64 段基址强制 0)绝对主流
为什么分页赢了:外部碎片无解(段长可变 → 物理内存被切成乱七八糟的块);固定页大小让任意页可放任意框,管理简单。代价只是每页尾部一点内部碎片(平均半页),完全可接受。

段页式(历史折中)

先分段(逻辑保护/共享),段内再分页(消外部碎片):VA → 段表 → 线性地址 → 页表 → PA。x86 32 位时代真实用过;64 位时代段被架空,只留 FS/GS 做线程本地存储(TLS)。

x86-64 的段残留

段基址全部归 0,分段退化成"合法性检查":FS 段基址指向 TLS/线程数据是唯一常见用途(Go runtime 用它定位 g 结构)。面试答"现代 OS = 纯分页 + 段仅剩 TLS 残留"即可。

碎片对偶(易考)

分段 → 外部碎片(空闲块总量够、不连续没法用);分页 → 内部碎片(页内浪费,平均 ≤ 半页)。伙伴系统/Slab 是内核对物理内存分配的碎片治理(下一篇展开)。

对比表五维度 + "分页为什么赢"(外部碎片无解)。x86-64 段退化到 TLS 是高级细节(Go 的 g 结构挂在 FS 基址上——与 GMP deck 呼应)。

Page Fault · The Universal Mechanism

缺页:不是故障,是内存管理的总线机制

缺页异常的分类与处理流程 流程图:CPU 访问的页不在内存或权限不符时触发缺页异常,内核先查 VMA 判断访问是否合法;非法访问发 SIGSEGV;合法按类型分流:文件映射页从 Page Cache 调入为次缺页需读盘为主缺页,匿名页首次访问分配清零页,写只读页触发写时复制,无空闲内存时先回收或换出其他页。 访问触发缺页(fault) PTE 不在内存 / 写只读页 / 访问越权 内核查 VMA:这次访问合法吗? 地址在哪个映射里、权限对不对 非法 → SIGSEGV 野指针 / 栈溢出穿 guard page → 进程崩溃 非法 合法↓ 文件页调入(minor/major) mmap 的文件页不在内存: Page Cache 已有 → minor fault 需要读盘 → major fault 共享库加载就是这条路 匿名页惰性分配 malloc 返回的只是地址区间: 首次写入才分配物理页 读首次 → 映射共享零页 demand paging:不碰不装 写时复制 COW fork 后父子共享只读页: 写触发 fault → 复制该页 改 PTE 为私有可写 fork 快的秘密(第 4 篇) 换页 swap in 页被换到磁盘: 读回 + 可能先驱逐别的页 (major fault,昂贵) 内存不足时的常态 同一个异常入口,四种出口 —— 这就是"缺页是机制不是故障"的含义 观测:ps -o min_flt,maj_flt · sar -B · minor 应远多于 major(major 高 = 内存吃紧或读盘放大)· major fault 的代价是磁盘 I/O 量级
核心观点页:一个异常入口(查 VMA 判合法性)四种出口(文件调入/惰性分配/COW/swap in)。minor vs major 的区别 = 要不要读盘;SIGSEGV 是"非法"分支。

Reclaim · Swappiness · OOM

内存吃紧时:回收、换出与 OOM

回收的两类对象

文件页(读过的文件缓存):干净页直接丢弃(有文件兜底)、脏页先写回再回收;匿名页(堆/栈):没有文件兜底,必须写到 swap(或 zram 压缩内存盘)才能释放。回收顺序与代价不同,这就是 swappiness 的意义:0–100 调节倾向(默认 60,越低越不愿意 swap 匿名页;设 0 只是"极不愿意",不是禁用)。

kswapd 与水位

每 zone 三条水位线(min/low/high):free 低于 low 唤醒 kswapd 后台回收直到 high;低于 min 则触发直接回收(进程自己边分配边回收,延迟飙升——内存毛刺的来源)。

换页算法的角色

回收谁?用近似 LRU(active/inactive 链表)挑"最久没用"的页——细节是下一篇页面置换 deck 的主角。

最后的兜底:OOM killer

回收+swap 都救不回来时,内核按 oom_score(占内存多 × 加权,可用 oom_score_adj 干预)挑一个进程杀掉,释放内存保系统。Go 服务被 OOM 杀不会触发 recover——是信号级终止。

top 指标含义常见误读
VIRT虚拟地址空间总大小(含保留未用)大 ≠ 内存问题(Go 进程天然大)
RES实际驻留物理内存判断真实占用的主指标
SHRRES 中可共享部分(共享库等)多进程共享库会重复计数到各自 RES
Swap已换出的匿名页非 0 未必坏(长驻冷数据)
overcommit(Linux 特性):malloc 大内存常"成功"——内核允许超卖(heuristic/always/never 三模式,默认 heuristic)。真正分配发生在首次触碰(缺页),这就是"malloc 4TB 不报错、一写才 OOM"的原因。
面试金句:"Linux 的内存紧张三段式:回收文件页 → 换出匿名页 → OOM 兜底;每一步的代价差一个数量级,观测先看 si/so 与 majflt。"
回收流水线:文件页(干净弃/脏写回)→ 匿名页(swap/zram)→ OOM。kswapd 水位线机制解释"内存毛刺"。overcommit 与 VIRT/RES 表是运维高频。

Linux Internals & Tooling

Linux 落地细节:VMA、工具与常见现象

VMA:内核怎么"记账"

页表太细,内核用 VMA(虚拟内存区域)管理地址空间布局:代码段、数据段、堆、mmap 区、栈各是一个 VMA(连续地址 + 统一权限 + 后备对象)。缺页时先查 VMA 定合法性,再下探页表。/proc/<pid>/maps 列出全部 VMA。

常用观测命令

vmstat 1:si/so(swap 进出)与 cs;sar -B:majflt/s 缺页速率;ps -o min_flt,maj_flt -p PID:进程累计缺页;cat /proc/pid/smaps_rollup:Rss/Pss/Anonymous 细分(Pss 把共享页按比例分摊);free -h:buff/cache 与 available。

两个经典现象

free 显示内存耗尽但系统不卡:buff/cache 是"借给缓存的免费内存",看 available 才准;② 程序启动后 RES 缓慢爬升:demand paging 逐页触发 minor fault——正常惰性分配,不是泄漏。

# 进程地址空间布局(VMA 一览)
$ cat /proc/self/maps | head -6
55... r--p app        # 代码段(只读)
55... rw-p app        # 数据段
7f... r--p libc.so    # 共享库(文件映射)
7f... rw-p [anon]     # mmap 匿名区/堆扩展
7ff... rw-p [stack]   # 栈(向下生长)

# 看真实内存压力
$ vmstat 1 | awk '{print $7,$8}'  # si so
$ sar -B 1                        # majflt/s
$ cat /proc/meminfo | egrep 'Swap|Anon'
布局预告:maps 输出的从低到高 = 下一篇 内存布局 deck 的目录(text → data → heap → mmap → stack),那里画完整地址空间图并展开 malloc。
面试提示:VIRT 大 RES 小是"地址空间保留 ≠ 实际占用";shrink 堆用 mallopt/M_TRIM 或换 tcmalloc——能与下一篇联动答。
落地页:VMA 是页表之上的记账层(缺页合法性的第一道判断)。命令面板(vmstat/sar/smaps_rollup)+ 两个经典现象。maps 输出直接预告下一篇。

Go × Virtual Memory

Go 与虚拟内存:为什么 Go 进程 VIRT 这么大

现象

刚启动的 Go 服务 VIRT 就有几个 GB,RES 却只有几十 MB——监控常误报。原因:Go runtime 启动时按 arena 预留大片虚拟地址(mmap PROT_NONE 保留),只是地址空间预约,不占物理内存。

runtime 如何消费虚拟内存机制

预留 + 按需提交:arena 64MB 一块,用前 mprotect 变可读写——教科书 demand paging 的手动版;② goroutine 栈:匿名内存,2KB 起步、增长靠复制,溢出前有 guard page 兜底;③ 归还:GC 后用 madvise(MADV_FREE/DONTNEED) 把冷页还给 OS——RES 缓慢下降的来源。

两个运维要点

① 监控看 RES/PSS 而不是 VIRT;② GODEBUG=madvdontneed=1(或新版默认行为变化)影响归还语义——容器里 RES 回收观感差异的经典来源;环境内存受限用 GOMEMLIMIT(1.19+)让 GC 主动向 OS 少要内存。

Go 行为虚拟内存机制
arena 预留大 VIRTmmap PROT_NONE 保留地址空间
堆按需增长mprotect 提交 + 缺页填物理页
goroutine 2KB 栈匿名页 + stackguard0 软件检查 + 栈复制扩容
GC 后 RES 回落madvise 归还冷页
GOMEMLIMIT软内存上限,控制向 OS 的索取
面试金句:"Go 进程 VIRT 巨大不是泄漏,是 runtime 把虚拟内存当'廉价资源'用足了:地址随便预约、物理页按需领取、冷页主动归还——三层机制各就各位。"
深挖链接:arena/mcentral/mcache 与逃逸分析见 内存分配 deck;本页只讲 OS 机制侧。
运维必会页:"VIRT 大不是问题"。runtime 三件消费(预留/提交/归还)映射到 mmap/mprotect/madvise 三系统调用。GOMEMLIMIT 是 1.19+ 的现代答案。

Interview QA · Part 1

高频追问:原理与机制

1 · 虚拟内存是什么?解决了什么问题?

隔离/扩展/灵活

给每个进程一套独立虚拟地址空间,经 MMU 页表翻译到物理内存。三层价值:进程隔离(保护)、地址空间可超物理(swap + 按需加载)、机制复用(COW/共享库/mmap)。

2 · 为什么要多级页表?

稀疏树

单级页表按整个地址空间铺开要 512GB/进程;多级页表是稀疏树——顶级常驻、下级按需分配,实际使用只付实际开销。代价:遍历 4 次访存,由 TLB 弥补。

3 · TLB 是什么?为什么进程切换后 TLB 会失效?

映射缓存

TLB 缓存 VA→PA 映射,命中省去页表遍历。切换进程=换页表,旧映射语义作废所以要刷;PCID/ASID 给 TLB 项打进程标签后可以不刷,显著降低切换成本。

4 · 缺页中断的处理流程?minor 和 major 的区别?

VMA 查合法性

CPU 触发 fault → 内核查 VMA 判合法(非法发 SIGSEGV)→ 按类型处理:文件页调入(Page Cache 有=minor,需读盘=major)、匿名页惰性分配、COW 复制、swap in。区别 = 要不要磁盘 I/O。

5 · 分页和分段的区别?为什么现代 OS 用分页?

外部 vs 内部碎片

分段按逻辑单元切(大小可变、外部碎片无解);分页固定大小(内部碎片可控、任意页放任意框)。现代 OS 纯分页,x86-64 段退化到仅 TLS(FS/GS 基址)。

6 · 什么是大页?为什么 Redis 要求关闭 THP?

2MB/1GB

大页让单个 TLB 项覆盖更大范围,降低 miss;代价是 COW 粒度变粗。Redis fork 后子进程写内存触发复制,2MB 大页意味着"写一个字节复制 2MB"——内存膨胀 + 延迟尖刺,所以要 THP=never。

原理组六题覆盖 VM 定义、多级页表、TLB、缺页、分段对比、大页。第 6 题的 Redis THP 案例是运维与 OS 交叉的加分答案。

Interview QA · Part 2

高频追问:工程与排障

7 · malloc 的内存马上占用物理内存吗?

惰性分配

不。malloc 只登记 VMA(地址空间预留),首次写入触发缺页才分配物理页(demand paging);Linux 还有 overcommit 超卖,所以"malloc 4TB 成功、写到一半 OOM"是常态而非 bug。

8 · 内存不足时 Linux 的处理顺序?

回收→swap→OOM

① 回收文件页(干净弃、脏写回);② 匿名页换出到 swap/zram(kswapd 按水位线后台做,耗尽则前台直接回收);③ 都不够则 OOM killer 按 oom_score 杀进程。代价逐级升高,si/so 与 majflt 是观测点。

9 · VIRT、RES、SHR 各是什么?Go 进程 VIRT 巨大正常吗?

地址空间 vs 驻留

VIRT 是虚拟地址空间(含 PROT_NONE 预留),RES 是真实驻留物理内存,SHR 是其中可共享部分。Go runtime 预留大 arena,VIRT 天然巨大——正常;监控与 OOM 判断看 RES/PSS。

10 · swap 设多少?关掉行不行?

swappiness

数据库/低延迟服务常 swap 很小甚至关(怕 major fault 尖刺),配合 GOMEMLIMIT/资源上限控内存;通用服务保留少量 swap + 低 swappiness,回收极端时有缓冲。关 swap 后内存耗尽直接 OOM——用容量换确定性。

11 · 进程被 OOM kill,应用能感知吗?

SIGKILL 级

不能优雅处理:OOM killer 发的是 SIGKILL(不可捕获),进程直接消失。系统层留日志(dmesg 里的 oom-killer 记录);预防靠 RES 监控告警 + GOMEMLIMIT/资源限额,把"被杀"变成"主动收缩"。

12 · 读文件慢,怎么判断是不是缺页/回收的锅?

sar -B / vmstat

看三处:sar -B 的 majflt/s(每秒主缺页——读盘加载文件页)、vmstat 的 si/so(匿名页换入换出)、/proc/meminfo 的 Dirty 与 Writeback(脏页写回挤占 I/O)。majflt 高=文件页冷读或内存被挤;si/so 高=物理内存真的不够。

工程组六题:惰性分配、回收顺序、VIRT/RES、swap 取舍、OOM 感知、慢读排障。第 12 题给出完整的"三看"排查法。

Related & References

相关知识点与参考

OS 系列(本分类)

页面置换算法 →(内存不够时"换谁出去")
进程内存布局与堆分配 →(地址空间的完整地图与 malloc)
进程线程协程 →(fork + COW 的机制基础)
操作系统总览 →(缺页异常属于"异常"这一门类)

跨领域联动

Go 内存分配与逃逸分析 →(arena 预留/提交/归还的 runtime 版)
Redis 持久化 →(fork + COW + THP 的真实代价)
LRU 缓存(手撕实现)→(哈希 + 双链表结构与 O(1) 实现)

参考来源(本 deck 结论可溯源至下列一手材料)

OSTEP ch.13–23(地址空间与分页机制)虚拟化目标、多级页表、TLB、换页的完整理论框架
CSAPP ch.9(Virtual Memory)地址翻译、页表、mmap 与动态内存分配的衔接
Intel SDM Vol.3 ch.4 / AMD64 Manual4 级页表 9-9-9-9-12 切分、PTE 位定义、la57
kernel Documentation/admin-guide/mm(THP / oom)THP 语义、水位回收、OOM 评分
go.dev doc: GOMEMLIMIT / runtime/metricsGo 内存上限与 madvise 归还语义
xiaolincoding.com《图解系统》vmem / linux_mem / mem_reclaim虚拟内存与回收的中文系统讲解
收尾:OSTEP + CSAPP 双主源,Intel SDM 是位级细节出处。总页数 13。下一篇:页面置换算法(回收"换谁出去"的问题)。