《现代操作系统》读书笔记——内存管理

发布于 12 天前  62 次阅读


内存管理

不管存储器有多大,程序都可以把它填满

地址空间

要保证多个应用程序同时处于内存中并且不互相影响,则需要解决两个问题:保护重定位

保护

一个更好的办法是创造一个新的内存抽象:地址空间。就像进程的概念创造了一类抽象的CPU以运行程序一样,地址空间为程序创造了一种抽象的内存。地址空间是一个进程可用于寻址内存的一套地址集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间(除了在一些特殊情况下进程需要共享它们的地址空间外(fork))。

重定位

这个简单的解决办法使用一种简单的动态重定位。它所做的是简单地把每个进程的地址空间映射到物理内存的不同部分。从CDC6600(世界上最早的超级计算机)到Intel8088(原始IBMPC的心脏),所使用的经典办法是给每个CPU配置两个特殊硬件寄存器,通常叫做基址寄存器界限寄存器。当使用基址寄存器和界限寄存器时,程序装载到内存中连续的空闲位置且装载期间无须重定位,如图3-2c所示。当一个进程运行时,程序的起始物理地址装载到基址寄存器中,程序的长度装载到界限寄存器中。在图3-2c中,当第一个程序运行时,装载到这些硬件寄存器中的基址和界限值分别是0和16384。当第二个程序运行时,这些值分别是16384和32768。如果第三个16KB的程序被直接装载在第二个程序的地址之上并且运行,这时基址寄存器和界限寄存器里的值会是32768和16384。

使用基址寄存器和界限寄存器是给每个进程提供私有地址空间的非常容易的方法,因为每个内存地址在送到内存之前,都会自动先加上基址寄存器的内容。在很多实际系统中,对基址寄存器和界限寄存器会以一定的方式加以保护,使得只有操作系统可以修改它们。

image-20200627212501666

使用基址寄存器和界限寄存器重定位的缺点是,每次访问内存都需要进行加法和比较运算。比较可以做得很快,但是加法由于进位传递时间的问题,在没有使用特殊电路的情况下会显得很慢。

交换技术

有两种处理内存超载的通用方法。最简单的策略是交换(swapping)技术,即把一个进程完整调入内存,使该进程运行一段时间,然后把它存回磁盘。空闲进程主要存储在磁盘上,所以当它们不运行时就不会占用内存(尽管它们的一些进程会周期性地被唤醒以完成相关工作,然后就又进入睡眠状态)。另一种策略是虚拟内存(virtualmemory),该策略甚至能使程序在只有一部分被调入内存的情况下运行。

交换系统的操作如图3-4所示。开始时内存中只有进程A。之后创建进程B和C或者从磁盘将它们换入内存。图3-4d显示A被交换到磁盘。然后D被调入,B被调出,最后A再次被调入。由于A的位置发生变化,所以在它换入的时候通过软件或者在程序运行期间(多数是这种情况)通过硬件对其地址进行重定位。例如,在这里可以很好地使用基址寄存器和界限寄存器。

image-20200627213011737

交换在内存中产生了多个空闲区(hole,也称为空洞),通过把所有的进程尽可能向下移动,有可能将这些小的空闲区合成一大块。该技术称为内存紧缩(memorycompaction)。这个操作通常不进行,因为它要耗费大量的CPU时间。例如,一台有1GB内存的计算机可以每20ns复制4个字节,它紧缩全部内存大约要花费5s。

为了解决进程数据段增长的问题往往会预先分配一些额外的内存,然而,当进程被换出到磁盘上时,应该只交换进程实际上使用的内存中的内容,将额外的内存交换出去是一种浪费。

内存管理

位图

使用位图方法时,内存可能被划分成小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0表示空闲,1表示占用(或者相反)。

image-20200627214105429

分配单元的大小是一个重要的设计因素。分配单元越小,位图越大。

因为内存的大小和分配单元的大小决定了位图的大小,所以它提供了一种简单的利用一块固定大小的内存区就能对内存使用情况进行记录的方法。这种方法的主要问题是,在决定把一个占k个分配单元的进程调入内存时,存储管理器必须搜索位图,在位图中找出有k个连续0的串。查找位图中指定长度的连续0串是耗时的操作(因为在位图中该串可能跨越字的边界),这是位图的缺点。

链表

另一种记录内存使用情况的方法是,维护一个记录已分配内存段和空闲内存段的链表。其中链表中的一个结点或者包含一个进程,或者是两个进程间的一个空的空闲区。可用图3-6c所示的段链表来表示图3-6a所示的内存布局。链表中的每一个结点都包含以下域:空闲区(H)或进程(P)的指示标志、起始地址、长度和指向下一结点的指针。

