地址翻译从虚拟到物理

地址翻译——从虚拟到物理的全过程

用”教室座位表”理解整件事

 
一个班级有50个学生,但学校实际座位排布不是按学号的。
 
老师(CPU)喊:"3号同学,回答问题!"
 
班长(MMU)翻座位表(页表)→ 3号 → 实际坐在第5排第2列
 
班长告诉老师:"3号在第5排第2列"
 
整个过程3号同学自己都不知道自己被"翻译"了——对3号来说,
 
他就是3号。对老师来说,喊"3号"就行,不用关心他坐哪。
 

拆开看每一步:

 
程序里写:int *p = malloc(100);
 
printf("%p", p);  // 打印 0x7fff... → 这是虚拟地址,不是物理地址!
 
CPU执行指令:load 0x1000  ← CPU说"我要读虚拟地址0x1000的数据"
 

 
    MMU 翻页表:虚拟页号→物理页框号
 

 
   物理地址 0x3F000  ← 内存条实际收到的地址
 
内存条这辈子都不知道什么叫"虚拟地址"——它只认物理地址。
 
翻译这件事只发生在CPU内部,对运行的程序完全透明——
 
程序从头到尾都觉得自己在用"真实的物理地址"。
 

一个虚拟地址,切成两半看

 
虚拟地址 = 页号 + 页内偏移
 
假设32位系统,页大小4KB(=4096字节=2^12)
 

graph LR

    subgraph 虚拟地址 0x00001004

        A[页号 高20位<br/>0x00001]

        B[页内偏移 低12位<br/>0x004]

    end

    A -->|查页表| C[物理页框号 0x3F]

    C --> D[物理地址 0x3F004]

    B --> D

翻译三步:① 取高20位 → 页号=1 ② 翻页表 → 页号1 → 物理页框号=0x3F ③ 拼起来 → 物理地址 = 0x3F004。偏移量从头到尾不变——变的只是”页号”和”框号”之间的对应关系。

类比

 
你去电影院:票上写 "3号厅 5排 4座"
 
3号厅 → 翻导览图 → 实际在2楼东侧
 
5排 4座 → 不变,进了厅里找就行
 
你手里的"3号厅"是虚拟的编号,实际那间房可能是"201室"。
 
但"5排4座"到了哪个厅都一样——不需要翻译,直接数过去。
 

页表里存了什么——不只是”翻译结果”


graph TD

    subgraph 页表项 PTE 结构

        F1[虚拟页号 0<br/>有效位 1<br/>物理页框 3F<br/>权限 rw-<br/>脏位 0<br/>访问位 1]

        F2[虚拟页号 1<br/>有效位 1<br/>物理页框 2A<br/>权限 r--<br/>脏位 1<br/>访问位 1]

        F3[虚拟页号 2<br/>有效位 0<br/>不在内存中]

        F4[虚拟页号 3<br/>有效位 1<br/>物理页框 7C<br/>权限 rwx<br/>脏位 0<br/>访问位 0]

    end

    F1 --> OK1[✅ 在内存]

    F2 --> OK2[✅ 在内存]

    F3 --> PF[❌ 缺页中断]

    F4 --> OK4[✅ 在内存]

🔴 一句话讲清:每个进程有自己独立的页表。CPU里有一个叫PTBR(页表基址寄存器)的寄存器,存着当前进程页表在物理内存中的起始地址。进程切换时OS只做一件事——把PTBR指向新进程的页表。从此所有地址翻译自动切到新进程的视角。

多级页表——“这本书的目录为什么只有几页”

 
先看一级大表的问题:
 
  32位系统,4KB一页 → 总共2^20=1,048,576页(约100万条)
 
  每条4字节 → 页表本身4MB。每个进程都占4MB → 100个进程=400MB!
 
  而且绝大多数页表项其实是空的(进程没用那么多地址空间)→ 纯纯浪费。
 
  64位更夸张——页表条目多到一页表根本放不下,必须分级。
 
解法:多级页表——只给"真正用到的区域"建表
 
  一级表(总目录):不是每个条目都存"答案",而是标记"下面有没有东西"
 
    标记为空 → 不需要二级表 → 省了整个二级表的内存
 
    标记有 → 指向一个二级表 → 二级表里才真正存物理框号
 
  书目录类比——100章的书,你只看第1章和第100章:
 
    传统单级 = 把100章的详细目录全印出来 → 100页目录
 
    多级     = 只印总目录(1页)+ 第1章详细目录(1页)+ 第100章详细目录(1页)
 
            = 3页目录 → 省了97页纸!
 

    graph TD

        subgraph 一级页表 页目录

            L0[0: 空 → 不建二级表]

            L1[1: ----]

            L2[2: 空]

            L3[3: ----]

            LE[...

1023: 空]

        end

        subgraph 二级表

            S1[3F<br/>2A<br/>...]

        end

        subgraph 二级表 3

            S3[7C<br/>...]

        end

        L1 --> S1

        L3 --> S3

