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