内存篇

在这里插入图片描述

内存管理从根本上说是操作系统对存储设备进行的抽象和装扮

1. 基本内存管理

程序要运行,必须先加载到内存。但在很久以前,准确地说是在操作系统出现以前,程序并不需要加载到内存就能运行。实际上,在那个已经久远的年代里,程序曾经存放在卡片上,计算机每读一张卡片,就运行一条指令。因此,程序是直接从卡片到执行。

但这种从外部存储媒介上直接执行指令的做法效率极低,且灵活性很差。

因此,人们发明了内存储器来将需要运行的程序先行加载,再自动执行,从而提高效率和灵活性。这也导致了“存储的程序”概念的出现,而存储的程序概念又导致计算机及软件系统的革命性变化。此后,人们对内存的要求越来越多,越来越高。

理想状态下,程序员或用户对内存的要求是:大容量、高速度和持久性

当然这是不可能的,也许这就是人生吧(特朗普式调侃)

但程序员面临的物理现实却是一个由缓存、主存、磁盘、磁带等组成的内存架构。在这个内存的现实架构中,缓存的特点是低容量(相对主存来说)、高速度、高价格;主存的特点则是中容量、中速度和中价格;磁盘则属于大容量、低速度、低成本的存储媒介;磁带则通常更持久但速度更慢。

内存架构的物理现实
在这里插入图片描述

很显然,这样一个存储架构与程序员或用户对内存的要求相差甚远。 要以这个存储架构为基础来提供程序员所需的内存抽象,我们需要一个巧妙有效的内存管理机

内存管理机制负责对内存架构进行管理,使程序在内存架构的任何一个层次上的存放对于用户来说都是一样的。用户无须担心自己的程序是存储在缓存、主存、磁盘还是磁带上,反正运行、计算、输出的结果都一样。而内存管理实现这种媒介透明的手段就是虚拟内存。用我们多次讲过的“抽象”来说,

虚拟内存就是操作系统提供给用户的另一个“幻象”。这个幻象构建在内存架构的顶端,给用户提供一个比物理主存空间大许多的地址空间。

虚拟内存的概念

虚拟内存的概念听上去似乎有点太虚拟,但其实质则并不难理解。我们知道,一个程序如果要运行,必须加载到物理主存中。

但是,物理主存的容量非常有限。因此,如果要把一个程序全部加载到物理主存,则我们所能编写的程序将是很小的程序。

它的最大容量受制于主存容量(还要减去操作系统所占的空间和一些临时缓存空间)。另外,即使我们编写的每个程序的尺寸都小于物理主存容量,但还是存在一个问题:主存能够存放的程序数量将是很有限的,而这将极大地限制多道编程的发展。

那如何解决物理主存容量偏小的缺陷呢?最简单的办法就是购买更大的物理主存。而这将造成计算机成本的大幅飙升,可能很多人都会买不起计算机。

那有没有办法在不增加成本的情况下扩大内存容量呢?有,这就是虚拟内存。

虚拟内存的中心思想是将物理主存扩大到便宜、大容量的磁盘上,即将磁盘空间看做主存空间的一部分。用户程序存放在磁盘上就相当于存放在主存内。用户程序既
可以完全存放在主存,也可以完全存放在磁盘上,当然也可以部分存放在主存、部分存放在磁盘。在这里插入图片描述
虚拟内存除了让程序员感觉到内存容量大大增加之外,还让程序员感觉到内存速度也加快了。这是因为虚拟内存将尽可能从缓存满足用户访问请求,从而给人以速度
提升了的感觉。从这个角度来看,虚拟内存就是实际存储架构与程序员对内存的要求之间的一座桥梁。

当然,容量增大也好,速度提升也好,都是虚拟内存提供的一个幻象,实际上并不是这么回事儿,但用户感觉到是真的,这就是魔术。我们前面讲过,操作系统的一个角色就是魔术师。

操作系统应该存放在内存空间的哪一部分?

在这里插入图片描述
比较起来图11-4a的构造最容易理解。

因为操作系统是为用户提供服务的,在逻辑上处于用户程序之下。将其置于地址空间的下面,符合人们的惯性思维。

