一个系统中的进程是与其他进程共享CPU和主存资源的。然而,共享主存会形成一些特殊的挑战。随着对CPU需求的增长,进程以某种合理的平滑方式慢了下来。但是如果太多的进程需要太多的内存,那么它们中的一些就根本无法运行。当一个程序没有空间可用时,那就是它运气不好了。内存还很容易被破坏。如果某个进程不小心写了另一个进程使用的内存,它就可能以某种完全和程序逻辑无关的令人迷惑的方式失败。
为了更加有效地管理内存并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟内存(VM)。虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。通过一个很清晰的机制,虚拟内存提供了三个重要的能力:
1)它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。
2)它为每个进程提供了一致的地址空间,从而简化了内存管理。
3)它保护了每个进程的地址空间不被其他进程破坏。
物理地址和虚拟地址
计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每字节
都有一个唯一的物理地址(Physical Address,PA)。第一个字节的地址为0,接下来的字节地址为1,再下一个为2,依此类推。给定这种简单的结构,CPU访问内存的最自然的方式就是使用物理地址。我们把这种方式称为物理寻址(physicaladdressing)。
如图:

而现在大多数系统使用的是虚拟地址来访问主存,CPU通过生成一个虚拟地址(Virtual Address,VA)来访问主存,这个虚拟地址在被送到内存之前先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫做地址翻译(address translation)。

地址空间
地址空间(Address Space)是计算机系统中一个核心的抽象概念,它定义了程序可以访问的内存地址范围。在现代操作系统中,每个进程都拥有自己独立的地址空间,这为进程提供了内存隔离和保护。
1. 地址空间的基本概念
地址空间可以分为两种主要类型:
- 物理地址空间:对应实际物理内存(RAM)的地址范围。物理地址是内存控制器直接使用的地址,用于访问内存芯片中的特定存储单元。
- 虚拟地址空间:每个进程看到的、独立的地址范围。虚拟地址由CPU生成,需要通过内存管理单元(MMU)转换为物理地址后才能访问实际内存。
2. 虚拟地址空间的特点
虚拟地址空间为每个进程提供了以下重要特性:
- 独立性:每个进程都认为自己独占整个地址空间(通常是4GB或更大),与其他进程的地址空间完全隔离。
- 连续性:虚拟地址空间是连续的线性地址范围,从0开始向上扩展,简化了程序的地址计算。
- 保护性:操作系统通过页表等机制,确保一个进程无法访问其他进程的地址空间,防止恶意或错误的访问。
- 灵活性:虚拟内存可以比物理内存大得多,通过分页和交换技术,可以将暂时不用的数据存储到磁盘上。
3. 地址空间的组成
典型的进程虚拟地址空间通常包含以下几个主要区域:
- 代码段(Text Segment):存放可执行代码,通常是只读的。
- 数据段(Data Segment):包含已初始化的全局变量和静态变量。
- BSS段(BSS Segment):存放未初始化的全局变量和静态变量。
- 堆(Heap):动态分配的内存区域,向高地址方向增长。
- 栈(Stack):用于函数调用、局部变量等,向低地址方向增长。
- 共享库区域:映射共享库代码和数据。
- 内核空间:操作系统内核代码和数据,通常位于地址空间的高端。
4. 地址翻译过程
当CPU生成一个虚拟地址时,地址翻译过程如下:
- CPU将虚拟地址发送给内存管理单元(MMU)。
- MMU查询页表(Page Table),找到对应的物理页框号。
- 如果页表中没有对应条目(页错误),则触发缺页异常,由操作系统处理。
- MMU将物理页框号与页内偏移组合,得到物理地址。
- 物理地址被发送到内存总线,访问实际内存。
5. 地址空间的实际意义
地址空间的抽象带来了多重好处:
- 简化编程:程序员无需关心物理内存的分配和布局,只需在虚拟地址空间中编写程序。
- 内存保护:防止进程间的非法内存访问,提高系统稳定性。
- 内存共享:多个进程可以共享相同的代码段或数据段(如共享库),节省内存。
- 内存扩展:通过虚拟内存技术,程序可以使用比物理内存更大的地址空间。
- 简化加载:可执行文件可以加载到虚拟地址空间的任意位置,无需重定位。
虚拟页表
概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。VM系统通过将虚拟内存分割为称为虚拟页(Virtual Page,VP)的大小固定的块来处理这个问题。每个虚拟页的大小为P=2字节。类似地,物理内存被分割为物理页(Physical Page,PP),大小也为P字节(物理页也被称为页帧(page frame))。
在任意时刻,虚拟页面的集合都分为三个不相交的子集:
●未分配的:VM系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相
关联,因此也就不占用任何磁盘空间。
●缓存的:当前已缓存在物理内存中的已分配页。
●未缓存的:未缓存在物理内存中的已分配页。
可以通过图来理解他们之间的关系:

