这是“操作系统系列教程”的第十五篇。前面几篇我们讲了进程、线程、调度、同步和死锁,这些都是处理器管理的内容。从这一篇开始,我们进入操作系统另一个核心模块——内存管理。而虚拟内存,是内存管理中最重要、也最精彩的概念。

一个反直觉的现象

先来想一个奇怪的问题。

假设你的电脑只有 8GB 物理内存,但你同时打开了浏览器(占用约 2GB)、IDE(占用约 2GB)、音乐播放器(占用约 500MB)、微信(占用约 1GB)、Photoshop(占用约 3GB),再加上操作系统本身占用的 2GB。

加起来已经超过了 8GB。但电脑并没有崩溃,程序依然能正常运行。这是为什么?

再想一个更极端的例子:你在 Linux 服务器上运行一个需要 10GB 内存的程序,但服务器只有 4GB 物理内存。程序依然能跑起来,只是可能慢一点。

这看起来违背常识:物理内存不够,程序为什么还能运行?

答案就是——虚拟内存(Virtual Memory)。

虚拟内存是操作系统最精妙的设计之一。它让每个进程都以为自己拥有独立、连续、足够大的内存空间,而实际上,这些空间可能并不连续,甚至部分数据根本不在物理内存中。

这篇文章,我们就来把“虚拟内存”这件事讲清楚。

说明:本文涉及地址空间、分页、页表、缺页等概念。初学者如果遇到暂时看不懂的术语,可以先跳过细节,重点理解“虚拟内存让每个进程以为自己独占内存,实际上由操作系统在物理内存和磁盘之间调度”这一核心思想。

一、没有虚拟内存会怎样?

1.1 直接使用物理内存的问题

在早期的计算机中,程序直接使用物理内存地址。这种方式存在几个严重问题:

问题一:地址冲突

如果两个程序都使用物理地址 0x1000,就会互相覆盖。程序 A 写入的数据,可能被程序 B 覆盖,导致程序 A 崩溃。

问题二:内存不足

如果物理内存只有 4GB,而程序需要 6GB,程序就无法运行。程序员必须手动管理内存,把数据分成小块,用到时加载,不用时写回磁盘。这非常繁琐,容易出错。

问题三:安全性差

程序可以直接访问任何物理内存地址,包括其他程序的数据和操作系统的数据。一个恶意程序可以轻易读取或修改其他程序的内容。

问题四:编程困难

程序员必须知道数据在物理内存中的具体位置,无法使用统一的地址空间。程序的可移植性极差。

直接使用物理内存

地址冲突

内存不足

安全性差

编程困难

图 1:直接使用物理内存的四大问题。 地址冲突、内存不足、安全性差、编程困难。

1.2 一个直观的例子

假设物理内存只有 4GB,程序 A 需要 3GB,程序 B 需要 3GB。如果直接使用物理内存,两个程序无法同时运行,因为它们需要 6GB,超过了 4GB。

但如果你观察实际系统,两个 3GB 的程序可以同时运行。这是因为它们并不总是同时使用全部 3GB——程序 A 可能正在等待用户输入,程序 B 可能正在等待网络响应。虚拟内存让操作系统可以在它们之间灵活调度内存。

在这里插入图片描述

图 2:虚拟内存让多个程序共享有限的物理内存。 不活跃的数据被换出到磁盘,需要时再换入。

二、虚拟内存到底是什么?

2.1 从生活场景理解

我们可以用“图书馆”来类比虚拟内存。

图书馆场景操作系统
读者进程
书架上的书物理内存
图书馆总藏书虚拟内存
借书证上的编号虚拟地址
图书管理员内存管理单元(MMU)
书库(不对外开放)磁盘

读者想借书时,只需要知道书的编号(虚拟地址),不需要知道书具体在哪个书架(物理地址)。如果书在书架上(物理内存中),管理员直接拿给读者;如果书在书库里(磁盘上),管理员先去取来,再交给读者。

读者感觉自己可以借到图书馆的所有书(虚拟内存),但实际上书架(物理内存)只能放下一部分。

2.2 虚拟内存的定义

给出一个正式的定义:

虚拟内存(Virtual Memory)是操作系统提供的一种内存管理机制,它为每个进程提供一个独立、连续、远大于物理内存的虚拟地址空间,并通过地址映射和页面置换,在物理内存和磁盘之间动态调度数据。

这个定义里有四个关键点:

  1. 独立:每个进程有自己独立的虚拟地址空间,互不干扰;
  2. 连续:虚拟地址空间是连续的,程序员不需要关心物理内存的碎片;
  3. 远大于物理内存:虚拟地址空间可以比物理内存大得多;
  4. 动态调度:操作系统在物理内存和磁盘之间动态换入换出数据。

2.3 虚拟内存的核心思想