另外,操作系统处于地址空间下面还有一个实际好处:就是在复位、中断、陷入等操作时,控制移交给操作系统更方便,因为操纵系统的起始地址为0,无须另行记录操作系统所处的位置,程序计数器清零就可以了。

清零操作对于硬件来说非常简单,无须从总线或寄存器读取任何数据;而图11-4b的布置虽然也可以工作,但显然与人们习惯中操作系统在下的惯性思维不符。

由于现代的计算机内存除了RAM之外,可能还备有ROM。而操作系统既可以全部存放在ROM里,也可以部分存放在ROM里,这样又多出了两种分配方式,如图11-5所示。在这里插入图片描述
图11-5a模式下操作系统放在ROM里面的好处是不容易被破坏。 缺点就是ROM要做得大,能够容纳整个操作系统。由于 ROM 比较贵,通常情况下是备有少量的 ROM, 只将操作系统的一部分放在 ROM里,其余部分放在 RAM里。

因此,这两种分配模式图 b 更好。

图 b 分配模式还有另外一个好处:可以将输入输出和内存访问统一起来。即将输入输出设备里面的寄存器或其他存储媒介编入内存地址(在用户程序地址之上),使得访问输入输出设备如同访问内存一样。这种输入输出称为内存映射的输入输出。如果要访问的地址高于RAM的最高地址,则属于I/O操作,否则属于正常内存操作。

11.5 单道编程的内存管理

最简单的内存管理是单道程序下的内存管理。在单道编程环境下,整个内存里面只有两个程序:一个是用户程序,另一个是操作系统。由于只有一个用户程序,而操
作系统所占用的内存空间是恒定的,我们可以将用户程序总是加载到同一个内存地址上。即用户程序永远从同一个地方开始执行。

在这种管理方式下,操作系统永远跳转到同一个地方来启动用户程序。这样,用户程序里面的地址都可以事先计算出来,即在程序运行前就计算出所有的物理地址。这种在运行前即将物理地址计算好的方式叫做静态地址翻译。

这种内存管理方式是如何达到内存管理的两个目的的呢?

  1. 地址独立

固定地址的内存管理达到地址独立了吗?

那就看看用户在编写程序时是否需要知道该程序
将要运行的物理内存知识。显然不需要。因而用户在编程序时用的虚地址无须考虑具体的物理内存,即该管理模式达到了地址独立。

那么它是如何达到的呢?办法就是将用户程序加载到同一个物理地址上。通过静态编译即可完成虚地址到物理地址的映射,而这个静态翻译工作可以由编译器或者加载器来实现。

  1. 抵制保护达到了吗?

那要看该进程是否会访问到别的用户进程空间,或者别的用户进程是否会访问该进程地址。

答案是不会。因为整个系统里面只有一个用户程序。因此,固定地址的内存管理因为只运行一个用户程序而达到地址保护。

固定地址的内存管理单元非常简单,实际上并不需要任何内存管理单元。因为程序发出的地址已经是物理地址,在执行过程中无须进行任何地址翻译。而这种情况的直接结果就是程序运行速度快,因为越过了地址翻译这个步骤。

固定地址的内存管理缺点也很明显

  • 第1个缺点是整个程序要加载到内存空间中去。这样将导致比物理内存大的程序无法运行。
  • 第2个缺点是,只运行一个程序造成资源浪费。如果一个程序很小,虽然所用内存空间小,但剩下的内存空间也无法使用。
  • 第3个缺点是可能无法在不同的操作系统下运行,因为不同操作系统占用的内存空间大小可能不一样,使得用户程序的起始地址可能不一样。这样在一个系统环境下编译出来的程序很可能无法在另一个系统环境下执行。

11.6 多道编程的内存管理

随着多道编程度数的增加,CPU和内存的利用效率也随之增加。当然,这种增加有个限度,超过这个限度,则因为多道程序之间的资源反而造成系统效率下降。

虽然多道编程可以极大地改善CPU和内存的效率,改善用户响应时间,但是这种改善是需要付出代价的。

这个代价就是操作系统的复杂性。因为多道编程的情况下,无法将程序总是加载到固定的内存地址上,也就是无法使用静态地址翻译。 这样我们就必须在程序加载完毕后才能计算物理地址,也就是在程序运行时进行地址翻译,这种翻译称为动态地址翻译。

