在开发工作中,虽然CPU,内存和硬盘都是必不可少的硬件,不过,编程中,我们常常受到困扰的往往是内存相关的
bug
(编程中遇到CPU和硬盘相关的
bug
极少)。
这是因为我们的程序和数据虽然是存放在硬盘上的,但是运行时,CPU并不是直接从硬盘加载程序和数据的。
直接从硬盘读取指令非常慢,会成为整个系统的严重瓶颈,因此,程序及其数据首先被复制到内存(比硬盘驱动器小,但速度快得多)中,CPU从内存读取指令速度会快很多。
内存可以看作是一长串单元,每个单元都包含一些二进制数据,并标有一个称为存储器地址的数字。
内存地址
的范围从0到N,取决于系统中可用的主内存量。
程序使用的地址范围称为
地址空间
。
如下图,两个加载到
内存空间
中的程序
Program-1
和
Program-2
,
它们分别占用了
内存地址
0~2
和
5~8
的位置。
在操作系统的早期,程序可以直接访问整个主存储器,如何管理内存是程序员的工作之一。
当时编写软件的一大挑战性就在于开发人员需要设计一种管理
RAM
访问的好方法,并确保整个程序不会溢出可用内存。
后来,随着多任务处理的出现,当多个程序可以在同一台计算机上运行时,内存管理变得越来越棘手。
程序员不得不面对自己管理内存带来的主要问题:
RAM
Program-2
因此,对于20世纪60年代早期的硬件架构师来说,急需一种自动化的内存管理形式,这样可以显著简化编程并解决更关键的内存保护问题。
最后,他们想出了今天被称为
虚拟内存
的东西。
在
虚拟内存
中,程序不能直接访问物理RAM。相反,它与一个名为
虚拟内存
的空间交互。
操作系统与CPU一起提供这样的虚拟地址空间,并迟早将其转换为物理地址空间。
每个内存访问都是通过一个虚拟地址来执行的,该地址并不指向内存中的实际物理位置。
程序总是读取或写入虚拟地址,它完全不知道底层硬件中发生了什么。
比如,仍然是上面的
Program-1
和
Program-2
,对于这两个程序来说,开发人员可以假定它们的地址都是从
0
开始。
而它们实际在物理内存中的位置开发人员不用关心,交给操作系统来负责就可以了。
从上面的图中,我们可以看出虚拟内存的明显好处:
虚拟内存机制
需要一个位置来存储虚拟地址和物理地址之间的映射。
也就是说,给定虚拟地址
X
,系统必须能够找到对应的物理地址
Y
。
但是,不能将这样的信息保存为
1:1
关系,否则就需要一个与整个物理内存一样大的虚拟地址库。
现代虚拟内存实现通过将
虚拟内存
和
物理内存
解释为一长串固定大小的小块来克服这个问题(以及许多其他问题)。
虚拟内存
中将这个块称为
页
,
物理内存
中将这个块称为
帧
。
在CPU中有一个硬件组件叫做内存管理单元(
MMU
),它将
页
和
帧
之间的映射信息存储在一个称为
页表
的特殊数据结构中。
页表
中每一行都包含一个
页
索引及其对应的
帧
索引,每个正在运行的程序在
MMU
中都有一个自己的页表,
如下图所示:
程序
Program-1
占用
3个
内存页,编号为
0~2
,通过
MMU
页表映射到物理内存中帧
3,4,8
。
虚拟内存的虚拟地址由两部分组成:
当程序访问当前未映射到物理帧的虚拟地址时,会发生
页面错误
(
page faults
)。
更具体地说,当页面存在于程序的页面表中,但指向物理内存中不存在或尚未可用的帧时,就会发生
页面错误
。
比如:
MMU
检测到页面错误会将消息反馈到操作系统,操作系统将尽最大努力在物理内存中找到用于映射的帧。
大多数情况下,这是一个简单的操作,除非系统内存不足。
分页
(
paging
)是另一个内存管理技巧:操作系统将一些页面移动到硬盘驱动器,以便在没有更多物理内存可用时为其他程序或数据腾出空间。
分页
有时也被称为
交换
(
swapping
),
交换
是将整个进程移动到磁盘上。
分页
给程序一种无限可用内存的错觉,操作系统乐观地允许虚拟内存地址空间大于物理内存地址空间,知道数据可以在需要时移入和移出硬盘驱动器。
有些系统(如Windows)使用一个特殊的文件,称为分页文件。其他操作系统(例如Linux)有一个称为交换区域的专用硬盘分区。
不过,需要注意的是,硬盘驱动器比主内存慢得多。
因此,当发生页面错误并且页面临时移动到硬盘驱动器时,操作系统必须从缓慢的介质中读取数据并将其移回内存,从而导致延迟。
总而言之,更少的分页意味着系统可以更有效地运行。
当系统在分页上花费的时间多于运行应用程序的时间时,就会发生抖动,这是由不断的页面错误流触发的。
这是一种极端的情况,比如你运行了太多的程序,占用了整个内存以及在硬盘上的分页区域,
这时就容易发生页面错误,操作系统为了跟上大量的页面错误请求,不断地在硬盘驱动器和物理内存之间移动数据,使系统陷入停顿。
解决这个问题可以通过增加内存的容量,或者减少正在运行的程序的数量,或再次通过调整交换文件的大小来避免抖动。
虚拟内存还提供了跨运行应用程序的安全性,比如你的浏览器无法窥视你的文本编辑器的虚拟内存,反之亦然。
内存保护的主要目的是防止进程访问不属于它的内存。
内存保护机制通常由
MMU
及其管理的页表提供。当一个程序试图访问一部分它不拥有的虚拟内存时,就会触发一个无效的页面错误。
MMU
和操作系统捕获信号并引发故障条件,称为分段错误(就是耳熟能详的
segmentation fault
),操作系统通常会终止程序作为响应。
总之,虚拟内存为我们解决了很多问题,也简化了简化了程序员的工作,是目前主流的内存管理方式。