内存分配算法
  • 首次适配(first fit)

    存储管理器沿着段链表进行搜索,直到找到一个足够大的空闲区,除非空闲区大小和要分配的空间大小正好一样,否则将该空闲区分为两部分,一部分供进程使用,另一部分形成新的空闲区。首次适配算法是一种速度很快的算法,因为它尽可能少地搜索链表结点。

  • 下次适配(next fit)

    它的工作方式和首次适配算法相同,不同点是每次找到合适的空闲区时都记录当时的位置。以便在下次寻找空闲区时从上次结束的地方开始搜索,而不是像首次适配算法那样每次都从头开始。Bays(1977)的仿真程序证明下次适配算法的性能略低于首次适配算法。

  • 最佳适配(best fit)

    搜索整个链表(从开始到结束),找出能够容纳进程的最小的空闲区。最佳适配算法试图找出最接近实际需要的空闲区,以最好地区配请求和可用空闲区,而不是先拆分一个以后可能会用到的大的空闲区。

    因为每次调用最佳适配算法时都要搜索整个链表,所以它要比首次适配算法慢。让人感到有点意外的是它比首次适配算法或下次适配算法浪费更多的内存,因为它会产生大量无用的小空闲区。一般情况下,首次适配算法生成的空闲区更大一些。

  • 最差适配(worst fit)

    总是分配最大的可用空闲区,使新的空闲区比较大从而可以继续使用。

  • 快速适配(quick fit)

    将那些常用大小的空闲区维护单独的链表。例如,有一个n项的表,该表的第一项是指向大小为4KB的空闲区链表表头的指针,第二项是指向大小为8KB的空闲区链表表头的指针,第三项是指向大小为12KB的空闲区链表表头的指针,以此类推。像21KB这样的空闲区既可以放在20KB的链表中,也可以放在一个专门存放大小比较特别的空闲区的链表中。

虚拟内存

最早虚拟内存主要用户解决单个程序大于内存的问题。在20世纪60年代所采取的解决方法是:把程序分割成许多片段,称为覆盖(overlay)。程序开始执行时,将覆盖管理模块装入内存,该管理模块立即装入并运行覆盖0。执行完成后,覆盖0通知管理模块装入覆盖1,或者占用覆盖0的上方位置(如果有空间),或者占用覆盖0(如果没有空间)。一些覆盖系统非常复杂,允许多个覆盖块同时在内存中。覆盖块存放在磁盘上,在需要时由操作系统动态地换入换出。

虽然由系统完成实际的覆盖块换入换出操作,但是程序员必须把程序分割成多个片段。把一个大程序分割成小的、模块化的片段是非常费时和枯燥的,并且易于出错。很少程序员擅长使用覆盖技术。因此,没过多久就有人找到一个办法,把全部工作都交给计算机去做。采用的这个方法(Fotheringham,1961)称为虚拟内存(virtualmemory)。

虚拟内存的基本思想是:

每个程序拥有自己的地址空间,这个空间被分割成多个块,每一块称作一页或页面(page)。每一页有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。

从某个角度来讲,虚拟内存是对基址寄存器和界限寄存器的一种综合。8088为正文和数据分离出专门的基址寄存器(但不包括界限寄存器)。而虚拟内存使得整个地址空间可以用相对较小的单元映射到物理内存,而不是为正文段和数据段分别进行重定位。下面会介绍虚拟内存是如何实现的。虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。

分页

由程序产生的这些地址称为虚拟地址(virtualaddress),它们构成了一个虚拟地址空间(virtual addressspace)。在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存总线上,读写操作使用具有同样地址的物理内存字;而在使用虚拟内存的情况下,虚拟地址不是被直接送到内存总线上,而是被送到内存管理单元(Memory Management Unit,MMU),MMU把虚拟地址映射为物理内存地址,如图3-8所示。

image-20200627215535103

图3-9中一个简单的例子说明了这种映射是如何工作的。在这个例子中,有一台可以产生16位地址的计算机,地址范围从0到64K,且这些地址是虚拟地址。然而,这台计算机只有32KB的物理内存,因此,虽然可以编写64KB的程序,但它们却不能被完全调入内存运行。在磁盘上必须有一个可以大到64KB的程序核心映像的完整副本,以保证程序片段在需要时能被调入内存。

image-20200627215619396

虚拟地址空间按照固定大小划分成称为页面(page)的若干单元。在物理内存中对应的单元称为页框(pageframe)。页面和页框的大小通常是一样的,在本例中是4KB,现有的系统中常用的页大小一般从512字节到64KB。对应于64KB的虚拟地址空间和32KB的物理内存,我们得到16个虚拟页面和8个页框。RAM和磁盘之间的交换总是以整个页面为单元进行的。