在这里插入图片描述
图 11-7 动态地址翻译:用户进程发出的虚拟地址由MMU翻译成物理地址。

也许有读者会想,我们可以在内存固定出几个地址出来,比如说4个。这样我们可以同时加载4个程序到内存,而这4个程序分别加到这4个固定的地址,这样不就可以进行静态地址翻译了吗?但是谁能提前知道某个特定的程序将加载到4个固定地址的哪一个呢?而且,这样做还将带来地址保护上的困难。既然所有的程序皆发出物理地址,该地址是否属于该程序可以访问的空间将无法确认。这样,程序之间的互相保护就成了问题。

11.6.1 固定分区的多道编程内存管理

固定分区的管理就是将内存分为固定的几个区域,每个区域的大小固定。在这里插入图片描述
最下面的分区为操作系统占用,其他分区由用户程序使用。这些分区大小可以一样,也可以不一样。考虑到程序大小不一的实际情况,分区的大小通常也各不相同。当需要加载程序时,选择一个当前闲置且容够大的分区进行加载,如图11-8所示。

11.7 闲置空间管理

在管理内存的时候,操作系统需要知道内存空间有多少空闲。如何才能知道有哪些空闲呢?

这就必须跟踪内存的使用。
  1. 跟踪的办法有两种:第1种办法是给每个分配单
    元赋予一个字位,用来记录该分配单元是否闲置

例如,字位取值0表示分配单元闲置,字位取值1则表示该分配单元已被占用。这种表示法就是所谓的位图表示法,如图11-17所示。在这里插入图片描述

从图中可以看出,内存空间最前面的5个分配单元已经被占用,接下来是4个分配单元则处于闲置状态,可以供程序使用。其他以此类推。

另外一种办法是将分配单元按是否闲置链接起来,这种办法称为链表表示法。对于图11-17的位图所表示的内存分配状态,如果用链表表示则如图11-18所示。在这里插入图片描述

位图表示和链表表示的比较

位图表示和链表表示各有优缺点。如果程序数量很少,那么链表比较好,因为链表的表项数量少。例如,如果只有3个程序在内存中,则最多只需要7个链表节点。但
是如果你的程序很稠密的话,那么链表的节点就很多了。

位图表示法的空间成本是固定的,它不依赖于内存中程序的数量。因此,从空间成本上分析,到底使用哪种表示法得看链表表示后的空间成本是大于位图表示还是小
于位图表示而定。

从可靠性看:

从可靠性上看,位图表示法没有容错能力。如果一个分配单元为1,你并不能肯定它应该为1,还是因为错误变成1的。因为链表有被占空间和闲置空间的表项,可以相
互验证,具有一定的容错能力。

从时间成本看:

位图表示法在修改分配单元状态时,操作很简单,直接修改其位图值即可,而链表表示法则需要对前后空间进行检查以便做出相应的合并。

例如,在图11-18所示的情况下,如果程序中间的那个程序(占用位置从9开始,长度为3)终止,则链表将如图11-19所示。如果是最前面的程序终止,则链表将如图11-20所示。在这里插入图片描述

第12章 页式内存管理

到目前为止,本书已经介绍了几种基本的内存管理方法,分别是固定加载地址的内存管理、固定分区的内存管理、非固定分区的内存管理和交换内存管理。固定加载
地址的内存管理只适合于单道编程,而其他3种则可用于多道编程。这3种适用于多道编程的内存管理模式均使用同一种实现机制:基址与极限

基址与极限的工作原理是将程序发出的虚拟地址加在基址而获得物理地址。如果地址超过指定的极限,则视为地址出界而禁止访问,否则访问正常进行。

交换内存管理:

在我们介绍过的3种多道编程的内存管理模式里,以交换内存管理最为灵活和先进。它可以解决因程序所需空间增长而无法继续运行的困难,又可以实现动态多道编程,可谓是多道编程内存管理“三剑客”里面的第一高手。但这个第一高手也不是什么问题都没有的。事实上,这种策略存在很多重大问题,而其中最重要的两个问题是 空间浪费和程序大小受限

  1. 空间浪费问题

