尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

一个指针解引用后,CPU 到底做了什么?从虚拟地址到页表、TLB 与物理内存

一个指针解引用后,CPU 到底做了什么?从虚拟地址到页表、TLB 与物理内存 1. 两个进程明明用了同一个地址为什么不会互相干扰假设有两个正在运行的进程。它们都访问地址0x400000却读出了各自的数据进程 A访问 0x400000 → 读到 A 的数据 进程 B访问 0x400000 → 读到 B 的数据同一个数字怎么会找到两个地方如果 A 在这里写入数据为什么不会把 B 的数据覆盖掉1.1 同一个地址为什么可以有两个结果先把物理内存想象成一排有编号的小格子每个格子保存一个字节。CPU 最终要通过这些编号找到数据。如果程序拿到的编号就是物理内存的编号进程 A 和进程 B 都写入同一个位置就会互相覆盖。进程之间也难以隔离。于是程序平时使用另一套编号虚拟地址。内存硬件最终使用的编号叫物理地址。每个进程都有自己的虚拟地址空间因此地址的数字相同不代表它们指向同一个物理位置。可以把地址前面想象成隐含了“属于哪个进程”进程 A 的 0x400000 → 一处物理内存 进程 B 的 0x400000 → 另一处物理内存1.2 CPU 怎么知道这次该去哪个地方CPU 需要一份地址对照关系才能把程序使用的虚拟地址变成物理地址。这份关系记录在页表中。不同进程通常有各自的用户地址空间也就有各自的页表。假设进程 A 和 B 的页表分别记录A 的页表虚拟地址 0x400000 附近 → 物理页框 10 B 的页表虚拟地址 0x400000 附近 → 物理页框 37进程 A 运行时CPU 用 A 的页表翻译切换到进程 B 时改用 B 的页表。负责执行地址翻译的硬件叫 MMU内存管理单元。这样同一个虚拟地址就可以找到不同的物理位置。程序中的指针也参与这件事。比如int value *ptr;指针ptr在普通 Linux 用户程序中保存的通常是虚拟地址CPU 读取它指向的数据时仍要完成上述翻译。不过页表不可能给每一个字节都单独写一条记录。它是怎样把地址组织起来的1.3 一个地址怎样拆成“页号页内偏移”如果按字节记录映射页表会大得难以使用。所以计算机把虚拟地址空间分成一块块固定大小的页物理内存也分成同样大小的页框。页表记录的是“虚拟页对应哪个物理页框”页内的具体字节位置由地址自己提供。先不管复杂的多级页表只看这个最核心的动作。假设页面大小是常见的 4KB4KB 4096 B 2¹² B一页里有2¹²个字节所以只需要地址的低 12 位就能表示这个字节在页内的位置。剩下的高位则表示它属于哪个虚拟页。虚拟地址 虚拟页号 页内偏移来算一个最小的例子。假设程序访问虚拟地址0x1234页面大小0x1000 虚拟页号0x1234 / 0x1000 0x1 页内偏移0x1234 % 0x1000 0x234假设页表中记录虚拟页 0x1 → 物理页框 0xA那么最终的物理地址就是0xA000 0x234 0xA234地址翻译改变的是页号不是页内偏移。页内偏移表示“第几个字节”。无论这个页被放到哪个物理页框它在页内的相对位置都不需要改变。1.4 页表项只保存物理页号吗页表可以先想象成一个数组用虚拟页号找到一条记录再从中取出物理页框号。这条记录叫页表项Page Table EntryPTE物理页框号常缩写为 PFN。但记录位置还不够。假如进程 A 找到了一个页面却试图改写只读数据CPU 也必须拦住它。因此页表项还会记录页面当前是否有有效映射、用户程序能不能访问、是否允许写入或执行等信息。这些位让页表同时承担了两件事地址翻译虚拟页在哪个物理页框 访问检查这次读、写或执行被允许吗所以页表不只是一张地址对照表也是进程内存保护的硬件依据。1.5 既然一张表就能查为什么还需要多级页表单级页表的查找思路非常直接但它有一个致命问题太大。以 32 位虚拟地址、4KB 页面为例虚拟页数量是2³² / 2¹² 2²⁰ 1,048,576 页如果每个页表项占 4 字节一个进程的单级页表就需要1,048,576 × 4B 4MB4MB 看起来不算大但这是每个进程的固定成本。更麻烦的是某个小程序可能只用到很少的虚拟页却仍然要为整个地址空间准备完整表格。多级页表的解法很像查字典先看目录再找具体的页。虚拟地址的高位 → 第一级表 虚拟地址的中间位 → 第二级表 虚拟地址的更低位 → 最后的 PTE如果一大段虚拟地址根本没有使用那么对应的下级页表就可以完全不创建。多级页表的核心不是让查找步骤更少而是让未使用的地址空间不占用下级页表。1.6 完整的寻址流程先以进程 A 访问0x400000为例把过程完整走一遍进程 A 给出虚拟地址 0x400000 ↓ 拆出虚拟页号和页内偏移 ↓ 用虚拟页号查进程 A 的页表 ↓ 得到物理页框号 ↓ 和页内偏移拼成物理地址 ↓ 访问真正的数据换成进程 B地址数字仍然可以是0x400000但这一次查的是 B 的页表于是可能得到另一个物理页框。这条流程里还有一个明显的麻烦页表本身也放在内存中内存访问的速度远远小于CPU。为了读一次数据难道要先读页表再读数据吗1.7 x86-64 的多级页表究竟怎样寻址现在再来看一个更接近真实硬件的例子。在常见的 x86-64 四级分页中4KB 页面使用的虚拟地址部分可以这样拆分47 39 38 30 29 21 20 12 11 0 ┌─ PML4 索引 ─┬─ PDPT 索引 ─┬─ PD 索引 ─┬─ PT 索引 ─┬─ 页内偏移 ─┐ │ 9 bit │ 9 bit │ 9 bit │ 9 bit │ 12 bit │ └────────────┤────────────┤──────────┤──────────┤────────────┘为什么每级刚好是 9 位x86-64 的一个页表项通常占 8 字节一张页表自身刚好放在一个 4KB 页中4096B / 8B 512 2⁹所以一级页表有 512 个项用 9 位索引刚好选中其中一项。CPU 的查找过程大致如下CR3寄存器给出当前地址空间的顶级页表根用虚拟地址的 PML4 索引找到第一个表项表项指向 PDPT再用 PDPT 索引查找继续走过 PD 和 PT最后找到 PTEPTE 给出物理页框和权限物理页框基址与原来的 12 位页内偏移组合得到物理地址。Linux 在内核中把页表抽象为五级PGD → P4D → PUD → PMD → PTE这不代表每台机器都真的要走五级。架构没有使用的层级可以被“折叠”。例如在 x86-64 四级分页下P4D 通常就是折叠的开启五级分页时它才对应真实的硬件层级。而且遇到大页时也不一定走到最后2MB 页可以在 PD/PMD 层结束1GB 页可以在 PDPT/PUD 层结束。现在寻址过程是明白了但新问题也出现了这不会太慢吗1.8 进程切换时CPU 怎么换另一张页表现在再回头看开头的两个进程。CPU 运行 A 时应该使用 A 的映射改为运行 B 时应该使用 B 的映射。它怎么找到当前该用的页表在 x86-64 中CR3寄存器保存当前地址空间顶级页表的物理地址。页表本身放在物理内存中CPU 按规定的格式读取它。切换到另一个地址空间时内核会设置相应的页表根。于是同一个虚拟地址可以被翻译到另一个物理页框。更准确地说页表跟着地址空间走。同一个进程里的线程通常共享地址空间也共享用户页表不同进程通常拥有不同的用户地址空间和页表根。不过“不同进程使用相同虚拟地址”不保证物理页面一定不同。共享内存、共享库或fork()后尚未复制的页面都可能让两个地址空间映射到同一物理页。能不能共享由映射和权限决定不是由地址数字是否相同决定。1.9 读一次数据先查四次页表页表本身也放在内存中。如果每次访问数据都完整走一遍四级页表逻辑上就可能先读取 4 个页表项然后才读到真正的数据。4 次页表访问 1 次真正数据访问实际处理器还有数据 Cache数据缓存和 page-walk cache页表遍历缓存等机制所以这些读取不一定真的都到 DRAM。但如果每条加载、存储指令都重复做地址翻译开销仍然无法接受。于是 CPU 在 MMU 附近放了一块很小、很快的缓存TLBTranslation Lookaside Buffer中文常称快表。TLB 保存的不是程序数据而是最近使用过的地址翻译虚拟页号 VPN → 物理页框 PFN 权限它可以理解成页表中少量热门结果的硬件快照。1.10 TLB 命中和未命中分别会发生什么现在让进程 A 再次访问0x400000。CPU 已经知道该使用 A 的地址空间接下来 MMU 会先拿虚拟页号查 TLB。1.10.1 TLB 命中TLB 已经有对应翻译MMU 可以直接拿到 PFN检查权限再与页内偏移组合成物理地址。如果翻译虽然命中但缓存的权限不允许这次读、写或取指访问仍然会触发异常“命中”并不能绕过页级保护。虚拟地址 → TLB 命中 → 物理地址这是最常见、也是最快的路径。1.10.2 TLB 未命中TLB 中暂时没有结果硬件需要遍历页表这个过程叫页表遍历page walk。如果页表中的映射存在且权限允许CPU 会把翻译结果填入 TLB然后继续执行原来的访存。虚拟地址 → TLB miss → page walk → 填充 TLB → 物理地址这条路更慢但它仍然是正常的硬件行为。1.10.3 页表也无法完成翻译如果页表遍历时发现当前没有可用映射或者这次访问违反了权限CPU 会触发 Page Fault进入内核。即使 TLB 中命中了翻译只要权限不允许这次访问也会触发异常。虚拟地址 → TLB miss → page walk 失败 → Page Fault这里有一个特别容易混淆的结论TLB miss 不是 Page Fault。TLB miss 后只要页表中有有效映射且权限允许地址翻译就能继续。映射缺失或权限冲突时才需要让内核介入。此时也不能直接断定程序出了错内核可能按需分配页面也可能最终拒绝这次访问。1.11 TLB 和 CPU Cache 到底有什么区别两者都快都小都会命中或未命中但它们缓存的东西完全不同。对比TLBCPU Cache缓存什么地址翻译和页权限指令和数据的 Cache Line常见关系VPN → PFN地址 → 数据解决什么查页表太慢访问主存太慢未命中后遍历页表查下级 Cache 或内存简单来说TLB 帮 CPU 找地址Cache 尽量直接交出数据。1.12 进程切换后TLB 里的映射不就乱了吗页表是地址空间的一部分。进程切换时CPU 需要切换当前使用的页表根在 x86-64 中这与CR3寄存器有关。如果 TLB 里只记虚拟页号不记这条翻译属于哪个地址空间那么进程 A 的映射确实可能被进程 B 误用。最直接的做法是在切换时使相关 TLB 项失效。现代处理器也可以使用 ASID 或 x86 的 PCID 这类标识给 TLB 项带上地址空间标签从而减少不必要的清空。1.13 大页为什么能减少 TLB 压力TLB 的容量有限。如果每条 TLB 项映射一个 4KB 页那么 1000 个页也只能覆盖大约 4MB 内存。换成 2MB 大页后一条映射覆盖的范围是原来的 512 倍。这种“TLB 能同时覆盖多大内存范围”的能力常被称为 TLB reach。大页还能让 page walk 在更高层级提前结束节省下级页表空间。但它也不是免费的大页更容易造成内存浪费分配和回收也会更复杂。所以大页解决的不是“页越大越好”而是特定工作负载下页表开销与 TLB 覆盖范围的问题。1.14 回到开头为什么同一个地址不会弄混现在回到两个进程都访问0x400000的场景。以进程 A 的一次读取为例CPU 得到虚拟地址0x400000MMU 把地址拆成虚拟页号和页内偏移MMU 先查询 TLBTLB 命中时直接得到物理页框和权限TLB 未命中时硬件从 A 当前使用的页表根开始逐级遍历找到合法 PTE 后结果被填入 TLBPFN 与原来的页内偏移组合成物理地址处理器再通过 Cache 层次获取真正的数据如果映射不存在或权限不允许就触发 Page Fault让内核决定如何处理。换成进程 BCPU 使用的是 B 的地址空间。同样的虚拟地址会按 B 的映射翻译因此可以读到 B 自己的数据。页表负责指向哪里、允许怎样访问TLB 负责加快重复的地址翻译。所以开头真正要补全的不是地址数字而是**“谁在使用这个地址”**。只看0x400000我们无法判断它最终落到哪一块物理内存知道当前地址空间及其映射之后CPU 才能找到答案。
返回列表