本文的产生源自我对虚拟地址的两个问题:
1.虚拟内存到底真实存在吗?如果存在难道不用依附于硬件吗?
2.如果不存在它又是怎么把对应的数据传进物理内存的?
本文需要你了解一定关于虚拟内存的知识,如果你也有这样的疑问,希望本文能帮到你,同时如果有错误的地方希望指出。欢迎评论区讨论,我也将好的评论改进如文章,谢谢!
Linux 虚拟内存与地址转换
核心思想:
程序使用的是虚拟地址,真正访问 RAM 时使用的是物理地址。
CPU 通过 MMU + 页表完成虚拟地址到物理地址的转换。
一、虚拟地址是什么?
1. 虚拟地址 ≠ 物理地址
假设:
int a = 10;
printf("%p\n", &a);
输出:
0x7fffffffdc20
这个地址通常是:
虚拟地址
它并不是说:
RAM 上真的存在一个地址叫
0x7fffffffdc20。
而是:
对于当前进程来说,这个地址编号可以通过地址转换找到对应的物理内存。
可以类比成:
虚拟地址 = 房间编号
物理地址 = 房间真正所在的位置
二、虚拟地址到底“存不存在”?
这个问题非常重要。
应该区分:
| 概念 | 是否存在 |
|---|---|
0x12345678 这个数字 | ✅ 存在 |
| 虚拟地址这个概念 | ✅ 存在 |
| 虚拟地址对应的物理内存 | ❓ 不一定 |
| 有效的虚拟地址映射 | ❓ 不一定 |
| 物理地址 | 如果存在对应物理内存,则存在 |
所以更准确的说法不是:
“这个虚拟地址不存在。”
而是:
这个虚拟地址没有建立有效的映射。
三、虚拟地址为什么可以使用?
程序看到的是:
虚拟地址空间
例如:
0x00000000
↓
0x10000000 ─────→ 物理内存 A
↓
0x20000000 ─────→ 物理内存 C
↓
0x30000000 ─────→ 没有映射
↓
0x40000000 ─────→ 物理内存 B
注意:
0x30000000
这个地址编号本身可以存在。
但是它当前没有有效的物理内存映射。
如果程序访问:
*(int *)0x30000000
就可能产生:
Page Fault
如果操作系统发现这是非法访问,最终可能导致:
Segmentation Fault
四、CPU 如何访问内存?
程序:
int a = 10;
CPU 需要访问 a。
大致过程:
程序
↓
CPU
↓
虚拟地址
↓
MMU
↓
页表
↓
物理地址
↓
内存控制器
↓
RAM
所以:
虚拟地址不是直接去访问内存条。
而是:
CPU 使用虚拟地址发起内存访问,MMU 将虚拟地址转换成物理地址,然后内存系统根据物理地址完成真正的读写。
五、虚拟地址的组成
虚拟地址并不是简单地作为一个完整数字去查页表。
通常可以拆成:
虚拟地址 = 虚拟页号 + 页内偏移
例如:
虚拟地址
┌──────────────┬────────────┐
│ 虚拟页号 │ 页内偏移 │
└──────────────┴────────────┘
其中:
-
虚拟页号(VPN):用于寻找对应的物理页
-
页内偏移(Offset):表示数据在这一页中的具体位置
六、为什么页内偏移不变?
假设:
虚拟地址:
0x12345 | 0x678
↑ ↑
虚拟页号 页内偏移
页表找到:
虚拟页号 0x12345
↓
物理页号 0x8ABCD
那么:
虚拟地址:
0x12345 | 0x678
↓ 页表转换
物理地址:
0x8ABCD | 0x678
注意:
虚拟页号 → 物理页号
但是:
页内偏移不变
所以可以记成:
页表负责把“虚拟页”找到对应的“物理页”,页内偏移保持不变。
七、页(Page)是什么?
操作系统通常不会把每一个字节单独建立映射。
而是把内存划分成一个个固定大小的块。
虚拟内存中的块:
虚拟页(Page)
物理内存中的对应块:
物理页框(Page Frame)
例如常见的:
4 KiB
也就是:
4096 Bytes = 2^12 Bytes
因此,如果页大小是 4 KiB:
页内偏移 = 12 bit
八、页表是什么?
页表可以理解成:
记录虚拟页和物理页之间映射关系的数据结构。
简单理解:
虚拟页号 物理页号
0x12345 → 0x8ABCD
0x56789 → 0x12345
0xABCDE → 0x789AB
因此:
虚拟地址
↓
找到虚拟页号
↓
查页表
↓
得到物理页号
↓
加上页内偏移
↓
得到物理地址
九、谁负责地址转换?
负责虚拟地址转换的是:
MMU
全称:
Memory Management Unit
内存管理单元
可以把 MMU 理解成:
硬件地址翻译器。
大致过程:
C 程序
↓
CPU
↓
虚拟地址
↓
MMU
↓
查页表
↓
物理地址
↓
内存系统
注意:
C 程序本身不负责把虚拟地址转换成物理地址。
十、页表放在哪里?
页表本身也是数据。
因此:
页表本身也存放在物理内存中。
可以简单理解成:
物理内存
┌─────────────────────┐
│ 页表 │
├─────────────────────┤
│ 程序数据 │
├─────────────────────┤
│ 其他数据 │
├─────────────────────┤
│ 其他进程 │
└─────────────────────┘
CPU 需要知道:
当前进程使用的页表在哪里?
在 x86-64 中,会涉及:
CR3
可以把它理解成:
CPU 用来定位当前地址空间顶层页表的重要寄存器。
十一、为什么不能每次都查页表?
如果每次访问内存都:
CPU
↓
查页表
↓
得到物理地址
↓
访问内存
会增加访问成本。
所以 CPU 提供了:
TLB
全称:
Translation Lookaside Buffer
可以理解为:
页表映射结果的高速缓存。
例如:
虚拟页号 物理页号
0x12345 → 0x8ABCD
0x56789 → 0x12345
这些转换结果可以被缓存到 TLB。
十二、TLB 命中和未命中
CPU 访问:
虚拟地址
首先会尝试:
查 TLB
情况一:TLB 命中
CPU
↓
虚拟地址
↓
TLB
↓
命中
↓
直接得到物理页号
↓
物理地址
↓
访问内存
不需要重新进行完整的页表遍历。
情况二:TLB 未命中
CPU
↓
虚拟地址
↓
TLB
↓
未命中
↓
查多级页表
↓
得到物理页号
↓
更新 TLB
↓
访问物理内存
所以:
TLB = 页表映射结果的高速缓存
十三、现代系统为什么使用多级页表?
如果使用一张巨大的页表:
虚拟地址
↓
一张超级大的页表
↓
物理地址
会产生巨大的空间开销。
所以现代系统通常使用:
多级页表
例如 x86-64 常见的四级分页结构可以简化理解成:
虚拟地址
│
├── 第一级索引
│
├── 第二级索引
│
├── 第三级索引
│
├── 第四级索引
│
└── 页内偏移
大致过程:
虚拟地址
↓
PML4
↓
PDPT
↓
PD
↓
PT
↓
物理页
↓
+ 页内偏移
↓
物理地址
十四、可以把多级页表理解成“查字典”
例如:
虚拟地址
┌──────┬──────┬──────┬──────┐
│索引1 │索引2 │索引3 │偏移 │
└──────┴──────┴──────┴──────┘
↓ ↓ ↓
PML4 PDPT PD
↓
PT
↓
物理页
CPU 根据虚拟地址中的不同索引:
第一层 → 第二层 → 第三层 → 第四层
最终找到:
物理页号
然后加上:
页内偏移
得到:
物理地址
十五、找到物理地址以后怎么办?
地址转换完成以后:
虚拟地址
↓
MMU
↓
物理地址
↓
内存系统
↓
RAM
可以简单理解为:
CPU
│
│ 物理地址
↓
内存控制器
│
↓
内存总线 / 内存接口
│
↓
RAM
例如:
a = 20;
本质上是 CPU 发起一次写内存操作:
我要把数据 20
写入某个物理地址
十六、Page Fault(缺页异常)
如果 CPU 访问:
虚拟地址
但是页表发现:
当前没有有效的物理页映射
就可能产生:
Page Fault
过程:
CPU
↓
访问虚拟地址
↓
MMU
↓
发现页面没有有效映射
↓
Page Fault
↓
CPU 转入异常处理
↓
操作系统处理
操作系统根据具体情况决定怎么办。
十七、Page Fault 不一定意味着程序出错
这是一个非常容易混淆的地方。
Page Fault:
不等于一定发生错误。
例如某个页面暂时不在 RAM 中,但它属于合法的虚拟内存区域。
操作系统可能:
磁盘 / SSD
↓
读取页面
↓
放入物理内存
↓
建立页表映射
↓
重新执行相关指令
↓
程序继续运行
但是,如果访问的是一个完全非法的地址:
非法虚拟地址
↓
Page Fault
↓
操作系统发现非法访问
↓
终止进程
↓
Segmentation Fault
所以:
Page Fault
≠
一定是错误
十八、完整的内存访问过程
假设:
int a = 10;
a = 20;
CPU 需要修改 a。
完整过程可以理解成:
C 程序
│
↓
变量 a
│
↓
虚拟地址
0x12345678
│
↓
TLB
┌─────┴─────┐
│ │
命中 未命中
│ │
│ ↓
│ 多级页表
│ │
│ ↓
│ 物理页号
│ │
└─────┬─────┘
↓
物理地址
0x8ABCD678
│
↓
内存系统
│
↓
RAM
│
↓
数据变成 20
十九、一个最重要的概念:进程拥有自己的虚拟地址空间
不同进程可以拥有相同的虚拟地址。
例如:
进程 A:
虚拟地址 0x12345678
↓
物理页 A
而:
进程 B:
虚拟地址 0x12345678
↓
物理页 B
虽然:
虚拟地址一样
但是:
页表不同
因此最终可能对应:
不同的物理内存
这就是为什么每个进程都可以拥有自己的:
虚拟地址空间
二十、指针和虚拟地址
例如:
int a = 10;
int *p = &a;
可以简单理解成:
a
┌──────────────┐
│ 10 │
└──────────────┘
↑
│
│ &a
│
p
┌──────────────┐
│ 虚拟地址值 │
└──────────────┘
所以:
p
保存的是一个地址值。
而:
*p
表示:
访问这个地址所对应的对象。
CPU 最终仍然需要经过:
虚拟地址
↓
MMU
↓
页表 / TLB
↓
物理地址
↓
RAM
二十一、几个概念一定不要混
1. 虚拟地址
程序使用的地址
2. 物理地址
用于定位物理内存的地址
3. 页
虚拟地址空间中的固定大小内存块
4. 物理页框
物理内存中的固定大小内存块
5. 页表
记录虚拟页 → 物理页映射关系的数据结构
6. MMU
负责地址转换的硬件
7. TLB
缓存虚拟页 → 物理页转换结果的高速缓存
8. Page Fault
访问虚拟页面时发生的一种异常
不一定意味着程序错误。
9. Segmentation Fault
程序进行了非法的内存访问
操作系统通常会终止该进程。
二十二、最终一定要记住的模型
把下面这张图记住,虚拟内存的主线基本就通了:
程序
│
↓
虚拟地址
│
↓
CPU
│
↓
TLB
┌─────┴─────┐
│ │
命中 未命中
│ │
│ ↓
│ 多级页表
│ │
│ ↓
│ 物理页号
│ │
└─────┬─────┘
↓
物理地址
│
↓
内存系统
│
↓
RAM
如果页面没有有效映射:
虚拟地址
↓
TLB / 页表
↓
Page Fault
↓
操作系统
↓
合法访问?
↙ ↘
是 否
↓ ↓
处理 非法访问
↓ ↓
继续 Segmentation Fault
二十三、一句话总结
程序使用虚拟地址,MMU 通过 TLB 和多级页表把虚拟页转换成物理页,页内偏移保持不变,最终形成物理地址,再由内存系统访问 RAM。
整个链条:
虚拟地址
↓
TLB
↓
多级页表
↓
物理页号
+
页内偏移
↓
物理地址
↓
内存系统
↓
RAM
二十四、接下来学习的知识路线
建议按照这个顺序继续学习:
虚拟地址
↓
虚拟页 / 物理页
↓
页表
↓
多级页表
↓
TLB
↓
Page Fault
↓
进程虚拟地址空间
↓
malloc()
↓
fork()
↓
写时复制 COW
↓
mmap()
其中最值得重点理解的是:
页表 → 多级页表 → TLB → Page Fault → fork + COW
这条线一旦打通,Linux 的进程、内存管理、malloc、fork、COW会变得非常容易串起来。
二十五. 虚拟内存 = 软件 + 硬件(回答第二个问题)
可以画成:
虚拟内存
│
┌─────────┴─────────┐
│ │
操作系统 硬件
│ │
管理页表 MMU
管理地址空间 TLB
处理Page Fault 地址转换
│ │
└─────────┬─────────┘
↓
RAM
所以你的问题可以直接回答:
虚拟内存是真实存在的吗?
是。
但它真实存在的是一种计算机系统机制和地址空间抽象,不是一块独立的硬件。
它需要依附硬件吗?
需要。
现代 CPU 必须提供类似 MMU、TLB、分页机制这样的硬件支持,操作系统再利用这些硬件建立虚拟内存系统。
最后把三个东西彻底分开
物理内存
= 真正的 RAM 硬件
虚拟地址空间
= 进程看到的地址空间
虚拟内存
= 利用 CPU 硬件 + 操作系统
把虚拟地址空间和物理内存联系起来的机制
所以你现在如果看到:
“虚拟内存”
脑子里千万不要想象成:
另一块隐藏的内存条 ❌
应该想象成:
程序
↓
虚拟地址空间
↓
页表
↓
MMU/TLB
↓
物理内存
这才是虚拟内存。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2502_93625125/article/details/166688292