随着程序在内存与磁盘间的交换,内存将变得越来越碎片化,即内存将被不同程序分割成尺寸大小无法使用的小片空间。

例如,假定我们运行8个程序:A、B、C、D、E、F、G、H,其启动、内存需要和交换过程如下:

★A启动,需占用内存200KB,分配空间1000KB~1199KB。
★B启动,需占用内存100KB,分配空间1200KB~1299KB。
★C启动,需占用内存300KB,分配空间1300KB~1599KB。
★A结束,释放内存空间1000KB~1199KB。
★D启动,需占用内存50KB,分配空间1000KB~1049KB。
★E启动,需占用内存100KB,分配空间1600KB~1699KB。
★C结束,释放内存空间1300KB~1599KB。
★F启动,需占用内存200KB,分配空间1300KB~1499KB。
★G启动,需占用内存50KB,分配空间1500KB~1549KB。
★H启动,需占用内存200KB,无法分配空间!

在上述前7个进程执行序列后,当前内存中尚有200KB的闲置空间,分别处于地址1050KB~1199KB和 1550KB~1599KB。但 因为不连续,所以无法容纳进程H。而进程H的空间需求只有200KB!

这种散布在进程之间的闲置空间称为外部碎片。这是因为从进程的粒度来看,这种碎片处于进程空间的外面。这种碎片化过程也称为“外部碎片化”。在这里插入图片描述
图12-2显示的是外部碎片化的示意图。在图12-2f的情况下,内存空间形成碎片,无法容纳新的进程H,尽管内存总闲置空间可以容纳进程H。

随着进程的进进出出,外部碎片将浪费大量的内存空间。我们可以采取一些措施来降低外部碎片的危害,

例如,在寻找空间容纳新的进程时,可以按照某种算法,如最先适用(first fit)或者最佳适用(best fit)来进行。最佳适用就是找到一个能够容纳新程序的最小空间。而最先适用就是找到第1个可以容纳新进程的空间。

实践证明,最先适用比最佳适用更好。虽然最佳适用的名字听上去很好,但因为每次都是最小适应,使得多出来的空间反而更加难以再次利用。

  1. 2 分页内存管理

为了解决交换系统存在的缺陷,出现了分页系统。

分页系统的核心:将虚拟内存空间和物理内存空间皆划分为 大小相同的页面,如4KB、8KB或16KB等,

并以页面作为内存空间的最小分配单位,一个程序的一个页面可以存放在任意一个物理页面里。

这样,由于物理空间是页面的整数倍,并且空间分配以页面为单位,将不会再产生外部碎片。同时,由于一个虚拟页面可以存放在任何一个物理页面里,空间增长也容易解决:只需要分配额外的虚拟页面,并找到一个闲置的物理页面存放即可。

在分页系统下,一个程序发出的虚拟地址由两部分组成:页面号和页内偏移值,如图12-3所示:在这里插入图片描述

例如,对于32位寻址的系统,如果页面大小为4KB,则页面号占20位,页内偏移值占12位。

为了解决程序比内存大的问题,我们可以允许一个进程的部分虚拟页面存放在物理页面之外,也就是磁盘上。在需要访问这些外部虚拟页面时,再将其调入物理内存。由此,交换系统的所有缺陷均被克服。

12.2.1 地址翻译

从上面的分析可以看出,分页系统要能够工作的前提是:对于任何一个虚拟页面,系统知道该页面是否在物理内存中。
如果在的话,其对应的物理页面是哪个;
如果不在的话,则产生一个系统中断(缺页中断),并将该虚页从磁盘转到内存,然后将分配给它的物理页面号返回。

也就是说,页面管理系统要能够将虚拟地址转换为物理地址。该翻译过程如下所示。

if(虚拟页面非法、不在内存或者被保护){
陷入到操作系统错误服务程序
}else{
将虚拟页面号转换为物理页面号
根据物理页面号产生最终物理地址
}

分页系统的核心:页面的翻译,即从虚拟页面到物理页面的映射。

而这个翻译过程由内存管理单元(MMU)完成。MMU接收CPU发出的虚拟地址,将其翻译为物理地址后发送给内存。内存单元按照该物理地址进行相应访问后读出或写入相关数据,如图12-4所示。在这里插入图片描述
图 12-4 虚拟地址到物理地址的流向图

