当前必须建立的模型
- 虚拟地址如何拆成页号和页内偏移;
- 页表项如何给出物理页号和权限;
- TLB、页表遍历和 page fault 的分工;
- 内核如何建立、修改和撤销映射。
先看清总路线:程序使用虚拟地址,硬件执行翻译,内核准备映射并处理失败。DDR、GPU、IOMMU 和 NUMA 都在后面的分支上,不是今天必须一起拿下的前置知识。
先固定一句话
内核不替每一次访存计算地址。内核建立页表、配置翻译入口并定义权限;MMU 在每次访存时自动执行翻译。翻译无法继续时,硬件把现场交给内核,内核决定修复还是拒绝。
high-level 软件通常只需要面对进程虚拟地址。下面的层次并没有消失,只是被运行时、操作系统和硬件共同遮蔽。
应用开发主要停在左侧;基础内核必须进入中间;DDR/GDDR 电气与封装细节位于更右侧。
“物理地址”仍然是一个地址空间里的编号。多根 DIMM、多个通道和许多 DRAM 颗粒,可以被内存控制器交错组织成一套 CPU 物理地址空间。
一套物理地址空间,不等于只有一块物理内存。同样,“有多块物理内存”也不自动意味着软件必须看见多个互不相干的地址空间。
| 名称 | 回答的问题 | 谁主要处理 |
|---|---|---|
| 虚拟地址 | 这个进程认为数据在哪里? | 编译器、运行时、内核、MMU |
| 线性地址 | x86 分段以后、分页以前的地址是多少? | x86 分段单元 |
| 物理地址 | 请求最终落入 CPU 可寻址物理空间的哪个位置? | MMU、互联、内存控制器 |
| 设备地址 / IOVA | DMA 设备使用什么地址访问内存? | 驱动、IOMMU、设备 |
| GPU 地址 | GPU 页表和内存域怎样解释地址? | GPU MMU、驱动、运行时 |
因此,“physical”不是宇宙中唯一绝对的坐标。进入 DMA、独立 GPU 或异构系统以后,还会出现不同发起者各自的地址域,以及它们之间的映射。
分页机制最核心的动作,就是用虚拟页号查到物理页号;页内偏移保持不变。
VA = VPN | page_offset
PTE[VPN] = PPN | permissions
PA = PPN | same_page_offset
TLB miss 不等于 page fault。TLB 没有缓存时,硬件仍可能从页表中找到有效 PTE;只有映射不存在、格式非法或权限不允许时,才需要产生异常。
page fault 是当前指令触发的同步异常,不是网卡、时钟等外设异步送来的普通中断。
linear = segment_base + offset
physical = page_translate(linear)
现代 x86-64 普通代码中,大多数段基址被设为零,于是 offset 与线性地址在数值上相同。分页仍然存在,所以线性地址通常不等于物理地址。
所以“Linux 把分段设平”是方向性的概括,但放到 Linux 0.11 用户进程上不能直接理解为所有段基址都等于零。
RISC-V 没有 x86 的通用分段路径。基础分页模型可以直接理解成虚拟地址经多级页表翻译为物理地址。
satp
分页模式、ASID、根页表物理页号
| RISC-V 构件 | 作用 | 对应心智位置 |
|---|---|---|
satp |
选择 Bare / Sv39 等模式并指向根页表 | 类似“分页入口” |
sfence.vma |
页表变化后约束地址翻译缓存 | TLB 一致性 |
stval |
保存 page fault 相关地址 | 故障对象 |
scause |
记录 trap 的原因 | 故障类型 |
sepc |
保存发生异常的指令地址 | 恢复位置 |
R/W/X/U/A/D |
读、写、执行、用户、访问和脏页状态 | PTE 权限与状态 |
satp.MODE = Bare 时不经过这套分页翻译。进入 S/U 模式分页以后,仍然需要多级页表、TLB、ASID、权限、大页和异常处理;它是结构更直接,不是没有复杂度。
文章把几条会相互影响、但并不等价的轴画成了一条演进线。先把它们拆开,图就容易重新落地。
| 维度 | 问题 | 可能的答案 |
|---|---|---|
| 物理内存池 | CPU 与 GPU 是否访问同一批物理页面? | 共享一池 / 各有一池 |
| 虚拟地址 | 同一个指针能否被不同计算单元理解? | 统一 / 独立 |
| 缓存一致性 | 一方写入后,另一方怎样看见? | 硬件一致 / 软件同步 |
| 封装 | 内存颗粒放在哪里? | DIMM / 板载 / on-package |
| 局部性 | 访问不同内存域的代价是否相同? | UMA / NUMA 特征 |
CPU RAM 与 VRAM 分离,常见路径需要显式复制或由运行时迁移。
CPU 与集成 GPU 使用系统内存,但地址和同步语义仍取决于硬件与 API。
物理内存仍可分成多个域,却通过共享寻址与一致性互联形成统一编程模型。
统一内存没有消灭边界。它把显式复制的成本,移动到缓存一致性、同步、带宽竞争、内存放置、封装价格和升级能力上。
基础内核首先处理 CPU 虚拟地址到 CPU 物理地址。做到设备、DMA、GPU 或 NUMA 时,才需要继续追问这个物理地址最终属于 DDR、GDDR、HBM,还是另一个可一致访问的内存域。
“写驱动要理解内存这么细”需要按设备类型回答。简单 UART 与现代 GPU 驱动并不处于同一个复杂度等级。
Linux 的驱动不是内核旁边的一套附属代码,它们就是内核的重要组成部分。核心分页、调度和系统调用的概念可以相对紧凑;真正把大量硬件、并发、热插拔、电源与失败状态接进同一系统,规模会迅速增长。
一个现代 GPU 驱动完全可能比教学内核复杂得多,但这不构成学习基础页表或 UART 的前置条件。
先让每一层形成一个可运行、可解释的小闭环,再决定是否进入下一层。
RISC-V 可以提供更干净的第一条路线;Linux 0.11 适合观察真实历史结构。二者不必先后通读,可以围绕同一个具体问题互相解释。
虚拟地址 = VPN + offset
PTE = PPN + permissions
物理地址 = PPN + original offset
VA → satp / page table → PA。现在需要守住的不是全部寄存器名称,而是能够从“VPN 如何变成 PPN”重新推导硬件、页表和异常处理的分工。