>_<


文章目录

  • 引子
  • 历史的弃子——从覆盖技术说起
  • 化固为动——动态分区分配
  • 从连续到离散——分页、分段、段页式
  • 被欺骗的眼睛——虚拟内存



 


 

引子

在讨论「如何」提高内存利用率之前,先问一句「为什么」要提高内存利用率

很简单,因为不够用。

通常意义上的内存即RAM,其价格必然远远高于磁盘。现如今,内存条单条128G都可以买的到(贫穷不仅限制了我的想象力,更限制了我的内存条容量);可时间倒推几十年,那时的人们在内存上可谓省吃俭用。

在学习计算机组成原理的时候,我们就有这样的意识:CPU的与外部在速度上是有着巨大矛盾的。
而较高的内存利用率,使得其装得下更多的作业,也能够保证CPU的忙碌。

 
 
 

历史的弃子——从覆盖技术说起

这是在内存空间的分配中,曾经盛行的一种思路。

计算机发展早期,人们难免需要思考这样一个问题:一个程序太大了,内存装不下怎么办?

其实也不难想到,既然内存装不下,那就将程序段分多次装入,新装入的覆盖掉原先的。

这其实就是覆盖技术的基本思想。具体的实现是:将内存分为固定区+覆盖区,固定区中放入最活跃的程序段,而一些不常用且不可能被同时访问的程序段就放在覆盖段。

在起初,这种思想在内存空间的分配中应用十分广泛。比如在单道程序阶段的单一连续分配,在多道程序阶段的固定分区分配
 
如今我们这样评价覆盖技术:

  • 优点:无论是单一连续分配(只有一个分区)还是固定分区分配(多个分区提前固定好),都不会产生外部碎片
  • 缺点:0外部碎片的代价是,覆盖技术会产生大量大块的内部碎片——这导致内存的利用率很低

 
 

化固为动——动态分区分配

动态分区分配衍生出的诸多算法思路至今仍令人津津乐道。

提前划定好分区会导致过多的内存空间成为内部碎片,是因为我们压根不知道将要到达的进程究竟有多大;那么,是否可以在进程真正到达时,动态地为其分配分区呢?

这就是又一种内存空间的分配思路——动态分区分配

其实现方式是维护一个空闲分区表/链,进程来到就分配给其空闲分区,进程退出就回收其空闲分区。这样,内存空间的利用率大大的提升了。
 

还没有结束。我们似乎忽略了一个问题,当有多个空闲分区都满足要求时,进程应该分到哪个空闲分区?这听起来不是什么重要的事情,但是,下面要说的这些分区分配算法,非常有助于理解内存的分配与管理:

  1. 首次适应算法(First Fit):空闲分区按地址递增排序(不需要额外开销),每次都从低地址开始查找,找到第一个满足的空闲分区——似乎没什么致命的缺陷,只是低地址会留下碎片,而每次查找时都要经过它们
  2. 最佳适应算法(Best Fit):核心思想是优先使用小分区,以保留大分区——这需要先按分区大小进行排序,甚至每次分配之后又要重新排序,因而需要额外的算法开销;优点明显就是得以保留了大分区;缺点是会产生比First Fit多得多的小碎片
  3. 最坏适应算法(Worst Fit):核心思想是优先使用大分区,从而避免产生小碎片——一来还是需要根据大小进行排序的开销;二来大分区会被迅速用完,这会导致大进程到来后没有任何一个分区可用,这无比致命
  4. 邻近适应算法(Next Fit):该思路企图优化First Fit,即根据地址默认排序,每次从上次查找结束的位置开始查找——的确比起首次适应算法避免了每次都要走低地址的小碎片;但是高地址大分区会被用完

最终,反而首次适应算法(First Fit)的实际效果是最好的。

 
 
 

从连续到离散——分页、分段、段页式

如果一个水杯是一个内存空间的话,是鹅卵石更能塞满它,还是沙子更能塞满它呢?