内存管理单元对虚拟地址的翻译只是对页面号的翻译,即将虚拟页面号翻译成物理页面号。而对于偏移值,则不进行任何操作。这是因为虚拟页表和物理页表大小完全一样,虚拟页面里的偏移值和物理页面里的偏移值完全一样,因此无须翻译。

那么内存单元是通过什么手段完成这样的翻译的呢?

当然是查页表。

对于每个程序,内存管理单元都为其保存一个页表,该页表中存放的是虚拟页面到物理页面的映射。

每当为一个虚拟页面寻找到一个物理页面后,就在页表里面增加一个记录来保留该虚拟页面到物理页面的映射关系。随着虚拟页面进出物理内存,页表的内容页不断发生变化。

如果页面有效且在物理内存,又没有受保护,则使用该虚拟页面号作为索引,找到页表中对应该虚拟页面的记录,读取其对应的物理页面号:在这里插入图片描述
图 12-5 虚拟地址到物理地址的翻译过程

那么内存管理单元是怎么知道一个页面是否有效,是否被保护,是否在物理内存呢?这个简单,将这些信息储存在页表里面即可。这样,页表不只是用来进行翻译,还用来进行页面的各种状态判断,因此,页表在分页系统里面的地位举足轻重。

页表的缺点

12.3.1 多级页表

在多级页表结构下,页表根据存放的内容可分为:顶级页表、一级页表、二级页表、三级页表等。顶级页表里面存放的是一级页表的信息,一级页表里面存放的是二级页表的信息,以此类推,到最后一级页表存放的才是虚拟页面到物理页面的映射。一个程序在运行时其顶级页表常驻内存,而次级页表则按需要决定是否存放在物理内存。

例如,如果使用两层页表的话,虚拟地址的前10位可作为顶级页表的索引,中间10位可作为次级页表的索引,最后12位可作为页内偏移值如图12-9所示。
在这里插入图片描述
图 12-9 双级页表下页表号和页内偏移值的字位分配举例

这样,当CPU发出一个虚拟地址时,我们将虚拟地址一分为三,用最前面10位的值找到顶级页表的对应记录,得到所需要的次级页表。

用中间10位的值作为索引在刚才获得次级页表里找到对应的记录,得到对应的物理页面号。然后将物理页面号与页内偏移值链接起来获得最后的物理地址,

如图12-10所示。如果我们需要的次级页表不在物理内存,那么系统将产生缺页中断,而缺页中断即可将所需次级页表带入内存

多级页表为什么占用的内存空间少呢?因为大部分次级页表会放到磁盘上,而放在内存里面的页表较少。因此,内存占用少。

多级页表有什么缺点呢?它降低了系统的速度。因此每次内存访问都变成多次内存访问。对于二级页表,一次内存访问变成了三次内存访问。如果次级页表不在内存,还需要加上一次磁盘访问。这样,系统的速度将大为下降。对于级数更多的页表来说,内存访问速度额下降将更加明显。

12.4 翻译速度

多级页表让内存的使用更有效了,但是系统效率又降下来了。

地址翻译因增加了内存访问次数而降低了系统效率。如果只使用单级页表,则每次内存访问变为两次内存访问,速度的下降还尚可以忍受。但如果使用多级页表或反转页表,则每次内存访问将变为多于两次的内存访问,这样效率的下降将非常明显。

由于内存访问是每条指令都需要执行的操作,这样将造成整个系统效率的下降。 你要使用 CPU,自然是要加载指令,你加载指令,就要从内存中寻找指令,增加了地址翻译,每次寻找指令就多费了一些功夫

那有没有什么办法改善翻译的速度呢?

有。因为程序的运行呈现所谓的时空局域性,即在一段时间内,程序所要访问的地址空间有一定的空间局域性。如果一个页面被访问,则该页面的其他地址可能也将被随后访问。这样,我们可以将该页面的翻译结果存放在缓存里,而无须在访问该页面的每个地址时再翻译一次。这样就可以大大提高系统的执行效率。

这里是引用

这种存放翻译结果的缓存称为翻译快表(Translation Look-Aside Buffer, TLB)。

