03.05 虚拟内存:从地址翻译到网关驻留预算
返回第三卷 · 字节与地址前置:03.01 · 进程地址空间前置:03.02 · 进程观察前置:03.03
03.05 虚拟内存:从地址翻译到网关驻留预算
DeepTutor 初稿经技术与教学审阅后的静态课程。所有地址、页框和连接预算都是纸上模型;没有运行 Go、Linux 内存实验、IM 服务、基准或站点。4 KiB 是本章的假设页大小,真实系统还可能使用其他页大小。教材没有测量任何现成系统的 RSS。
一、同一条连接,为什么“申请了”不等于“占了 RAM”
03.01 已认识字节、地址和存储层次;03.02 把进程地址空间与实际物理内存分开;03.03 学会只读观察进程;03.04 说明许多连接可等待事件而不持续占用 CPU。现在看另一个 IM 网关问题:连接数增加时,进程显示一大片地址空间,是否说明同样多的物理内存已经被占用?如果业务对象减少而 RSS 没有立即下降,是否必然泄漏?
先只用一个理想化模型:4096 条虚构连接,每条为接收缓冲保留 8 KiB 的虚拟地址范围,但目前只触碰其中 4 KiB。这些保留范围合计 4096×8 KiB=32 MiB;若每条恰好只有一页新驻留,触碰带来的理想新增驻留是 4096×4 KiB=16 MiB。后者并非真实网关 RSS:我们还没计程序代码、Go 运行时、其他对象、页表、共享页、栈或映射,也没有说实际 Go 分配器按“每连接独立两页”布置。这个对比只说明虚拟范围、已触碰页与驻留页面不能直接划等号。
| 第一遍 | 第二遍 | 交付物 |
|---|---|---|
| 虚拟地址、页/页框、地址拆分与页表 | TLB、缺页、驻留/RSS、交换和网关预算 | 一张 0x2345 地址翻译图、四类访问结果表、连接内存假设清单 |
本章不要求先会 Go 垃圾回收、mmap 或 Linux 内核代码。03.06 才深入分配、映射、碎片和 Go heap 与 RSS 的关系;03.10 再系统学习观测工具。
二、页与页框:把地址拆成“哪一页、页内哪里”
虚拟地址是进程按自己的地址视图使用的位置。分页把虚拟范围分成固定大小的虚拟页,把物理内存分成相应大小的页框。操作系统与硬件一起把“虚拟页号”映到“物理页框号”;同一虚拟页内的页内偏移保持不变。连续的两个虚拟页可以映到不连续的物理页框,程序仍可按连续虚拟地址访问。
本章假设页大小 4 KiB = 4096 B = 2^12 B,因此一个虚拟地址的低 12 位是页内偏移。计算地址 0x2345:
0x2345 = 2 × 0x1000 + 0x345
虚拟页号 VPN = 2
页内偏移 offset = 0x345 = 837 字节再设玩具页表写着 VPN 2 → PFN 7,且该页已驻留并允许本次访问,则物理地址是 7×0x1000+0x345=0x7345。VPN 变成 PFN,偏移 0x345 不变;若算出 0x7002,就是误把页号 2 当作偏移。本图没有指定真实 CPU 的完整地址位数或多级页表格式。
| 输入 | 拆分 | 查得映射 | 合成结果 |
|---|---|---|---|
虚拟 0x2345 | VPN 2,偏移 0x345 | PFN 7 | 物理 0x7345 |
虚拟 0x2000 | VPN 2,偏移 0 | PFN 7 | 物理 0x7000 |
虚拟 0x2fff | VPN 2,偏移 0xfff | PFN 7 | 物理 0x7fff |
“一页 4096 字节”也意味着边界:0x2fff 仍在 VPN 2,0x3000 已进入 VPN 3,不能继续套用 VPN 2 的页表项。地址计算前先确定页大小,才能正确拆分。
三、页表:地址能否访问,和物理页在哪里
页表保存地址转换所需的信息。入门时可想成“这个进程的某虚拟页是否有可用映射、对应哪个物理页框、允许读/写/执行哪些操作”。实际系统常有多级页表、共享映射、写时复制等细节,本章只把它们留作后续;纸上 VPN→PFN 对照并非 Linux 页表格式。
页表视图是按进程隔离的:进程 P 的虚拟地址 0x2345 可以对应 PFN 7,进程 Q 的同一数值虚拟地址可能对应另一个页框,也可能根本不可访问;共享映射是需要明确建立的例外。由此可理解“指针数值相同”不能让两个进程自动看到同一对象。
要区分三种情况:
- 有合法映射,页已驻留,权限允许。 可翻译并完成访问。
- 地址属于合法范围,但所需页暂未驻留或尚未建立物理页。 可能由操作系统按需建立、调入,然后重试;是否成功取决于资源和映射状态。
- 地址不属于允许访问的范围,或本次读/写/执行违反权限。 访问会失败;操作系统不会因为程序“想要用它”就自动给它一页可读写内存。
“地址已保留”只是某种地址范围安排,不等于每一页已经有私有物理页;“页表里有记录”也不意味着当前访问一定合法或驻留。进程保护、映射规则和具体异常处理都参与决定结果。
四、TLB:缓存的是翻译,不是消息正文
每次访问内存都需要先得到地址翻译。若每次都完整查页表,成本很高;CPU 的 TLB(translation lookaside buffer)缓存近期的虚拟页到物理页框翻译及访问相关信息。它解决的是地址翻译的重复查找,与缓存消息正文的数据缓存不是同一层。
沿固定例子走两次 0x2345,假设 VPN 2 已在页表中映到 PFN 7 且驻留:
| 第一次访问 | 第二次访问 |
|---|---|
| TLB 里暂无 VPN 2:TLB miss | TLB 已有有效翻译:TLB hit |
| 查页表,映射与权限均有效 | 使用缓存的翻译及必要权限判断 |
填充 TLB,再访问物理 0x7345 | 再访问对应物理位置 |
| 没有因此发生缺页 | 也没有缺页 |
这里的关键是:TLB miss 不等于 page fault(缺页异常)。TLB 未缓存,但页表可给出有效且已驻留的翻译,就只需完成翻译路径;只有缺少可立即用于这次访问的页/映射或权限不符等条件,才进入异常处理。真实硬件的页表行走、TLB 层级和权限缓存比表格复杂,不能用这两行推算具体延迟。
五、缺页:异常后可能补页、读入,也可能失败
缺页异常是一次内存访问需要操作系统处理的事件名称,绝不等同“磁盘坏了”。看三条不同纸上路径:
| 访问条件 | 可能处理 | 不能直接推断 |
|---|---|---|
| 合法的按需分配页,尚未实际建立物理页 | 分配/映射一个页框,按要求初始化,再重试;可不读磁盘 | “发生缺页=一定做磁盘 I/O” |
| 合法映射的页不在 RAM,内容需从后备存储取回 | 调入后重试,可能产生 I/O 和明显等待 | “每次缺页开销一样” |
| 未映射地址或违反访问权限 | 拒绝访问,进程可能收到故障信号 | “访问任何地址都能自动补页” |
Linux 的“次要/主要缺页”统计通常用于区分未需后备存储读取与需后备存储读取等情况,但一次故障应结合系统口径和映射类型判断;不能拿一个计数直接证明 IM 服务在读哪条消息。写时复制、文件映射和透明大页还有其他变体,03.06 才继续。TLB miss、可恢复缺页、权限失败是三种不同诊断层次;把它们都叫“缓存未命中”会让排障失去方向。
缺页处理期间,当前执行路径可能等待内核准备页面;这连接了 03.04 的“任务等待不等于占满 CPU”。但也不能反推“消息慢一定是缺页”:网络、存储、锁、队列和下游等待都可能有相同表面症状。
六、驻留、RSS 与交换:指标回答的是不同问题
先做更小的模型:进程保留连续 3 页的合法虚拟范围,合计 12 KiB,只实际触碰第 0、2 页;假设每页触碰后都有独立物理页框,且都继续驻留,没有别的映射。则这段范围对应的虚拟大小是 12 KiB,理想新增驻留是 8 KiB,第 1 页未触碰不能直接算作已占一页 RAM。页表等元数据另计;实际程序启动时也不可能只拥有这三页。
Linux /proc/<pid>/status 的 VmSize 描述虚拟内存大小,VmRSS 描述进程驻留集,且 VmRSS 分为 RssAnon、RssFile、RssShmem 等视角。某些 /proc 快速统计被文档标为不精确;RSS 包含代码、栈、匿名页、文件映射和共享页等,不能直接读作“Go 业务对象的字节和”。共享页还会影响跨进程相加的解释。数字必须带观察时间、单位和统计来源,别把 VmSize−VmRSS 简单当成“已换出字节数”:那还可能含未触碰的保留范围。
交换是在系统配置与策略允许时,把某些内存页面的内容放到后备存储,再需要时读回;它不是“每个虚拟页都必然有一个 swap 副本”。文件映射可由文件作为后备,某些页可能不能按这个简化说法换出;系统也可能未启用 swap。若内存压力大,缺页调入与回收可增加时延;但只看一次 RSS 不能证明发生了交换、内存泄漏或 OOM。容器限制与 OOM 将在 03.11 深入。
七、把页预算放回 IM 网关症状
回到 4096 条连接的纸上模型。每条保留 8 KiB,是两页虚拟范围;只触碰 4 KiB,是一页新驻留:
虚拟范围:4096 × 2 页 × 4096 B = 32 MiB
理想新增驻留:4096 × 1 页 × 4096 B = 16 MiB
两者的差额 16 MiB 在此模型中是尚未触碰的页,不是已换出的证据这个数字不是真实 Go 网关的按连接成本。实际连接还可能关联文件描述符、内核 socket 缓冲、Go 对象、goroutine 栈、TLS 状态和共享数据;内核 socket 资源不一定计在该进程 RSS 中,Go 分配器也可能复用、保留或以不同粒度管理页面。连接关闭、对象失去引用、垃圾回收、页返回操作系统和 RSS 下降是几件相隔的事,留到 03.06 与 05.07 展开。
面对“连接数翻倍,RSS 却增加得更多”,先列能区分原因的证据:同口径时间序列中的连接数、进程 VmSize/VmRSS、Go heap 在相同时间窗的对象与存活量、缓冲容量、GC 和内核/容器限制。只观察到相关变化,还不能断定泄漏。反过来,“Go heap 看着稳定”也不能排除栈、映射、共享页或进程外内核资源增长。03.10 将教如何选择只读观察工具和解释采样误差。
八、交付地址图、异常表与分层练习
交付三份纸上答案:画出 0x2345→VPN 2/offset 0x345→PFN 7→0x7345;分别解释“TLB miss 但页已驻留、合法首次触碰、非法地址”三条路径;再写出三页与 4096 连接模型的计算、漏计项和能检验的未来观测。不能把玩具值写成真实 Linux 或 OpenIM 测量。
分层练习:先答,再展开反馈
1. 虚拟地址是物理内存上的直接位置吗?
不是。进程使用虚拟地址,硬件与操作系统按映射翻译到物理位置。
2. 4 KiB 等于多少字节、多少个 2 的幂?
4096 B,即 2^12 B。
3. 4 KiB 页的页内偏移占低几位?
低 12 位。
4. `0x2345` 的 VPN 是多少?
2,因为它位于 0x2000..0x2fff 这一页。
5. `0x2345` 的页内偏移是什么?
0x345,十进制 837 字节。
6. VPN 2→PFN 7 时,物理地址是什么?
0x7345,在 PFN 7 的页框基址 0x7000 上加 0x345。
7. `0x2fff` 与 `0x3000` 属于同一虚拟页吗?
不属于;前者 VPN 2,后者 VPN 3。
8. 连续虚拟页必然映到连续物理页框吗?
不必然;页表可分别映射。
9. 两个进程的虚拟地址数值相同,就自动共享对象吗?
不会;各自映射可不同,共享必须有明确机制。
10. 页表只记录 PFN,不管访问权限吗?
不对。地址转换还涉及访问权限和映射状态。
11. TLB 主要缓存消息正文还是地址翻译?
地址翻译及相关访问信息,不是消息正文。
12. TLB miss 一定是缺页异常吗?
不是。页表可提供已驻留、有效的翻译。
13. 合法页第一次触碰发生缺页就一定要读磁盘吗?
不一定,按需建立/清零页面可不读后备存储。
14. 写入只读映射会被自动升级成可写吗?
不会凭应用意愿自动升级;可能因权限错误失败,具体合法写时复制是另一种受控机制。
15. 访问完全未映射的地址会如何?
不能假定自动得到一页;会进入异常处理并可能因非法访问失败。
16. 保留 3 页只触碰第 0、2 页,玩具虚拟和新增驻留各是多少?
虚拟 12 KiB;在独立驻留且不计其他映射的假设下,新增驻留 8 KiB。
17. `VmSize−VmRSS` 都是 swap 中的数据吗?
不是。还可能包含从未触碰等未驻留的合法虚拟范围。
18. 进程 RSS 是 Go 业务对象大小之和吗?
不是。RSS 是进程驻留页的统计视角,含多种映射与共享页等。
19. 系统一定启用了 swap 吗?
不一定,取决于配置和运行条件。
20. 4096 连接各保留 8 KiB,虚拟范围合计多少?
32 MiB;这是固定假设,不是实测按连接成本。
21. 每连接只新触碰 4 KiB 时,理想新增驻留多少?
16 MiB;页表、代码、Go 运行时、内核 socket 等未计。
22. 业务对象变少、RSS 未立刻下降,就证明泄漏吗?
不能。分配器保留、共享/映射、GC 与页返回时机均需进一步区分。
来源与下一步
- OSTEP:分页入门与TLB:页、页框、页表、偏移与翻译缓存。
- Linux 内核内存管理概念:虚拟内存、页表和 TLB 的系统层背景。
- Linux
/proc/<pid>/status与statm:虚拟大小、RSS 及统计精度的口径。
按学习路线,下一章03.06 分配与映射进一步解释分配、映射、页缓存与 Go heap/RSS 的差别。离开本章前,应能从“连接多但 CPU 不高、RSS 变化”这个症状出发,说清地址范围、翻译、首次触碰、驻留和统计视角各回答什么问题。