在上面的比喻中,鹅卵石就像是单一连续分配/固定分区分配中的程序段、动态分区分配中的进程——它们都需要内存中一块连续的空间;沙子就像是接下来要说的段/页——实现在内存空间中的离散存放,从而提高内存空间的利用率,即“装满杯子”。

其中,分页比起分段,更能体现“通过离散存放提高内存利用率”的思想。下面对其进行着重介绍。
 
进程的逻辑地址空间被分为页、页面(Page),内存的物理地址空间被分为页框、页帧、内存块(Page Frame)—— 本质上,它们其实就是大小相等的

如何通过页表(指慢表,暂不考虑快表机制),实现进程逻辑地址和内存物理地址的转换,是分页的关键。下面简要描述其地址变换过程

  1. 逻辑地址 —> 逻辑页号 + 页内偏移量
  2. 页号的合法性检查(与页表长度对比)
  3. 页表始址,页号 —> 页表对应的页表项,取出内容即为块号(第一次访存)
  4. 内存块号 + 页内偏移量 —> 物理地址
  5. 访问内存中的内存单元(第二次访存)

 
我们可以触类旁通,思考下面的问题:页表归根结底也是存放在内存中的,如果一个进程较大,它需要的页表也就较大,而页表项又需要连续的内存空间…

这是个套娃的问题——如何让页表离散到内存中?
也就需要套娃的答案——在页表上再次使用页表,使其离散。即二级页表。

只要能明白为什么要使用二级乃至多级页表,其实就说明你已经完全理解内存的非连续(离散)分配管理方式了。再次强调,离散存放的目的是本篇文章的主题,即提高内存利用率。
 

补充一点,分段的主要目的并不是提高内存利用率,而是根据程序自身的逻辑切分,以更好满足用户的需求(方便编程,分段共享和保护,动态链接和增长);段页式是二者的兼顾;因为本文的侧重点在于讨论内存的利用率,因而着重介绍分页存储管理。

 
 
 

被欺骗的眼睛——虚拟内存

内存可以无限被放大。然而这是计算机OS制造的假象。

简单总结一下之前的内容:

  • 连续分配管理方式:单一连续分配、固定分区分配、动态分区分配
  • 离散分配管理方式:基本分页、基本分段、基本段页式
  • 为了提高内存的利用率,我们变固定为动态、变连续为离散…总的来说,传统存储管理方式已经基本妥善解决了碎片导致的内存利用率问题。

不妨把视角放高一些,不拘泥于内存本身的容量大小,从作业/进程的流动性的角度想一想——如果一个作业/进程因为没有被CPU处理,就赖在内存不走,而且它在短时间内也不会被处理(即「驻留性」)——是不是内存的利用率就被降低了呢?

之前谈到过,「交换」的思想在动态分区分配时,用于交换整个进程——即「交换技术」
而现在,我们可以进一步继承「交换」的思想,用于交换段/页——即「虚拟技术」
 

是不是说,虚拟技术的实现必须建立在离散分配的内存管理方式的基础上呢?

答案是肯定的。虚拟存储(请求分页、请求分段、请求段页式)就是在传统存储(基本分页、基本分段、基本段页式)的基础上加了两个功能而已:

  • 请求调页功能:当访问的信息不在内存时,由操作系统负责将所需的信息从外存调入内存
  • 页面置换功能:当内存不够时,由操作系统负责将内存中暂时用不到的信息换出到外存

通俗的说,通过这种内存与外存的调入调出,使得留在内存中的都是“短时间内用得到的”,调到外存的都是“短时间内用不到的”——因此,内存利用率得到了提高。
 

不仅如此,我们之所以称其为「虚拟」,还是因为OS通过这种调入调出的手法,使得「用户所感受到的逻辑上的对应物」,是远远远远大于「真实存在的物理实体」的;

虚拟存储器的容量是多大呢?是主存+外存,但也不是。甚至从理论上讲,虚拟内存与实际容量已经不再有必然的联系——32位的计算机,理论虚拟存储容量是2^32B;只是在物理层面无法达到罢了。