TLB里面存放的是从虚拟页面到物理页面的映射,其记录的格式与内容和正常页表的记录格式与内容一样如图12-11所示。这样,在进行地址翻译时,如果TLB里有该虚拟地址记录,即“命中”,就从TLB获得其对应的物理页面号,而无须经过多级页表或反转页表查找,从而大大提高翻译速度。如果TLB里面没有该虚拟页面。在这里插入图片描述
图 12-11 TLB示意图

如果CPU发出的虚拟地址属于虚拟页面800,则我们从TLB里面直接获得对应的物理页面号13,而无须经过多级页表或反转页表。但我们怎么知道800在TLB里呢?由于
TLB里面不能按虚拟页面号进行索引,唯一的办法是一个记录一个记录地按顺序查找,以确认我们所需的虚拟页面是否在TLB里面。但这样的话,我们使用TLB的意义就不复存在了。因为搜索TLB所花费的时间可能已经远远超过查找多级页表所花的时间了。 问题出现

那上述问题的出路在哪里呢?本书在论述锁的时候,讲过一个哲学原理:软件没有办法构建原子操作,只能把硬件请出来。

在这里,我们看到,软件没有办法在以一次内存访问完成TLB查找,到这里卡住了。解决方案就是使用硬件。我们在TLB里面进行的比较不是一个个地顺序比较,而是同时比较,即将所有的TLB记录与目的虚拟地址同时比较,因此只需要一次查找就能确定一个虚拟页面号是否在TLB里。这种设计需要同时配备多套比较电路,比较电路的套数需与TLB的大小一样。这也就是为什么TLB非常昂贵

高档品,想快点,就花钱。

当然,在TLB未命中的情况下,我们既可以将页表相应记录存入TLB,然后再由TLB来满足地址翻译需求(即重新启动指令),也可以直接由页表来满足翻译请求,同时将翻译结果存入TLB。这两种方式提供的抽象是不一样的。前者提供的抽象是所有翻译皆由TLB完成,而后者提供的抽象则是翻译过程既看到TLB,又看到正常页表。

方式的差异将影响模块化的设计思路。

显然,采用多级页表的分页系统的效率将取决于TLB的命中率。如果命中率很高,则系统效率高;如果命中率低,则系统效率低。例如,Linux使用的是三级页表(见图
12-12)。按照常理来说,这将使得系统的执行效率大大降低,但许多人并没有感觉到Linux特别慢,这就是因为Linux的TLB命中率高。据某些资料宣称其命中率达98%。不过,Linux在运行图形界面时却相当慢。(这是为什么呢?)在这里插入图片描述
图 12-12 Linux使用三级页表

TLB通常由CPU制造商提供,但TLB的更换算法则有可能由操作系统提供。

12.5 缺页中断处理

之前讲到:在分页系统里,一个虚拟页面既有可能在物理内存,也有可能保存在磁盘上。如果CPU发出的虚拟地址对应的页面不在物理内存,就将产生一个缺页中断。

而缺页中断服务程序负责将需要的虚拟页面找到并加载到内存。那么缺页中断程序是如何知道虚拟页面在磁盘的什么地方呢?

它当然不知道。但它知道产生缺页中断进程所对应的源程序文件名和产生缺页中断的虚拟地址。

这样,缺页中断服务程序首先根据虚拟地址计算出该地址在相应程序文件里面的位移量或偏移量(off-set),然后要求文件系统在这个偏移量的地方进行文件读操作。那么知道读多少内容吗?

当然知道了,读一个页面的数据!而文件系统当然知道如何根据程序的文件名和偏移值来读取数据。

下面是缺页中断的处理步骤:

1)硬件陷入内核。
2)保护通用寄存器。
3)操作系统判断所需的虚拟页面号。
4)操作系统检查地址的合法性。
5)操作系统选择一个物理页面来存放将要调入的页面。
6)如果选择的物理页面包含有未写磁盘的内容,则首先进行写盘操作。
7)操作系统将新的虚拟页面调入内存。
8)更新页表。
9)发生缺页中断的程序进入就绪状态。
10)恢复寄存器。
11)程序继续。

12.6 锁住页面