进程实际只用到了虚拟地址空间里低地址(代码/数据)和高地址(栈)两端,

中间巨大的空白区域→一级表全标空→不建二级表→内存省下来了。

一句话讲清:“多级页表的核心——把稀疏的大表按需拆成树。进程虚拟地址空间大部分其实是空的,多级结构让空的地方不占页表空间。代价是多了一步查表——但TLB缓住了99%的翻译,多查一次几乎不发生。“

TLB——“为什么翻译这么快”

 
每次访问内存都要先翻页表:
 
  翻页表 = 一次额外的内存访问 = 几十到上百个CPU时钟周期
 
  如果每取一条指令、每读一个数据都要多访问一次内存 → 系统慢到没法用。
 
解法:把最常翻的那几条翻译结果,抄在CPU内部的一张"便签纸"上。
 
这张便签纸就是 TLB(Translation Lookaside Buffer)。
 
  工作方式:
 
    CPU要访问地址 → 先看便签TLB
 
      → 便签上有(命中)     → 直接拿到物理地址 → 快(1个时钟周期)
 
      → 便签上没有(未命中) → 翻页表大册子(内存里)→ 翻到后抄在便签上 → 下次就快了
 
类比——外卖小哥送餐:
 
  没有TLB:每接一单都得翻整张城市地图找地址
 
  有TLB:兜里揣张小本子,记最近10个客户的地址
 
    接单 → 翻小本子 → 有!直接出发                    ← 命中
 
    接单 → 翻小本子 → 没有 → 翻大地图 → 记小本子上 → 出发 ← 未命中
 
TLB通常才几十到几百条,但命中率能到99%以上。
 
为什么?局部性原理——程序短时间内反复访问同一片地址区域。
 
查过一次的页,后面连续命中,不用再翻大册子。
 

速记卡(面试闪卡)

Q1:一句话讲清「地址翻译从虚拟到物理」到底是什么?

A:地址翻译是 CPU 里的 MMU 凭页表把程序用的虚拟地址翻成内存条真实物理地址,对程序透明。

Q2:虚拟地址怎么切(page + offset) —— 怎么理解?

A:类比:虚拟地址像电影票:“3号厅”是页号(高几位),“5排4座”是页内偏移(低12位)。翻译只换厅号到真实房号,座位号从头到尾不动——变的只是页号↔框号的对应关系。(Page table)

Q3:页表里存了啥(PTE) —— 怎么理解?

A:类比:页表项(PTE)像每户档案卡:有效位说”在不在家”、物理页框号是真实门牌、权限位管能不能改、脏位记有无动过。有效位为0就触发缺页中断(page fault),OS 去磁盘把这一页搬回来。(Valid bit)

Q4:多级页表为什么省(multi-level) —— 怎么理解?

A:类比:单级页表像把100章书的详细目录全印出来——大部分空白也占纸。多级页表(multi-level)只印总目录+你真用到的几章详目,中间大片虚空不建二级表,内存立省九成,代价仅多查一层。(Sparse save)

Q5:TLB 为什么快(Translation Lookaside Buffer) —— 怎么理解?

A:类比:每次翻译都要翻内存里的页表大册子,太慢。TLB 是 CPU 兜里的小本子,抄最近翻过的几条翻译结果——命中就1个时钟周期拿到物理地址,像外卖小哥记熟老客户地址不用翻全城地图。(Cache hits)

Q6:核心速记主线有哪些?

  • 翻译三步:取页号→查页表得物理框号→拼偏移得物理地址,偏移不变

  • 页表项 PTE 含有效位/物理框号/权限/脏位,无效触发缺页中断

  • 多级页表按需建树,稀疏地址空间不占多余页表内存

  • PTBR 存当前进程页表基址,进程切换只换 PTBR

  • TLB 缓存热点翻译,靠局部性原理命中率 99%+

口诀

A:虚拟地址分两半,页号偏移不动;

页表翻译查框号,无效缺页中断;

多级页表省内存,稀疏不建二级;

TLB 小本抄答案,翻译快如闪电。

相关链接