当程序访问了一个未映射的页面的时候,MMU注意到该页面没有被映射,于是使CPU陷入到操作系统,这个陷阱称为缺页中断(pagefault)。操作系统找到一个很少使用的页框且把它的内容写入磁盘(如果它不在磁盘上)。随后把需要访问的页面读到刚才回收的页框中,修改映射关系,然后重新启动引起陷阱的指令。

MMU的内部结构

image-20200627220935033

可用页号作为页表(page table)的索引,以得出对应于该虚拟页面的页框号。如果“在/不在”位是0,则将引起一个操作系统陷阱。如果该位是1,则将在页表中查到的页框号复制到输出寄存器的高3位中,再加上输入虚拟地址中的低12位偏移量。如此就构成了15位的物理地址。输出寄存器的内容随即被作为物理地址送到内存总线。

页表

页表项的结构是与机器密切相关的,但不同机器的页表项存储的信息都大致相同。图3-11中给出了页表项的一个例子。不同计算机的页表项大小可能不一样,但32位是一个常用的大小。最重要的域是。毕竟页映射的目的是找到这个值,其次是,

image-20200627221052957

  • 页框号

    实际对应物理内存页框位置

  • “在/不在”位

    这一位是1时表示该表项是有效的,可以使用;如果是0,则表示该表项对应的虚拟页面现在不在内存中,访问该页面会引起一个缺页中断。

  • “保护”(protection)位

    指出一个页允许什么类型的访问。最简单的形式
    是这个域只有一位,0表示读/写,1表示只读。一个更先进的方法是使用三位,各位分别对应是否启用读、写、执行该页面。

  • “修改”(modified)位

    在写入一页时由硬件自动设置修改位。该位在操作系统重新分配页框时是非常有用的。如果一个页面已经被修改过(即它是“脏”的),则必须把它写回磁盘。如果一个页面没有被修改过(即它是“干净”的),则只简单地把它丢弃就可以了,因为它在磁盘上的副本仍然是有效的。这一位有时也被称为脏位(dirty bit),因为它反映了该页面的状态。

  • “访问”(referenced)位

    不论是读还是写,系统都会在该页面被访问时设置访问位。它的值被用来帮助操作系统在发生缺页中断时选择要被淘汰的页面。不再使用的页面要比正在使用的页面更适合淘汰。这一位在即将讨论的很多页面置换算法中都会起到重要的作用。

  • 高速缓存禁止位

    最后一位用于禁止该页面被高速缓存。对那些映射到设备寄存器而不是常规内存的页面而言,这个特性是非常重要的。假如操作系统正在紧张地循环等待某个I/O设备对它刚发出的命令作出响应,保证硬件是不断地从设备中读取数据而不是访问一个旧的被高速缓存的副本是非常重要的。通过这一位可以禁止高速缓存。

应该注意的是,若某个页面不在内存时,用于保存该页面的磁盘地址不是页表的一部分。原因很简单,页表只保存把虚拟地址转换为物理地址时硬件所需要的信息。操作系统在处理缺页中断时需要把该页面的磁盘地址等信息保存在操作系统内部的软件表格中。硬件不需要它。

在深入到更多应用实现问题之前,值得再次强调的是:虚拟内存本质上是用来创造一个新的抽象概念——地址空间,这个概念是对物理内存的抽象,类似于进程是对物理机器(CPU)的抽象。虚拟内存的实现,是将虚拟地址空间分解成页,并将每一页映射到物理内存的某个页框或者(暂时)解除映射。因此,本章的基本内容即关于操作系统创建的抽象,以及如何管理这个抽象。

加速分页过程

在任何分页式系统中,都需要考虑两个主要问题:

  • 虚拟地址到物理地址的映射必须非常快。

    由于每次访问内存,都需要进行虚拟地址到物理地址的映射。所有的指令最终都必须来自内存,并且很多指令也会访问内存中的操作数。因此,每条指令进行一两次或更多页表访问是必要的。如果执行一条指令需要1ns,页表查询必须在0.2ns之内完成,以避免映射成为一个主要瓶颈。

  • 如果虚拟地址空间很大,页表也会很大。

    假设页长为4KB,32位的地址空间将有100万页,而64位地址空间简直多到超乎你的想象。如果虚拟地址空间中有100万个页,那么页表必然有100万条表项。另外请记住,每个进程都需要自己的页表(因为它有自己的虚拟地址空间)。

TLB(转换检测缓冲区)