如果发生缺页中断,就需要从磁盘上将需要的页面调入内存。如果内存没有多余的空间,就需要在现有的页面里选择一个页面进行替换。使用不同的更换算法,页面更换的顺序将各不相同。但不管使用何种算法(将在第13章介绍),每个页面都存在被替换的可能。

这听上去像是一个很公平的事情。问题是有时我们需要把页面锁在内存,不想被交换出去。比如包含用于存放输入数据缓冲区的页面。如果这个页面被替换了,输入数据来的时候就没有地方写,将造成系统效率下降。此外,如果一个页面非常重要,我们知道它将被经常访问,也可以把它锁住,从而防止不必要的页面替换。

如何把页面锁在内存里呢?很简单,只需要对该页面做出特殊标记即可。即我们在页表的相应记录项里增加一项标志。如果该标志被设置,缺页中断服务程序在选择被替换的页面时将跳过该页面。

12.7 页面尺寸

分页系统的一个考虑因素就是页面应该设计为多大?如果太大,可能造成浪费。因为一个程序的最后一个页面很有可能是不满的。最好的情况是一个程序的大小正好
是页面大小的整数倍;最坏的情况则是页面的整数倍多1条指令,多出来的这条指令就要占用一个页面,造成一个页面的绝大部分空间浪费。

在平均情况下,最后一个页面有半个页面被浪费。这种浪费称为内部碎片,即一个进程内部的碎片空间。(还记得外部碎片吗?)这样,页面越大,内部碎片就越大,浪费就越多。

例如,如果虚拟地址空间比较稀疏,大多数地址都是非法的。这样,用大页面造成的浪费将更为明显。当然,对一般的用户程序来讲,其地址空间一般是连贯的,不大可能出现断开的空间。而中间出现断开的程序,最典型的例子是编译器。编译器程序的虚拟地址空间里面有许多不连续的空间。如果被编译的程序尺寸小,则中间的很多空间都将是非法空间。如果页面尺寸很小,则浪费减少了,可以较好地容纳各种数据结构。

但页表尺寸将增大,或者多级页表的层次增多。由此可见,页面大小并不能随意确定,必须考虑各种参数的折中。如何找到一个合适的尺寸呢?具体来说,就是要在页表大小和内部碎片之间进行平衡。


我们既不希望太大的内部碎片,又不想页表太大。那就先算一下因页表和内部碎片造成的系统消耗。

假定p表示页面大小,e表示页表一个记录的大小,s表示程序的平均尺寸;

则整个系统浪费的空间可由下面的表达式计算:

其中s·e/p是一个程序页表所占的内存空间,而p/2则是一个程序平均浪费的页面空间(半个页面)。对上述表达式求极小值就可以得出页面尺寸p的最优大小为:
在这里插入图片描述
在这里插入图片描述

当然,如果需要,我们也可以使用可变尺寸的页面。例如,如果我们服务的环境既有许多很大的程序,也有许多很小的程序,则使用一种固定尺寸的页面不一定是最有效率的。这时,我们可以考虑使用可变尺寸,即不同的程序可以使用不同的页面大小,尽可能地降低系统的空间浪费。

但是可变页面页面策略的缺点也十分明显,首先是操作系统对内存的管理将更为复杂。其次,我们也很难正确地判断每个程序使用何种页面尺寸最为合适。

最后,可变页面尺寸也不是一定就能消除内部碎片的。因此,可变页面尺寸策略听上去动听,实际上并不中用。历史上的Multic操作系统就因为支持可变页面尺寸而受人诟
病。

12.8 内存抖动现象——发生在缺页中断

发生缺页中断时,我们需要在内存中加入磁盘上的页面的,但是可能存在内存已满的情况,此时就需要更换页面。

在更换页面时,如果更换的页面是一个很快就会被再次访问的页面,则在此次缺页中断后很快又会发生新的缺页中断。

在最坏情况下,每次新的访问都是对一个不在内存的页面进行访问,即每次内存访问都产生一次缺页中断,这样每次内存访问皆变成一次磁盘访问,而由于磁盘访问速度比内存可以慢一百万倍,因此整个系统的效率急剧下降。这种现象就称为内存抖动,或者抽打、抽筋(tras-hing)。

