操作系统

虚拟内存:程序看到的地址是怎么翻译的

每个程序都以为自己独占一大片内存,其实背后有一层翻译。

概况

以 Linux 上一个程序读取自己内存中的一个变量为例,只讲概念。

一句话总结
程序用虚拟地址,硬件按页表翻译成物理地址
好处
隔离、布局统一、物理内存可以不连续
页
Linux 在 x86-64 上通常以 4KB 为一页;大页(如 2MB)用来减少翻译开销
页表
每个进程一套,由内核维护
TLB
最近翻译结果的缓存
权限位
每页记着可读、可写、可执行、用户可访问,违反即触发异常
多级页表
x86-64 通常 4 级,只为用到的区域建表
写时复制
fork 后共享只读页,写时才复制
swap
不常用的页换到磁盘;频繁换页叫抖动

步骤 5 / 9

动手:把一个虚拟地址拆成 4 级索引

48 位虚拟地址 = 4 段 9 位索引 + 12 位页内偏移;0x7ffd1234abcd 拆出来是 255、500、145、330 和 0xbcd。

x86-64 的 4 级页表只用虚拟地址的低 48 位(更高的 16 位必须和第 47 位相同)。为什么是 9 和 12? 页大小 4KB = 2^12 字节,所以最低 12 位是页内偏移,翻译时原样保留。 每张页表本身也正好占一页:4096 字节 ÷ 每项 8 字节 = 512 项 = 2^9,所以每一级用 9 位当下标。12 + 9 × 4 = 48。 在右边输入地址 0x7ffd1234abcd(这是一个典型的栈地址,栈在用户空间顶部附近): 第 47–39 位 = 255,是第 1 级(PML4)的下标;第 38–30 位 = 500,第 2 级(PDPT);第 29–21 位 = 145,第 3 级(PD);第 20–12 位 = 330,第 4 级(PT),这一项里存着物理页号;最低 12 位 = 0xbcd,即页内第 3021 字节。 MMU 从 CR3 寄存器拿到第 1 级表的物理地址,用 255 找到下一级表,再用 500、145、330 依次往下,最后把物理页号和 0xbcd 拼起来就是物理地址。 可以试试:把地址加 1,只有偏移变化;加 0x1000(一页),第 4 级下标变成 331;换一个很小的地址如 0x400000(传统的程序代码起点),看前两级下标都是 0,和栈共用的表很少,这就是多级页表省空间的原因。

  • PML4 下标(47–39) 255
  • PDPT 下标(38–30) 500
  • PD 下标(29–21) 145
  • PT 下标(20–12) 330
  • 页内偏移(11–0) 0xbcd
事件日志(5)
当前状态发生了什么
用户态程序想读取一个变量
用户态地址落在“堆”区域
用户态查页表,得到物理地址
翻译中逐级查找页表
动手实验地址被拆成 4 个 9 位下标和 12 位偏移