以malloc为例:操作系统是这样分配地址空间的

页表
同任何缓存一样,虚拟内存系统必须有某种方法来判定一个虚拟页是否缓存在DRAM中的某个地方。如果是,系统还必须确定这个虚拟页存放在哪个物理页中。如果不命中(缺页),系统必须判断这个虚拟页存放在磁盘的哪个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到DRAM中,替换这个牺牲页。

抖动
抖动(Thrashing)是虚拟内存系统中一种严重的性能问题,指系统花费大量时间在页面置换(换入换出)上,而实际用于执行进程的时间却很少,导致系统整体性能急剧下降。
1. 抖动的产生原因
抖动通常发生在以下情况:
- 内存过载:系统中运行的进程过多,它们的工作集(Working Set)总和超过了可用物理内存容量。
- 页面置换频繁:操作系统频繁地在内存和磁盘之间交换页面,导致CPU大部分时间都在等待I/O操作完成。
- 局部性原理失效:进程频繁访问的页面无法同时驻留在内存中,导致缺页率异常升高。
2. 抖动的工作机制
当系统发生抖动时,会出现以下恶性循环:
- 进程A发生缺页,操作系统需要为其调入所需页面。
- 由于物理内存已满,操作系统必须选择一个牺牲页(通常是进程B的页面)换出到磁盘。
- 不久后,进程B需要访问刚被换出的页面,又发生缺页。
- 操作系统又需要换出另一个进程(可能是进程A)的页面来为进程B腾出空间。
- 这种频繁的页面换入换出导致磁盘I/O负载极高,CPU利用率却很低。
3. 抖动的表现特征
- CPU利用率骤降:CPU大部分时间处于空闲状态,等待I/O完成。
- 磁盘I/O异常高:磁盘活动指示灯持续闪烁,I/O队列长度增加。
- 系统响应缓慢:用户界面卡顿,命令执行延迟明显。
- 缺页率急剧上升:每秒缺页次数远超正常水平。
- 吞吐量下降:单位时间内完成的进程数量减少。
4. 预防和缓解抖动的策略
操作系统采用多种策略来预防和缓解抖动:
4.1 工作集模型
工作集是进程在最近一段时间内访问的页面集合。操作系统通过监控每个进程的工作集大小,确保分配给进程的物理页框数不小于其工作集大小,从而减少缺页。
4.2 页面置换算法优化
- 局部置换算法:每个进程只能从自己的页面中选择牺牲页,避免影响其他进程。
- 工作集时钟算法:结合工作集概念和时钟算法的优点,更智能地选择置换页面。
- 页面老化算法:根据页面访问频率和最近访问时间动态调整页面优先级。
4.3 负载控制
- 准入控制:当系统内存压力大时,暂停创建新进程或延迟进程调度。
- 交换出进程:将整个进程换出到磁盘,释放其占用的所有物理内存。
- 调整多道程序度:动态调整系统中同时运行的进程数量。
4.4 内存分配策略
- 均衡分配:在所有进程间平均分配物理页框。
- 按优先级分配:为高优先级进程分配更多物理内存。
- 按工作集分配:根据进程的工作集大小动态调整分配。
5. 实际系统中的抖动处理
现代操作系统采用综合策略处理抖动:
- Linux:使用页面回收机制(kswapd)、内存压缩(zswap/zram)和内存控制组(cgroups)来管理内存压力。
- Windows:采用工作集修剪、内存优先级和超级预取等技术优化内存管理。
- 实时系统:通过静态内存分配和确定性调度避免抖动。
6. 开发者的应对策略
应用程序开发者也可以采取措施减少抖动的发生:
- 优化内存使用:减少内存泄漏,及时释放不再使用的内存。
- 改善局部性:优化数据结构和算法,提高空间和时间局部性。
- 使用内存池:对于频繁分配释放的小对象,使用内存池减少碎片。
- 监控内存使用:使用性能分析工具监控应用程序的内存使用模式。