当然,我们可以通过仔细设计页面更换算法来降低内存抖动的概率,但却不能完全避免。例如,如果一个系统运行着很多进程,而且这些进程都比较大,即每个程序所占的虚拟空间都较大。这样就有可能造成某个程序频繁需要的页面不能完全放在内存里,造成该程序运行时的内存抖动。

又例如,虽然系统只有一个程序,但是该程序频繁访问的页面数超过物理内存的页面数,这样也将造成内存抖动。

发生内存抖动时,系统的效率将与停滞差不多,几乎看不到任何进展的迹象。为什么呢?这就好像人在抽筋的时候,不大可能有心思去做成什么事情一样。如果页面不断地换出去调进来,CPU的资源将完全耗费在缺页中断上,无法进行任何有效工作。用户体验到的就是计算机停止了工作,而硬盘灯一直亮着。

那么我们有什么办法解决内存抖动呢?这要看是什么原因造成的抖动了。

  • 如果是因为页面替换策略失误,当然可以修改替换算法来解决这个问题。
  • 如果不是页面替换策略的问题,而是因为运行的程序太多,造成至少一个程序无法同时将所有频繁访问的页面调入内存,则需要降低多道编程的度数。通过减少同时运行的程序个数而使得每个程序都有足够的资源来运行而不产生抖动。

例如,我们可以把其中一些进程全部交换到磁盘上,把空出来的空间给其他尚在内存的进程使用。这种降低多道编程度数的做法也称为负载平衡。

当然,有时候即使进行负载平衡也不一定能够消除内存抖动, 如果还是存在一个进程的频繁访问页面就超过物理内存的页面数,即使降低多道编程度数,系统仍将抖
动。

这个时候解决的办法只有两个:一是终止该进程,永远不许其运行;二是增加物理内存的容量,就是开疆拓土。只要有钱,总可以买更多的内存。不过,即使这个办法也是有限度的。因为,一个体系结构能够支持的内存容量总是有限度的。

比莱迪异常

前面说过,如果出现内存抖动,我们可以通过降低多道编程的度数来解决。减少同时运行的程序个数,使得每个程序所占有的物理页面数增加。我们正常的思维是,给一个进程分配的物理页面数增加了,它的缺页中断数就应该减少。但真的是这样吗?答案可不一定。

我们来看一个例子。假定我们使用请求分页(demanding page),即页面只在需要的时候才调入内存,而不是在一开始就调入若干个页面。某个程序共有5个虚拟页面:0、1、2、3、4。对这5个页面的访问顺序为:0、1、2、3、0、1、4、0、1、2、3、4。假定我们分配给该进程的物理页面数为3,页面更换策略为先来先倒,即
在没有多余空闲的物理页面供使用时,最先进入内存的页面被替换掉。

由于一开始内存中没有页面,因此访问第0个页面将产生缺页中断。中断服务程序将该页面调入内存。访问第1、第2页面时情况一样。各发生一次缺页中断,但无需更换页面。访问页面3时就有问题了,必须更换页面,那么把最老的第0页面替换。这样一直访问下去,我们看到整个访问序列将产生9次缺页中断。如图12-13所示。在这里插入图片描述

如果我们分配给该进程的物理页面数增加1页到4页,按理该进程产生的缺页中断次数应该减少,但事实是,其缺页中断次数非但没有减少,反而增加了一次,到10次,如图12-14所示。这种增加物理页面数反而导致缺页次数增加的现象称为比莱迪异常(Beladys anomaly)。在这里插入图片描述

这里需要注意的是,比莱迪异常并不能说明我们就不能给进程增加物理页面数,或者增加物理页面数就一定会导致缺页中断次数增加。

事实上,比莱迪现象不是一种常见现象,而是一种异常现象。因此,只要需要,我们仍然给进程增加物理页面数,而且也期望着其缺页次数随着物理页面数的增加而降低。只不过是在我们这样做的时候要注意比莱迪异常。如果我们发现在增加物理页面数后程序的效率不升反降,则有可能发生了比莱迪异常。这个时候的应对策略是继续增加物理页面数,直到该现象消失为止.

当然,我们也可以改变页面替换算法来避免比莱迪异常。例如,改先进先出算法为LRU或工作集算法即可避免比莱迪异常。这些页面替换算法是第13章要讨论的议题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值