在开发工作中,虽然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. 总结
总之,虚拟内存为我们解决了很多问题,也简化了简化了程序员的工作,是目前主流的内存管理方式。