6.1 地址空间与地址翻译


文档摘要

6.1 地址空间与地址翻译 本节摘要:虚拟地址是仓储车间的"货架编号系统":每个进程都以为自己独占整座仓库,编号到物理货架的翻译由硬件现场完成。本节讲翻译链路的三件套——页表、TLB、缺页异常——以及页大小与多级页表的权衡。读完你应当能手工拆解一次虚拟地址的翻译过程,并解释为什么 TLB 未命中的代价比缓存未命中更贵。 地址的两副面孔 没有虚拟地址的世界,程序里写死物理货架号,两个进程一碰面就互相踩货。虚拟地址把"程序看到的地址"与"物理仓库的真实位置"解耦:每个进程拿到一套独立编号,翻译机构在运行时把它映射到物理内存的任意位置。

6.1 地址空间与地址翻译

本节摘要:虚拟地址是仓储车间的"货架编号系统":每个进程都以为自己独占整座仓库,编号到物理货架的翻译由硬件现场完成。本节讲翻译链路的三件套——页表、TLB、缺页异常——以及页大小与多级页表的权衡。读完你应当能手工拆解一次虚拟地址的翻译过程,并解释为什么 TLB 未命中的代价比缓存未命中更贵。

地址的两副面孔

没有虚拟地址的世界,程序里写死物理货架号,两个进程一碰面就互相踩货。虚拟地址把"程序看到的地址"与"物理仓库的真实位置"解耦:每个进程拿到一套独立编号,翻译机构在运行时把它映射到物理内存的任意位置。这份解耦一口气兑现了四样东西——隔离(进程 A 的编号在 B 的映射表里查不到,想踩也踩不到)、重定位(程序加载到任意物理位置,代码里不用改一个地址)、按需加载(编号先发、货可以后到)、共享(两个映射表把不同编号指到同一物理货架,共享库就这样实现)。

翻译不是逐字节的,而是按为单位——典型 4 KB 一页。页是翻译、保护与加载的公共颗粒度:权限位(可读、可写、可执行)挂在页上,缺页处理以页为单位把磁盘上的数据搬进来,地址随机化(ASLR)也按页打乱布局。拆解一次翻译看清结构(以常见的三级页表、4 KB 页为例):

虚拟地址(示例拆分): 47 39 38 30 29 21 20 12 11 0 ┌───────────┬──────────┬──────────┬───────────┬───────────┐ │ 一级索引 │ 二级索引 │ 三级索引 │ 页内偏移 │ (4KB 页) │ └───────────┴──────────┴──────────┴───────────┴───────────┘ 每级索引查一张页表取下一级表地址;最后一级给出物理页号, 拼上页内偏移即为物理地址。

低 12 位在翻译中原样穿越——翻译只管页号,页内的位置不变。多级页表的意义在于省内存:整张映射表按需分配,没用的地址范围根本不建表项;x86-64 的四级表、RISC-V 的 Sv39(三级)与 Sv48(四级)都是这个思路的变奏。

TLB:翻译结果的缓存

逐级查表太贵——每次访存都先翻三级表等于访存次数翻四倍,不可接受。解法照旧是缓存:TLB(翻译后备缓冲)把"虚拟页号 → 物理页号与权限"的近期结果缓存起来,命中则翻译零开销。这带来一条重要的性能规律:TLB 是按页工作的,程序的"页工作集"决定了 TLB 命中率。数据结构散布在几千个页上(比如巨型链表每个节点散落各处),哪怕缓存总量装得下,TLB 也会被打穿——这就是"缓存友好但 TLB 不友好"的反直觉场景。

TLB 未命中的代价比缓存未命中更贵:缓存未命中去下一级缓存找数据,TLB 未命中却要去内存里逐级走页表(页表遍历),还要防着遍历本身再引发缓存抖动。现代硬件用页表遍历缓存、更大的 TLB 分层(L1 速度型加 L2 容量型)来止血,软件端则有大页这张牌:4 KB 页换 2 MB 大页,一张表项覆盖的地址范围放大 512 倍,TLB 压力骤减——数据库、JVM 这类大工作集应用对大页的执念正源于此。代价是碎片化管理与大页分配的延迟,这是一道典型的工程权衡题而非免费午餐。