为计算机设置一个小型的硬件设备,将虚拟地址直接映射到物理地址,而不必再访问页表。这种设备称为转换检测缓冲区(TranslationLookasideBuffer,TLB),有时又称为相联存储器(associatememory),如图3-12所示。它通常在MMU中,包含少量的表项,在此例中为8个,在实际中很少会超过64个。每个表项记录了一个页面的相关信息,包括虚拟页号、页面的修改位、保护码(读/写/执行权限)和该页所对应的物理页框。除了虚拟页号(不是必须放在页表中的),这些域与页表中的域是一一对应的。另外还有一位用来记录这个表项是否有效(即是否在使用)

image-20200628215431654

TLB是如何工作

将一个虚拟地址放入MMU中进行转换时,硬件首先通过将该虚拟页号与TLB中所有表项同时(即并行)进行匹配,判断虚拟页面是否在其中。如果发现了一个有效的匹配并且要进行的访问操作并不违反保护位,则将页框号直接从TLB中取出而不必再访问页表。如果虚拟页面号确实是在TLB中,但指令试图在一个只读页面上进行写操作,则会产生一个保护错误,就像对页表进行非法访问一样。

如果MMU检测到没有有效的匹配项时,就会进行正常的页表查询。接着从TLB中淘汰一个表项,然后用新找到的页表项代替它。这样,如果这一页面很快再被访问,第二次访问TLB时自然将会命中而不是不命中。当一个表项被清除出TLB时,将修改位复制到内存中的页表项,而除了访问位,其他的值不变。当页表项中从页表装入到TLB中时,所有的值都来自内存。

多级页表

引入多级页表的原因是避免把全部页表一直保存在内存中。特别是那些从不需要的页表就不应该保留。比如一个需要12MB内存的进程,其最底端是4MB的程序正文段,后面是4MB的数据段,顶端是4MB的堆栈段,在数据段上方和堆栈段下方之间是大量根本没有使用的空闲区。

倒排页表

在这种设计中,在实际内存中每一个页框有一个表项,而不是每一个虚拟页面有一个表项。例如,对于64位虚拟地址,4KB的页,1GB的RAM,一个倒排页表仅需要262144(2^10*2^10/2^8,即总物理内存除页大小)个页表项。表项记录哪一个(进程,虚拟页面)对定位于该页框。

image-20200628221326451

倒排页表的不足

从虚拟地址到物理地址的转换会变得很困难。当进程n访问虚拟页面p时,硬件不再能通过把p当
作指向页表的一个索引来查找物理页框。取而代之的是,它必须搜索整个倒排页表来查找某一个表项(n,p)。

解决方法:

  • 使用TLB优化频繁访问的页
  • 建立一张散列表,用(使用到的)虚拟地址来散列。

页面置换算法

最优页面置换算法

这个算法惟一的问题就是它是无法实现的。

在缺页中断发生时,有些页面在内存中,其中有一个页面(包含紧接着的下一条指令的那个页面)将很快被访问,其他页面则可能要到10、100或1000条指令后才会被访问,每个页面都可以用在该页面首次被访问前所要执行的指令数作为标记。

最近未使用页面置换算法

为使操作系统能够收集有用的统计信息,在大部分具有虚拟内存的计算机中,系统为每一页面设置了两个状态位。当页面被访问(读或写)时设置R位;当页面(即修改页面)被写入时设置M位。

当启动一个进程时,它的所有页面的两个位都由操作系统设置成0,R位被定期地(比如在每次时钟中断时)清零,以区别最近没有被访问的页面和被访问的页面。

当发生缺页中断时,操作系统检查所有的页面并根据它们当前的R位和M位的值,把它们分为4类:

  • 没有被访问,没有被修改。
  • 没有被访问,已被修改。
  • 已被访问,没有被修改。
  • 已被访问,已被修改。

NRU(Not Recently Used,最近未使用)算法随机地从类编号最小的非空类中挑选一个页面淘汰之。这个算法隐含的意思是,在最近一个时钟滴答中(典型的时间是大约20ms)淘汰一个没有被访问的已修改页面要比淘汰一个被频繁使用的“干净”页面好。NRU主要优点是易于理解和能够有效地被实现,虽然它的性能不是最好的,但是已经够用了。

先进先出页面置换算法

由操作系统维护一个所有当前在内存中的页面的链表,最新进入的页面放在表尾,最久进入的页面放在表头。当发生缺页中断时,淘汰表头的页面并把新调入的页面加到表尾。

缺点是没有区分页面被使用的频繁度,经常被访问的页面可能被清除。

第二次机会页面置换算法

FIFO算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:检查最老页面的R位。如果R位是0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是1,就将R位清0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续搜索。这一算法称为第二次机会(secondchance)算法。