虚拟内存的核心思想可以概括为三句话:

  • 每个进程都以为自己独占内存;
  • 虚拟地址和物理地址分离;
  • 只有真正用到的数据才加载到物理内存。

虚拟内存核心思想

每个进程以为自己独占内存

虚拟地址与物理地址分离

按需加载, 局部性原理

图 3:虚拟内存的核心思想。 独立、分离、按需加载。

三、虚拟地址与物理地址

理解虚拟内存,首先要理解两种地址:虚拟地址和物理地址。

3.1 两种地址的定义

地址类型定义谁使用
虚拟地址程序使用的地址,也叫逻辑地址程序员、编译器、应用程序
物理地址内存单元的实际地址内存管理单元、硬件

程序员写代码时,使用的是虚拟地址。CPU 执行指令时,先把虚拟地址转换为物理地址,再访问内存。

3.2 地址转换过程

物理内存页表内存管理单元CPU物理内存页表内存管理单元CPU发出虚拟地址查询虚拟地址对应的页表项返回物理页框号拼接物理页框号和页内偏移访问物理地址返回数据

图 4:地址转换过程。 CPU 发出虚拟地址,MMU 通过页表转换为物理地址。

3.3 一个具体的例子

假设虚拟地址是 32 位,页大小为 4KB。那么:

  • 虚拟地址的低 12 位(0~11)是页内偏移;
  • 虚拟地址的高 20 位(12~31)是虚拟页号。

地址转换过程:

  1. 取出虚拟页号(高 20 位);
  2. 在页表中查找虚拟页号对应的页表项;
  3. 页表项中存储着物理页框号(如 20 位);
  4. 把物理页框号和页内偏移拼接,得到物理地址。

例如:

  • 虚拟地址:0x00003A5C;
  • 虚拟页号:0x00003;
  • 页内偏移:0xA5C;
  • 页表项查到物理页框号为 0x00012;
  • 物理地址:0x00012A5C。

虚拟地址
0x00003A5C

虚拟页号
0x00003

页内偏移
0xA5C

页表查找

物理页框号
0x00012

物理地址
0x00012A5C

图 5:虚拟地址到物理地址的转换。 虚拟页号通过页表映射为物理页框号,加上页内偏移得到物理地址。

四、分页机制

虚拟内存的实现依赖分页机制。分页把虚拟地址空间和物理内存都划分为固定大小的块,分别称为页和页框。

4.1 页和页框

概念位置大小说明
页(Page)虚拟地址空间4KB(常见)虚拟内存的划分单位
页框(Page Frame)物理内存4KB(常见)物理内存的划分单位

页和页框大小相同,这样虚拟页才能映射到物理页框。

4.2 页表

页表(Page Table)是存储虚拟页到物理页框映射关系的数据结构。每个进程有自己的页表。

页表项通常包含:

字段说明
物理页框号对应的物理页框
有效位该页是否在物理内存中
访问位该页是否被访问过
修改位该页是否被修改过
保护位读、写、执行权限

物理内存

虚拟地址空间

虚拟页 0

虚拟页 1

虚拟页 2

虚拟页 3

页表

物理页框 5

物理页框 2

物理页框 8

图 6:页表实现虚拟页到物理页框的映射。 多个虚拟页可以映射到不连续的物理页框。

4.3 多级页表

如果虚拟地址空间很大(如 64 位系统),页表会非常大。例如,48 位虚拟地址、4KB 页大小,页表有 2^36 个页表项,每个页表项 8 字节,总共需要 512GB 来存储页表。这显然不可行。

解决方案是多级页表。把页表本身也分页,只加载用到的部分。

以二级页表为例:

  • 顶级页表(页目录)指向二级页表;
  • 二级页表指向物理页框。

虚拟地址

页目录索引

页表索引

页内偏移

页目录

二级页表

物理页框

图 7:二级页表结构。 通过两级索引减少页表占用的内存。

4.4 快表(TLB)

每次访问内存都要查询页表,会降低性能。为了解决这个问题,CPU 引入了快表(Translation Lookaside Buffer,TLB),也叫页表缓存。

TLB 是一个小容量的高速缓存,存储最近使用的页表项。CPU 先查 TLB,如果命中,直接得到物理页框号;如果不命中,再查页表。

命中

未命中

CPU 发出虚拟地址

TLB 是否命中

直接得到物理页框号

查询页表

更新 TLB

访问物理内存

图 8:TLB 加速地址转换。 大部分地址转换都能在 TLB 中命中。

五、缺页中断与页面置换

虚拟内存的关键机制是:只有真正用到的页才加载到物理内存。当程序访问一个不在物理内存中的页时,就会触发缺页中断(Page Fault)。

5.1 缺页中断的处理流程