缺页异常:货还没到的工单

翻译过程中查不到有效表项,或权限不符,硬件不自行其是,而是抛出缺页异常,把控制权交给操作系统(异常入口机制在 7.1 节展开)。缺页不总是错误,多数时候是正常的协作机制:按需调页——程序启动并不真把全部内容读进内存,只建立映射,首次访问某页时才真正加载;交换——物理内存吃紧时,操作系统把冷页挪到磁盘,程序再访问时缺页把它换回来;写时复制——fork 出的子进程与父进程共享页面并标为只读,任一方首次写入才触发缺页、复制出私有副本。

识别"良性缺页"与"恶性缺页"是排错基本功:首次访问映射区、写时复制触发属于前者,性能上表现为偶发长延迟;解引用野指针、越界踩到未映射区属于后者,表现为段错误崩溃。同一个异常入口,两种截然不同的人生——操作系统靠出错地址与访问类型的上下文区分它们,你在分析崩溃报告时也应带上这两条信息。

翻译路径的全程演练

把翻译链路亲手走一遍,各环节的代价就都有了体感。设 4 KB 页、三级页表(以 Sv39 为参照)、TLB 冷启动:CPU 发出虚拟地址,TLB 查询未命中;页表遍历单元拿着页表基址寄存器出发,第一级查表要访存一次(大概率缓存未命中,走内存),拿到二级表地址;第二级再访存、第三级再访存——三次访存后终于拼出物理页号;期间若某级表项的有效位是零,或权限位拒了本次访问,翻译就地终止并抛缺页或保护异常。全链路的最坏情形(三级全走内存)是三次主存延迟,约数百个周期——这就是"TLB 未命中比缓存未命中贵"的定量来源,也是大页(一张表项覆盖 2 MB,遍历深度还减一级)值钱的原因。

软件侧有一套对应的观测手段:性能剖析工具能把"TLB 未命中停顿"从访存停顿里单拎出来。看到一个数据密集型负载的 dTLB 未命中计数居高不下,对策按序试:调整数据结构的空间局部性(让热数据聚页)、换大页、或改遍历算法为分块以缩页工作集。翻链条的功夫加上观测数据,性能问题就不再是黑盒。

容易踩的坑

第一个坑:把"64 位 CPU"当成"64 位地址全可用"。x86-64 当前实际实现的虚拟地址宽度是 48 位(部分平台 57 位),高位必须按符号扩展填满——手写汇编或解析内核转储时遇到高位全是 1 或全是 0 的地址,先想到规范形式检查,而不是当成有效偏移。第二个坑:忽视 TLB 锁定与上下文切换的成本。进程切换要换掉整套地址映射,TLB 里上一个进程的翻译全部作废(靠地址空间标识符缓解),高频切换的服务进程会把性能耗在重新暖机上——排查"上下文切换吃掉性能"时要连 TLB 一起算账。第三个坑:给实时或内核场景随手用大页。大页降低 TLB 压力的同时抬高了分配延迟与内存碎片,实时路径反而受害;工具没有银弹,只有场景匹配。

本节要点回顾

  • 虚拟地址解耦编号与货架,一表四得:隔离、重定位、按需加载、共享;翻译以页为单位。
  • 多级页表按需建表省内存,TLB 缓存翻译结果;TLB 未命中要真刀真枪走页表,比缓存未命中更贵。
  • 页工作集决定 TLB 命中率:缓存友好不等于 TLB 友好;大页是软件端的解药也是新的权衡。
  • 缺页异常是协作机制多于错误:按需调页、交换、写时复制都借道于此;恶性缺页才是段错误。
  • 64 位平台的可用地址宽度与规范形式、上下文切换的 TLB 代价,都是工程中真实会咬人的细节。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U