第二次机会算法就是寻找一个最近的时钟间隔以来没有被访问过的页面。如果所有的页面都被访问过了,该算法就简化为纯粹的FIFO算法。

时钟页面置换算法

尽管第二次机会算法是一个比较合理的算法,但它经常要在链表中移动页面,既降低了效率又不是很有必要。一个更好的办法是把所有的页面都保存在一个类似钟面的环形链表中,一个表针指向最老的页面,如图3-16所示。

image-20200628222510913

当发生缺页中断时,算法首先检查表针指向的页面,如果它的R位是0就淘汰该页面,并把新的页面插入这个位置,然后把表针前移一个位置;如果R位是1就清除R位并把表针前移一个位置,重复这个过程直到找到了一个R位为0的页面为止。了解了这个算法的工作方式,就明白为什么它被称为时钟(clock)算法了。

最近最少使用页面置换算法

对最优算法的一个很好的近似是基于这样的观察:在前面几条指令中频繁使用的页面很可能在后面的几条指令中被使用。反过来说,已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。这个思想提示了一个可实现的算法:在缺页中断发生时,置换未使用时间最长的页面。这个策略称为LRU(Least Recently Used,最近最少使用)页面置换算法。

虽然LRU在理论上是可以实现的,但代价很高。为了完全实现LRU,需要在内存中维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾。困难的是在每次访问内存时都必须要更新整个链表。在链表中找到一个页面,删除它,然后把它移动到表头是一个非常费时的操作,即使使用硬件实现也一样费时(假设有这样的硬件)。

用软件模拟LRU

一种可能的LRU实现方案称为NFU(NotFrequentlyUsed,最不常用)算法。该算法将每个页面与一个软件计数器相关联,计数器的初值为0。每次时钟中断时,由操作系统扫描内存中所有的页面,将每个页面的R位(它的值是0或1)加到它的计数器上。这个计数器大体上跟踪了各个页面被访问的频繁程度。发生缺页中断时,则置换计数器值最小的页面。

NFU的主要问题是它从来不忘记任何事情(状态不会被清除)。比如,在一个多次(扫描)编译器中,在第一次扫描中被频繁使用的页面在程序进入第二次扫描时,其计数器的值可能仍然很高。实际上,如果第一次扫描的执行时间恰好是各次扫描中最长的,含有以后各次扫描代码的页面的计数器可能总是比含有第一次扫描代码的页面小,结果是操作系统将置换有用的页面而不是不再使用的页面。

幸运的是只需对NFU做一个小小的修改就能使它很好地模拟LRU。其修改分为两部分:首先,在R位被加进之前先将计数器右移一位(相当于除2);其次,将R位加到计数器最左端的位而不是最右端的位。修改以后的算法称为老化(aging)算法,图3-18解释了它是如何工作的。假设在第一个时钟滴答后,页面0到页面5的R位值分别是1、0、1、0、1、1(页面0为1,页面1为0,页面2为1,以此类推)。换句话说,在时钟滴答0到时钟滴答1期间,访问了页0、2、4、5,它们的R位设置为1,而其他页面的R位仍然是0。对应的6个计数器在经过移位并把R位插入其左端后的值如图3-18a所示。图中后面的4列是在下4个时钟滴答后的6个计数器的值。

image-20200628223003201

发生缺页中断时,将置换计数器值最小的页面。如果一个页面在前面4个时钟滴答中都没有访问过,那么它的计数器最前面应该有4个连续的0,因此它的值肯定要比在前面三个时钟滴答中都没有被访问过的页面的计数器值小。

该算法与LRU有两个区别。如图3-18e中的页面3和页面5,它们都连续两个时钟滴答没有被访问过了,而在两个时钟滴答之前的时钟滴答中它们都被访问过。根据LRU,如果必须置换一个页面,则应该在这两个页面中选择一个。然而现在的问题是,我们不知道在时钟滴答1到时钟滴答2期间它们中的哪一个页面是后被访问到的。因为在每个时钟滴答中只记录了一位,所以无法区分在一个时钟滴答中哪个页面在较早的时间被访问以及哪个页面在较晚的时间被访问,因此,我们所能做的就是置换页面3,原因是页面5在更往前的两个时钟滴答中也被访问过而页面3没有。

LRU和老化算法的第二个区别是老化算法的计数器只有有限位数(本例中是8位),这就限制了其对以往页面的记录。如果两个页面的计数器都是0,我们只能在两个页面中随机选一个进行置换。实际上,有可能其中一个页面上次被访问是在9个时钟滴答以前,另一个页面是在1000
个时钟滴答以前,而我们却无法看到这些。在实践中,如果时钟滴答是20ms,8位一般是够用的。假如一个页面已经有160ms没有被访问过,那么它很可能并不重要。