磁盘内核MMUCPU磁盘内核MMUCPU访问虚拟地址查询页表,发现有效位为 0触发缺页中断检查地址是否合法从磁盘读取缺失的页返回页数据把页放入空闲页框更新页表重新执行触发缺页的指令再次访问虚拟地址地址转换成功

图 9:缺页中断的处理流程。 从磁盘加载缺失的页,更新页表,重新执行指令。

5.2 页面置换算法

如果物理内存已满,需要置换(换出)一些页,腾出空间给新页。选择换出哪个页,就是页面置换算法要解决的问题。

常见的页面置换算法:

算法原理优缺点
FIFO先进先出简单,但可能置换常用页
LRU最近最少使用效果好,但实现开销大
Clock近似 LRU折中方案,实际常用
OPT最优置换理论最优,无法实现

页面置换算法

FIFO
先进先出

LRU
最近最少使用

Clock
时钟算法

OPT
最优置换

图 10:常见的页面置换算法。

5.3 LRU 算法示例

假设物理内存有 3 个页框,访问序列为:1, 2, 3, 4, 1, 2, 5, 1, 2, 3。

LRU 置换过程:

访问页框状态缺页
11是
21, 2是
31, 2, 3是
44, 2, 3(换出 1)是
14, 1, 3(换出 2)是
24, 1, 2(换出 3)是
55, 1, 2(换出 4)是
15, 1, 2(命中)否
25, 1, 2(命中)否
35, 3, 2(换出 1)是

总共缺页 8 次。

访问序列

1

2

3

4

1

2

5

1

2

3

图 11:LRU 页面置换示例。 访问序列和缺页情况。

六、虚拟内存的优势

6.1 突破物理内存限制

虚拟内存让程序可以使用比物理内存更大的地址空间。不活跃的页被换出到磁盘,需要时再换入。这就是为什么 8GB 内存能跑几十 GB 的程序。

6.2 地址空间隔离

每个进程有独立的虚拟地址空间,进程之间不能直接访问对方的内存。这提高了系统的安全性。

6.3 简化编程

程序员不需要关心物理内存的分配和碎片,只需要使用虚拟地址。编译器、链接器都可以基于虚拟地址工作。

6.4 内存共享

多个进程可以共享同一块物理内存,例如共享库。多个进程的虚拟页可以映射到同一个物理页框。

虚拟内存优势

突破物理内存限制

地址空间隔离

简化编程

内存共享

图 12:虚拟内存的四大优势。

七、虚拟内存的代价

虚拟内存不是免费的,它也有代价。

7.1 地址转换开销

每次访问内存都要进行地址转换。虽然有 TLB 加速,但仍然有开销。

7.2 缺页中断开销

缺页中断需要访问磁盘,磁盘速度比内存慢几个数量级。频繁的缺页中断会导致系统性能急剧下降。

7.3 页表占用内存

页表本身占用内存。多级页表减少了占用,但仍然需要一定的空间。

7.4 抖动

如果物理内存严重不足,系统会频繁换入换出,导致 CPU 大部分时间花在换页上,而不是执行程序。这种现象称为抖动(Thrashing)。

在这里插入图片描述

图 13:虚拟内存的代价。 抖动是最严重的性能问题。

八、虚拟内存的实际应用

8.1 Linux 中的虚拟内存

Linux 使用分页机制实现虚拟内存。每个进程有独立的页表,内核维护物理页框的分配和回收。

常用命令:

# 查看内存使用情况
free -h

# 查看虚拟内存统计
vmstat

# 查看进程内存映射
pmap PID

8.2 Windows 中的虚拟内存

Windows 使用页面文件(Page File)实现虚拟内存。页面文件通常位于 C:\pagefile.sys,大小可以配置。

8.3 交换空间

Linux 使用交换空间(Swap)作为虚拟内存的磁盘部分。交换空间可以是分区,也可以是文件。

# 查看交换空间
swapon --show

# 创建交换文件
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

虚拟内存实现

Linux: 分页 + Swap

Windows: 分页 + 页面文件

macOS: 分页 + 压缩内存

图 14:不同操作系统的虚拟内存实现。

九、常见误区澄清

误区真相
“虚拟内存就是磁盘上的交换文件”虚拟内存是地址空间抽象,交换文件只是它的磁盘部分
“虚拟内存越大越好”交换空间过大会导致频繁换页,性能下降
“有了虚拟内存就不需要物理内存”物理内存仍然是性能的关键,虚拟内存只是补充
“虚拟地址就是物理地址”两者完全不同,通过页表映射
“页越大越好”页越大,内部碎片越多;页越小,页表越大
“缺页中断就是错误”缺页中断是正常机制,不是错误
“虚拟内存只用于内存不足时”虚拟内存始终在工作,即使内存充足
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