一、课程引入与行业背景
1.1 存储墙与功耗墙
当前计算机系统面临的两大瓶颈:
存储墙(Memory Wall):计算并行化已经相对成熟(流水线、乱序执行等),但数据传输带宽始终是瓶颈。即使英伟达 GPU 之间通过 NVLink 连接可达约 90 GB/s,服务器之间通过 InfiniBand 也只有约 5 GB/s,面对当前数据吞吐需求仍然不够。GPU 上的 HBM(High Bandwidth Memory)带宽已经非常高,但对 AI 训练和推理来说仍然存在瓶颈。
计算很重要,但在存储面前就是小弟。以后永远记得这两件事——存储墙和功耗墙。
功耗墙(Power Wall):数据中心需要消耗大量电力并进行散热。设计时需要在性能和功耗之间取得平衡:如果达到 80% 的性能但能将功耗降低一半,往往是值得的。CPU 可以主动降速运行甚至休眠来降低功耗。
1.2 KV Cache 与存储优化的现实意义
DeepSeek V4 等大模型如何降低成本——大量依赖 KV Cache(键值缓存),本质上是如何实现高质量的数据缓存。知道 100 个优化并行的方法,但关键是知道哪个方法能降低 memory 的需求,降低对数据带宽的要求。
二、虚拟存储器的动机
2.1 从实地址到虚拟地址
早期计算机使用实地址(物理地址)方式:程序员直接操作物理内存,知道每一个代码和数据的位置。优点:简单直接。缺点:资源利用率极低,程序无法在不同机器间移植(换台电脑,物理地址就变了)。
2.2 分页的引入
为了解决物理地址的局限性,引入分页机制:
- 页框(Page Frame):将物理内存分成固定大小的块(通常 4KB)。
- 页(Page):将进程的逻辑空间也分成同样大小的块。
- 页框不需要连续分配,操作系统通过页表(Page Table)记录逻辑页到物理页框的映射。
就像人民币有 100、50、20、10 的面额一样,按固定大小分块给你,你要多少给多少个块。
2.3 逻辑地址 vs 物理地址
- 逻辑地址(Virtual Address):程序看到的地址空间。32 位系统中,每个进程都以为自己拥有 0 到 4GB 的完整地址空间。
- 物理地址(Physical Address):实际的内存地址,通常远小于逻辑地址空间。
程序以为自己在一片很宽阔的原野上奔跑——”心有多大世界就有多大”,但实际上可能住在一个很狭窄的小房间里。
2.4 按需分页(Demand Paging)
程序虽然可能很大,但不会同时使用所有代码和数据。用到哪个页就把哪个页调入内存,不用的放在硬盘上。这就是按需分页(Demand Paging)方式。
三、虚拟化的代价与云服务
3.1 虚拟化的性能开销
虚拟存储机制的好处是程序员不用关心物理内存布局,但缺点是地址转换需要大量开销。操作系统使应用程序的执行时间增加了大约 25%。
3.2 云服务中的超卖问题
- CPU 超卖容易:通过分时复用,一个 CPU 核可以卖给多个用户。虚拟化带来约 10%-15% 的性能损耗,但超卖后成本回收。
- 内存超卖困难:内存不能像 CPU 那样简单地分时复用。一旦超卖导致内存不足就会崩溃。这就是为什么云服务中增加内存的价格涨幅远高于增加核数。
3.3 大厂为何自建 GPU 数据中心
AI 大厂追求极致性能,不愿意接受虚拟化带来的 10%-15% 性能损耗。GPU 一旦开始训练就满载运行,无法有效复用。因此大厂往往不使用云服务的虚拟化,而是自建裸金属 GPU 集群。
想做所有虚拟的时候,方便了程序员,但一定有性能消耗。
四、页表结构与地址转换
4.1 页表的基本结构
每个页表项(Page Table Entry, PTE)包含:
| 字段 | 含义 |
|---|---|
| Valid(有效位) | 该页是否已装入内存 |
| Dirty(脏位) | 该页是否被修改过 |
| 访问权限(R/W/X) | 读/写/执行权限控制 |
| 禁止缓存位 | 用于 memory-mapped I/O,数据必须直接写出到外设 |
| 替换控制位 | 用于页面替换策略 |
| 物理页号(PPN) | 对应的物理页框编号 |
禁止缓存位的作用:如果 memory 映射的是显示器等外设,不能用 write-back 策略一直不往外写——屏幕上一动不动,内心已经闪过无数场景了,那也不行。
4.2 虚拟地址到物理地址的转换过程
- 从虚拟地址中提取 VPN(Virtual Page Number)和 VPO(Virtual Page Offset)。
- 用 VPN 查页表,找到对应的 PTE。
- 检查 Valid 位:是否有效?
- 检查访问权限:是否允许当前操作(读/写/执行)?
- 提取 PPN(Physical Page Number),拼接上 VPO 得到物理地址。
4.3 页面映射方式
虚拟存储器采用全相联映射:因为缺页(page fault)的代价极高(需要访问磁盘,等待数百万个时钟周期),所以必须尽量减少 miss。全相联映射允许任何虚拟页映射到任何物理页框,冲突最少。替换策略通常使用 LRU,写策略使用 Write-Back(减少对磁盘的访问)。
五、缺页处理
5.1 缺页的后果
当访问的页不在物理内存中时发生缺页(Page Fault):
- 该指令被阻塞,当前进程被挂起。
- 操作系统进行进程切换,让其他进程继续执行。
- 从硬盘(或 SSD)将所需页面调入内存。
- 数据准备好后通过中断通知 CPU。
- 恢复被挂起的进程继续执行。
缺页代价极高——一旦缺页,中断处理要做好多事情。所以配电脑的时候内存要配大——内存大了就不容易缺页。
5.2 SSD vs HDD 的缺页代价
- HDD:缺页等待数百万个时钟周期。
- SSD:速度提高约 10 倍,缺页等待数十万个时钟周期。
六、TLB(快表)
6.1 TLB 的动机
页表存储在主存中,每次地址转换都要访问主存查页表,这太慢了。解决方案:将页表中常用的条目缓存到一个小型高速 Cache 中。这个 Cache 就是 TLB(Translation Lookaside Buffer),中文叫快表。
页表既然在主存里就是数据,能不能往 Cache 里放一点?就这想法。
6.2 TLB 的结构
TLB 中每项包含:VA address tag(虚拟地址标签)、PA(对应的物理页号)、Valid 位、Dirty 位。
6.3 带 TLB 的地址转换流程
- CPU 发出虚拟地址,先查 TLB。
- TLB Hit:直接得到物理地址,然后访问 Cache/主存获取数据。
- TLB Miss:去主存中的页表查找。
- 页表中有(Page Table Hit):得到物理地址,更新 TLB,访问数据。
- 页表中没有(Page Table Miss / 缺页):触发缺页处理流程。
6.4 各种 Hit/Miss 组合分析
| TLB | Page Table | Cache | 情况说明 |
|---|---|---|---|
| Hit | - | Hit | 最佳情况:不需要访问主存 |
| Hit | - | Miss | TLB 命中但数据不在 Cache,访问主存取数据 |
| Miss | Hit | Hit | TLB 未命中,查页表后数据在 Cache 中 |
| Miss | Hit | Miss | TLB 未命中,查页表后数据在主存中 |
| Miss | Miss | - | 最坏情况:缺页,需要从硬盘调入 |
不可能的组合:
- TLB Hit + Page Table Miss:TLB 是页表的子集,不可能 TLB 有而页表没有。
- Page Table Miss + Cache Hit:页表都没记录的页,不可能已经在 Cache 中。
6.5 TLB 相关操作的权限
TLB 的 flush(刷新)指令和 Cache 的 flush 指令都是操作系统使用的特权指令。用户不能随便调 flush 指令,这就是为什么指令要分级。
七、多级页表
7.1 为什么需要多级页表
如果地址空间很大(如 32 位系统有 $2^{20}$ 个页面),一张平坦的页表需要上百万个条目,占用大量内存。
7.2 IA-32 的二级页表
- 虚拟地址 32 位 = 10 位页目录索引 + 10 位页表索引 + 12 位页内偏移。
- 页目录:1K 个目录项,指向 1K 个子页表。
- 子页表:每个有 1K 个页表项。
- 每页 4KB。
优势:只需要维护 2K 个表项(1K 目录项 + 当前使用的 1K 页表项),而非一个 1M 条目的巨大平坦表。
7.3 Intel Core i7 的四级页表
- 线性地址划分为四级,每级 512 个条目(9 位索引)。
- 每级页表只有 512 项,每项十几个字节,整张表非常小。
- 相比不分级的方案(需要维护巨大的单张页表),四级页表显著降低内存占用。
如果不通过分级的方式,就会变成一个超级大的页表。分级以后每个表都很小。
八、段式、页式与段页式管理
8.1 三种管理方式
| 特性 | 分页 | 分段 | 段页式 |
|---|---|---|---|
| 对用户 | 透明(不可见) | 可见 | 段可见,页透明 |
| 块大小 | 固定(如4KB) | 可变 | 段内再分页 |
| 碎片问题 | 内部碎片小 | 外部碎片(长短段替换) | 综合 |
| 内存分配 | 可以分散 | 需要连续 | 段内页可分散 |
| 磁盘通信 | 页为单位 | 段为单位 | 页为单位 |
8.2 段页式管理(Segmented Paging)
现代操作系统(Linux、Windows)使用段页式混合管理:顶层使用段描述(代码段、数据段、栈、堆等),段内再分页管理。通过段号找到段表,段表中指向对应的页表,再通过页表完成地址转换。
8.3 每个进程都有自己的页表
每一个进程都有自己这一套页表。进程很多的时候,光页表就要占很多空间。
九、存储保护机制
9.1 访问越界保护
C 语言中常见的内存越界问题(如 unsigned 的 length 为 0 时减 1 变成全 F,导致循环越界)。越界访问会触发 Access Violation(Windows)或 Segmentation Fault(Linux)。
9.2 页级权限保护
页表项中的权限位控制每个页面的访问属性:
- Read Only:只读页面。
- Read/Write:可读写数据页面。
- Execute Only:代码段,不允许写入。
CTF 比赛中的 PWN(栈溢出攻击)就是通过填充数据让栈溢出,篡改控制流,让程序执行攻击者的代码。为了防止这种攻击,加了内存保护、Canary、地址随机化(ASLR)等机制。
9.3 硬件保护与软件保护协同
- 软件保护(操作系统):设置页面属性(RO/RW/XO)、进行权限检查。
- 硬件保护:提供管理模式(内核态)和用户模式、Ring 0-3 分级、异常/陷阱/系统调用机制。
光软件和光硬件都是不可能完成这些保护操作的,需要一起来。
9.4 Rust 对内存安全的改进
C 语言积重难返,大量代码存在内存安全问题。Rust 在编译层面做检查,消除越界访问。Python 通过解释器隔离,根本不让用户直接访问物理内存。社区有人呼吁用 Rust 重写 Linux 内核,但已有 C 内核经过长期验证,替换风险大。
十、RISC-V SV39 页表方案
10.1 SV39 概述
RISC-V 架构使用 SV39 方案:
- 虚拟地址 39 位($2^{39}$ = 512GB 虚拟地址空间)。
- 物理地址 56 位(PPN 44 位 + 12 位页内偏移)。
- 页大小 4KB(Offset 12 位)。
- 采用三级页表,每级 VPN 为 9 位(512 个条目)。
10.2 SV39 页表项字段
| 字段 | 含义 |
|---|---|
| V(Valid) | 有效位 |
| R/W/X | 读/写/执行权限 |
| U | 用户页面标志(User/Supervisor 模式访问控制) |
| G | 全局映射位(对所有虚地址空间有效) |
| A(Accessed) | 自上次清除以来是否被访问过 |
| D(Dirty) | 脏位,write-back 时需要注意 |
| RSW | 保留给操作系统使用 |
| PPN[2:0] | 物理页号,分三段对应三级页表 |
10.3 为什么 PPN 分三段
如果不分段,需要维护一个 $2^{44}$ 条目的巨大页表。分成三级后,每级只有 512 个条目,每张子表只有约 2K 条目,每条目几个字节。通过三级查找逐级缩小范围,最终得到完整的 56 位物理地址。