工作集页面置换算法

在单纯的分页系统里,刚启动进程时,在内存中并没有页面。在CPU试图取第一条指令时就会产生一次缺页中断,使操作系统装入含有第一条指令的页面。其他由访问全局数据和堆栈引起的缺页中断通常会紧接着发生。一段时间以后,进程需要的大部分页面都已经在内存了,进程开始在较少缺页中断的情况下运行。这个策略称为请求调页(demand paging),因为页面是在需要时被调入的,而不是预先装入。

一个进程当前正在使用的页面的集合称为它的工作集(workingset)(Denning,1968a;Denning,1980)。如果整个工作集都被装入到了内存中,那么进程在运行到下一运行阶段(例如,编译器的下一遍扫描)之前,不会产生很多缺页中断。若内存太小而无法容纳下整个工作集,那么进程的运行过程中会产生大量的缺页中断,导致运行速度也会变得很缓慢,因为通常只需要几个纳秒就能执行完一条指令,而通常需要十毫秒才能从磁盘上读入一个页面。如果一个程序每10ms只能执行一到两条指令,那么它将会需要很长时间才能运行完。若每执行几条指令程序就发生一次缺页中断,那么就称这个程序发生了颠簸(thrashing)(Denning,1968b)。

不少分页系统都会设法跟踪进程的工作集,以确保在让进程运行以前,它的工作集就已在内存中了。该方法称为工作集模型(working set model)(Denning,1970),其目的在于大大减少缺页中断率。在让进程运行前预先装入其工作集页面也称为预先调页(prepaging)。请注意工作集是随着时间变化的。

人们很早就发现大多数程序都不是均匀地访问它们的地址空间的,而访问往往是集中于一小部分页面。一次内存访问可能会取出一条指令,也可能会取数据,或者是存储数据。在任一时刻t,都存在一个集合,它包含所有最近k次内存访问所访问过的页面。这个集合w(k,t)就是工作集。因为最近k=1次访问肯定会访问最近k>1次访问所访问过的页面,所以w(k,t)是k的单调非递减函数。随着k的变大,w(k,t)是不会无限变大的,因为程序不可能访问比它的地址空间所能容纳的页面数目上限还多的页面,并且几乎没有程序会使用每个页面。图3-19描述了作为k的函数的工作集的大小。

image-20200628223759916

计算工作集

不是向后找最近k次的内存访问,而是考虑其执行时间。例如,按照以前的方法,我们定义工作集为前1000万次内存访问所使用过的页面的集合,那么现在就可以这样定义:工作集即是过去10ms中的内存访问所用到的页面的集合。实际上,这样的模型很合适且更容易实现。要注意到,每个进程只计算它自己的执行时间。因此,如果一个进程在T时刻开始,在(T+100)ms的时刻使用了40msCPU时间,对工作集而言,它的时间就是40ms。一个进程从它开始执行到当前所实际使用的CPU时间总数通常称作当前实际运行时间。通过这个近似的方法,进程的工作集可以被称为在过去的τ秒实际运行时间中它所访问过的页面的集合。

基于工作集的页面置换算法

基本思路就是找出一个不在工作集中的页面并淘汰它。在图3-20中读者可以看到某台机器的部分页表。因为只有那些在内存中的页面才可以作为候选者被淘汰,所以该算法忽略了那些不在内存中的页面。每个表项至少包含两条信息:上次使用该页面的近似时间和R(访问)位。空白的矩形表示该算法不需要的其他域,如页框号、保护位、M(修改)位。

image-20200628224253426

该算法工作方式如下。如前所述,假定使用硬件来置R位和M位。同样,假定在每个时钟滴答中,有一个定期的时钟中断会用软件方法来清除R位。每当缺页中断发生时,扫描页表以找出一个合适的页面淘汰之。

在处理每个表项时,都需要检查R位。如果它是1,就把当前实际时间写进页表项的“上次使用时间”域,以表示缺页中断发生时该页面正在被使用。既然该页面在当前时钟滴答中已经被访问过,那么很明显它应该出现在工作集中,并且不应该被删除(假定τ横跨多个时钟滴答)。

如果R是0,那么表示在当前时钟滴答中,该页面还没有被访问过,则它就可以作为候选者被置换。为了知道它是否应该被置换,需要计算它的生存时间(即当前实际运行时间减去上次使用时间),然后与τ做比较。如果它的生存时间大于τ,那么这个页面就不再在工作集中,而用新的页面置换它。扫描会继续进行以更新剩余的表项。然而,如果R是0同时生存时间小于或等于τ,则该页面仍然在工作集中。这样就要把该页面临时保留下来,但是要记录生存时间最长(“上次使用时间”的最小值)的页面。

如果扫描完整个页表却没有找到适合被淘汰的页面,也就意味着所有的页面都在工作集中。在这种情况下,如果找到了一个或者多个R=0的页面,就淘汰生存时间最长的页面。在最坏情况下,在当前时间滴答中,所有的页面都被访问过了(也就是都有R=1),因此就随机选择一个页面淘汰,如果有的话最好选一个干净页面。

工作集时钟页面置换算法

缺页中断发生后,需要扫描整个页表才能确定被淘汰的页面,因此基本工作集算法是比较费时的。有一种改进的算法,它基于时钟算法,并且使用了工作集信息,称为WSClock(工作集时钟)算法(Carr和Hennessey,1981)。由于它实现简单,性能较好,所以在实际工作中得到了广泛应用。

与时钟算法一样,所需的数据结构是一个以页框为元素的循环表,参见图3-21a。最初,该表是空的。当装入第一个页面后,把它加到该表中。随着更多的页面的加入,它们形成一个环。每个表项包含来自基本工作集算法的上次使用时间,以及R位(已标明)和M位(未标明)。

image-20200628224612645

与时钟算法一样,每次缺页中断时,首先检查指针指向的页面。如果R位被置为1,该页面在当前时钟滴答中就被使用过,那么该页面就不适合被淘汰。然后把该页面的R位置为0,指针指向下一个页面,并重复该算法。该事件序列之后的状态参见图3-21b。

现在来考虑指针指向的页面在R=0时会发生什么,参见图3-21c。如果页面的生存时间大于τ并且该页面是干净的,它就不在工作集中,并且在磁盘上有一个有效的副本。申请此页框,并把新页面放在其中,如图3-21d所示。另一方面,如果此页面被修改过,就不能立即申请页框,因为这个页面在磁盘上没有有效的副本。为了避免由于调度写磁盘操作引起的进程切换,指针继续向前走,算法继续对下一个页面进行操作。毕竟,有可能存在一个旧的且干净的页面可以立即使用。

原则上,所有的页面都有可能因为磁盘I/O在某个时钟周期被调度。为了降低磁盘阻塞,需要设置一个限制,即最大只允许写回n个页面。一旦达到该限制,就不允许调度新的写操作。

如果指针经过一圈返回它的起始点会发生什么呢?这里有两种情况:

  • 至少调度了一次写操作。
  • 没有调度过写操作。

对于第一种情况,指针仅仅是不停地移动,寻找一个干净页面。既然已经调度了一个或者多个写操作,最终会有某个写操作完成,它的页面会被标记为干净。置换遇到的第一个干净页面,这个页面不一定是第一个被调度写操作的页面,因为硬盘驱动程序为了优化性能可能已经把写操作重排序了。

对于第二种情况,所有的页面都在工作集中,否则将至少调度了一个写操作。由于缺乏额外的信息,一个简单的方法就是随便置换一个干净的页面来使用,扫描中需要记录干净页面的位置。如果不存在干净页面,就选定当前页面并把它写回磁盘。

页面置换算法小结

image-20200628225047172

  • 最优算法在当前页面中置换最后要访问到的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,因此实际上该算法不能使用。然而,它可以作为衡量其他算法的基准。

  • NRU算法根据R位和M位的状态把页面分为四类。从编号最小的类中随机选择一个页面置换。该算法易于实现,但是性能不是很好,还存在更好的算法。

  • FIFO算法通过维护一个页面的链表来记录它们装入内存的顺序。淘汰的是最老的页面,但是该页面可能仍在使用,因此FIFO算法不是一个好的选择。

  • 第二次机会算法是对FIFO算法的改进,它在移出页面前先检查该页面是否正在被使用。如果该页面正在被使用,就保留该页面。这个改进大大提高了性能。

  • 时钟算法是第二次机会算法的另一种实现。它具有相同的性能特征,而且只需要更少的执行时间。

  • LRU算法是一种非常优秀的算法,但是只能通过特定的硬件来实现。如果机器中没有该硬件,那么也无法使用该算法。

  • NFU是一种近似于LRU的算法,它的性能不是非常好,然而,老化算法更近似于LRU并且可以更有效地实现,是一个很好的选择。

  • 最后两种算法都使用了工作集。工作集算法有合理的性能,但它的实现开销较大。工作集时钟算法是它的一种变体,不仅具有良好的性能,并且还能高效地实现。

总之,最好的两种算法是老化算法和工作集时钟算法,它们分别基于LRU和工作集。它们都具有良好的页面调度性能,可以有效地实现。也存在其他一些算法,但在实际应用中,这两种算法可能是最重要的。

分页系统中的设计问题

局部分配策略与全局分配策略

在某进程发生缺页中断而触发页面置换时

  • 局部:对该进程持有的页面进行页面置换算法
  • 全局:对所有页面进行页面置换

局部算法可以有效地为每个进程分配固定的内存片段。 全局算法在可运行进程之间动态地分配页框,因此分配给各个进程的页框数是随时间变化的。

全局算法在通常情况下工作得比局部算法好,当工作集的大小随进程运行时间发生变化时这种现象更加明显。若使用局部算法,即使有大量的空闲页框存在,工作集的增长也会导致颠簸。如果工作集缩小了,局部算法又会浪费内存。在使用全局算法时,系统必须不停地确定应该给每个进程分配多少页框。一种方法是监测工作集的大小,工作集大小由“老化”位指出,但这个方法并不能防止颠簸。因为工作集的大小可能在几微秒内就会发生改变,而老化位却要经历一定的时钟滴答数才会发生变化。

工作集算法无法用于全局策略。

负载控制

页面大小

共享

内存映射文件

清除策略

缺页中断

我们终于可以讨论缺页中断发生的细节了。缺页中断发生时的事件顺序
如下:

  • 硬件陷入内核,在堆栈中保存程序计数器。大多数机器将当前指令的各种状态信息保存在特殊的CPU寄存器中。
  • 启动一个汇编代码例程保存通用寄存器和其他易失的信息,以免被操作系统破坏。这个例程将操作系统作为一个函数来调用。
  • 当操作系统发现一个缺页中断时,尝试发现需要哪个虚拟页面。通常一个硬件寄存器包含了这一信息,如果没有的话,操作系统必须检索程序计数器,取出这条指令,用软件分析这条指令,看看它在缺页中断时正在做什么。
  • 一旦知道了发生缺页中断的虚拟地址,操作系统检查这个地址是否有效,并检查存取与保护是否一致。如果不一致,向进程发出一个信号或杀掉该进程。如果地址有效且没有保护错误发生,系统则检查是否有空闲页框。如果没有空闲页框,执行页面置换算法寻找一个页面来淘汰。
  • 如果选择的页框“脏”了,安排该页写回磁盘,并发生一次上下文切换,挂起产生缺页中断的进程,让其他进程运行直至磁盘传输结束。无论如何,该页框被标记为忙,以免因为其他原因而被其他进程占用。
  • 一旦页框“干净”后(无论是立刻还是在写回磁盘后),操作系统查找所需页面在磁盘上的地址,通过磁盘操作将其装入。该页面被装入后,产生缺页中断的进程仍然被挂起,并且如果有其他可运行的用户进程,则选择另一个用户进程运行。
  • 当磁盘中断发生时,表明该页已经被装入,页表已经更新可以反映它的位置,页框也被标记为正常状态。
  • 恢复发生缺页中断指令以前的状态,程序计数器重新指向这条指令。
  • 调度引发缺页中断的进程,操作系统返回调用它的汇编语言例程。
  • 该例程恢复寄存器和其他状态信息,返回到用户空间继续执行,就好像缺页中断没有发生过一样。

指令备份

分段

小结

本章中我们考察了存储管理。我们看到在最简单的系统中是根本没有任何交换或分页的。一旦一个程序装入内存,它将一直在内存中运行直到完成。一些操作系统在同一时刻只允许一个进程在内存中运行,而另一些操作系统支持多道程序设计。

接下来是交换技术。系统通过交换技术可以同时运行总内存占用超过物理内存大小的多个进程,如果一个进程没有内存空间可用,它将会被换到磁盘上。内存和磁盘上的空闲空间可以使用位图或空闲区列表来记录。

现代计算机都有某种形式的虚拟内存。在最简单的形式中,每一个进程的地址空间被划分为同等大小的块,称为页面,页面可以被放入内存中任何可用的页框内。有多种页面置换算法,其中两个比较好的算法是老化算法和工作集时钟算法。

为了使分页系统工作良好,仅选择算法是不够的,还要关注诸如工作集的确定、存储器分配策略以及所需要的页面大小等问题。分段可以帮助处理在执行过程中大小有变化的数据结构,并能简化连接和共享。

分段还有利于为不同的段提供不同的保护。有时,可以把分段和分页结合起来,以提供一种二维的虚拟内存。MULTICS系统以及IntelPentium都是这样既支持分段也支持分页的系统。


面向ACG编程