操作系统 第9章 虚拟内存

操作系统第9章 虚拟内存

9.1 背景

第8章所介绍的内存管理算法都基于一个基本要求∶执行指令必须在物理内存中。满足这一要求的第一种方法是将整个进程放在内存中。动态载入能帮助减轻这一限制,但是它需要程序员特别小心并且需要一些额外的工作。

但实际上,许多情况下并不需要将整个程序放到内存中。

虚拟内存(virtual memory)将用户逻辑内存与物理内存分开。这在现有物理内存有限的情况下,为程序员提供了巨大的虚拟内存。虚拟内存使编程更加容易,因为程序员不再需要担心可用的有限物理内存空间,只需要关注所要解决的问题。

进程的虚拟地址空间就是进程如何在内存中存放的逻辑(或虚拟)视图。通常,该视图为进程从某一逻辑地址(如地址 O)开始,连续存放。根据第八章,物理地址可以按页帧来组织,且分配给进程的物理页帧也可能不是连续的。这就需要内存管理单元(MMU)将逻辑页映射到内存的物理页帧

在这里插入图片描述

如图 9.2所示,允许随着动态内存分配,堆可向上生长。类似地,还允许随着子程序的不断调用,栈可以向下生长。堆与栈之间的巨大空白空间(或洞)为虚拟地址的一部分,只有在堆与栈生长时,才需要实际的物理页。包括空白的虚拟地址空间称为稀地址空间

在这里插入图片描述

除了将逻辑内存与物理内存分开。虚拟内存也允许文件和内存通过共享页而为两个或多个进程所共享。这带来了如下优点∶

  • 通过将共享对象映射到虚拟地址空间,系统库可为多个进程所共享。
  • 类似地,虚拟内存允许进程共享内存。
  • 虚拟内存可允许在用系统调用 fork()创建进程期间共享页,从而加快进程创建。

9.2 按需调页

看看一个执行程序是如何从磁盘载入内存的。一种选择是在程序执行时,将整个程序载入到内存。另一种选择是在需要时才调入相应的页。这种技术称为按需调页(demand paging),常为虚拟内存系统所采用。对于按需调页虚拟内存,只有程序执行需要时才载入页,那些从未访问的页不会调入到物理内存。

按需调页系统类似于使用交换的分页系统(见图9.4),进程驻留在第二级存储器上(通常为磁盘)。当需要执行进程时,将它换入内存。不过,不是将整个进程换入内存,而是使用懒惰交换(lazy swapper)。懒惰交换只有在需要页时,才将它调入内存。

交换程序(swapper)对整个进程进行操作,而调页程序(pager)只是对进程的单个页进行操作。因此,在讨论有关按需调页时,需要使用调页程序而不是交换程序。

9.2.1 基本概念

当换入进程时,调页程序推测在该进程再次换出之前会用到哪些页。对这种方案,需要一定形式的硬件支持来区分哪些页在内存里,哪些页在磁盘上。在8.5节中所描述的**有效-无效位(valid-invalid bit)**可以用于这一目的。

如果进程从不试图访问标记为无效的页,那么程序正常执行。

当进程试图访问那些尚未调入到内存的页时,会产生页错误陷阱(page-fault trap)。分页硬件在通过页表转换地址时,将发现已设置了无效位,会陷入操作系统。这种陷阱是由于操作系统未能将所需的页调入内存引起的。处理这种页错误的程序比较简单:

  • 检查进程的内部页表(通常与PCB一起保存),以确定该引用是合法还是非法的地址访问。
  • 如果引用非法,那么终止进程。如果引用有效但是尚未调入页面,那么现在应调入。
  • 找到一个空闲帧(例如,从空闲帧链表中选取一个)。
  • 调度一个磁盘操作,以便将所需要的页调入刚分配的帧。
  • 当磁盘读操作完成后,修改进程的内部表和页表,以表示该页已在内存中。
  • 重新开始因陷阱而中断的指令。进程现在能访问所需的页,就好像它似乎总在内存中。

在这里插入图片描述

一种极端情况是所有的页都不在内存中,就开始执行进程。当操作系统将指令指针指向进程的第一条指令时,由于其所在的页并不在内存中,进程立即出现页错误。当页调入内存时,进程继续执行,并不断地出现页错误直到所有所需的页均在内存中。这时,进程可以继续执行且不出现页错误。这种方案称为**纯粹按需调页(pure demand paging)**∶只有在需要时才将页调入内存。

从理论上来说,有的程序的单个指令可能访问多个页的内存(一页用于指令,其他页用于数据),从而一个指令可能产生多个页错误。这种情况会产生令人无法接受的系统性能。

幸运的是,对运行进程的分析说明了这种情况是极为少见的。如 9.6.1 小节所述,程序具有局部引用(locality of reference),这使得按需调页的性能较为合理。

支持按需调页的硬件与分页和交换的硬件一样∶

  • 页表∶该表能够通过有效-无效位或保护位的特定值,将条目设为无效。

  • 次级存储器∶该次级存储器用来保存不在内存中的页。次级存储器通常为快速磁盘。它通常称为交换设备,用于交换的这部分磁盘称为交换空间(swap space)

请求调页的关键要求是能够在页错误后重新执行指令。在出现页错误时,保存中断进程的状态(寄存器、条件代码、指令计数器),必须能够按完全相同的位置和地址重新开始执行进程,只不过现在所需要的页已在内存中且可以访问。

对绝大多数情况来说,以上要求容易满足。主要的困难在于一个指令可能改变多个不同位置。例如,考虑一个IBM系统 360/370的MVC指令(移动字符),该指令能够将多达256B的块从一处移到另一处(可能重叠)。如果任何一块(源或目的)跨越页边界,在移动执行了部分后可能会出现页错误。另外,如果源和目的块有重叠,源块可能已经修改,这时并不能简单地再次执行该指令。

这说明了一些虚拟技术的困难。分页是加在计算机系统的 CPU和内存之间的。它应该对用户进程完全透明。这样,人们就通常假定分页能够应用到任何系统中。这个假定对于非按需调页环境来说是正确的,但对于按需调页环境来说是不正确的。

9.2.2 按需调页的性能

按需调页对计算机系统的性能有重要影响。为了说明起见,下面计算一下关于按需调页内存的有效访问时间(effective access time)。对绝大多数计算机系统而言,内存访问时间(用 ma 表示)的范围为 10~200 ns。只要没有出现页错误,那么有效访问时间等于内存访问时间。然而,如果出现页错误,那么就必须先从磁盘中读入相关页,再访问所需要的字。

设p为页错误的概率(0≤p≤1)。希望p接近于0,即页错误很少。那么有效访问时间为∶
有 效 访 问 时 间 = ( 1 − p ) × m a + p × 页 错 误 时 间 有效访问时间 =(1-p) × ma+p × 页错误时间 访=(1p)×ma+p×
设平均页错误处理时间为 8 m s 8ms 8ms,内存访问时间为 200 n s 200ns 200ns,那么有效内存访问时间 8.2 μ m 8.2\mu m 8.2μm ,即系统会因采用按需调页而慢10倍。若要性能降低不超过10%,则需要 p < 0.0000025 p < 0.0000025 p<0.0000025 ,即每399990次访问中出现不到一次页错误。因此,对于按需调页,降低页错误率是非常重要的。否则,有效访问时间会增加,会显著地降低进程的执行速度。

按需调页的另一个重要方面是交换空间的处理和使用。磁盘 I/O 到交换空间通常比到文件系统要快。如果在进程开始时将整个文件镜像复制到交换空间,并从交换空间执行按页调度,那么有可能获得更好的调页效果。另一选择是开始时从文件系统中进行按需调页,但是当出现页置换时则将页写入交换空间,这种方法确保只有所需的页才从文件系统中调入,而以后出现的调页是从交换空间中读入的

9.3 写时复制

9.2 节描述了一个进程如何采用按需调页,仅调入包括第一条指令的页,从而能很快地开始执行。但是,通过采用类似页面共享的技术,采用系统调用 fork创建进程的开始阶段可能不需要按需调页。这种技术提供了快速进程创建,且最小化新创建进程必须分配的新页面的数量

回想一下系统调用 fork是将子进程创建为父进程的复制品。传统上,fork为子进程创建一个父进程地址空间的副本,复制属于父进程的页。然而,由于许多子进程在创建之后通常马上会执行系统调用 exec,所以父进程地址空间的复制可能没有必要。因此,可以使用一种称为写时复制(copy-on-write)的技术。这种方法允许父进程与子进程开始时共享同一页面。这些页面标记为写时复制页,即如果任何一个进程需要对页进行写操作,那么就创建一个共享页的副本。写时复制如图 9.7 和图 9.8 所示;这两个图反映了进程 1修改页C前后的物理内存的情况。

在这里插入图片描述

当子进程试图修改写时复制页时,操作系统会创建一个该页的副本,并将它映射到子进程的地址空间内。采用写时复制技术,很显然只有能被进程修改的页才会被复制,所有非修改页可为父进程和子进程所共享。注意只有可能修改的页才需要标记为写时复制。不能修改的页(即包含可执行代码的页)可以为父进程和子进程所共享。

当确定一个页要采用写时复制时,从哪里分配空闲页是很重要的。许多操作系统为这类请求提供了空闲缓冲池(pool)。这些空闲页在进程栈或堆必须书展时可用于分配,或用于管理写时复制页。操作系统通常采用**按需填零(zero-fill-on-demand)**的技术以分配这些页。按需填零页在需要分配之前先填零,因此清除了以前的内容

许多UNIX版本(包括Solaris和Linux)也提供了fork的变种——vfork。vfork会将父进程挂起,子进程直接使用父进程的地址空间。由干vfork不使用写时复制,因此如果子进程修改父进程地址空间的任何页,那么这些修改过的页在父进程重启时是可见的。vfork主要用于在子进程被创建后立即调用exec的情况。由于没有出现复制页面,vfork是一种非常高效的进程创建方法,有时用于实现UNIX命令行 shell的接口。

9.4 页面置换

如果一个进程具有10页但事实上只使用其中的 5页,那么请求页面调度就可以节省用以装入从不使用的另 5 页所必需的 I/O。也可以通过运行两倍的进程以增加多道程序的程度。因此,如果有40帧,那么可以运行8个进程,而不是每个进程都需要10帧(其中5个决不使用)而只能运行4个进程。

如果增加了多道程序的程度,那么会**过度分配(over-allocating)**内存。对于有40帧的系统,如果运行6个进程,且每个进程有10页大小但事实上只使用其中的5页,那么可以通过只载入5页来获得更高的CPU利用率和吞吐量,且有10帧可作备用。然而,有可能每个进程,对于特定数据集合,会突然试图使用其所有的10页,从而产生共需要 60帧,而只有40 帧可用。

内存的过度分配会出现以下问题。当一个用户进程执行时,一个页错误发生。操作系统会确定所需页在磁盘上的位置,但是却发现空闲帧列表上并没有空闲帧,所有内存都在使用——

这时操作系统会有若干选择。它可以终止用户进程,也可以交换出一个进程,以释放其所有帧,并降低多道程序的级别。但更为常用的解决方法是:页置换(page replacement)

9.4.1 基本页置换

页置换采用如下方法。如果没有空闲帧,那么就香找当前没有使用的帧。并将其释放。可采用这样的方式样来释放一个帧∶将其内容写到交换空间,并改变页表(和所有其他表)以表示该页不在内存中(见图 9.10)。现在可使用空闲帧来保存进程出错的页。修改页错误处理程序以包括页置换∶

  • 查找所需页在磁盘上的位置
  • 查找一个空闲帧
    • 若有空闲帧,则使用它
    • 若没有,那么就使用页置换算法以选择一个**"牺牲"帧(victim frame)**。
    • 将"牺牲"帧的内容写到磁盘上,改变页表和帧表。
  • 将所需页读入(新)空闲帧,改变页表和帧表。
  • 重启用户进程。

注意,如果没有帧空闲,那么需要采用两个页传输(一个换出,一个换入)。这种情况实际上把页错误处理时间加倍了,相应地也增加了有效访问时间。

在这里插入图片描述

可以通过使用**修改位(modify bit)脏位(dirty bit)**以降低额外开销。每页或帧可以有一个修改位,通过硬件与之相关联。每当页内的任何字或字节被写入时,硬件就会设置该页的修改位以表示该页已被修改。此时,若该页被选为替换页,就必须要把该页写回到磁盘上去。但是,如果修改位没有设置,则说明该页和磁盘上的页内容一致,就没有必要重写该页。这种技术也适用于只读页(例如,二进制代码的页)。这种页不能被修改。因此,如需要,可以放弃这些页。这种方案可显著地降低用于处理页错误所需要的时间,因为如果牺牲页没有被修改,可以降低一半的 I/O时间。

为实现按需调页,必须解决两个主要问题∶必须开发帧分配算法(frame-allocation algorithm)页置换算法(page-replacement algorithm)。如果在内存中有多个进程,那么必须决定为每个进程各分配多少帧。而且,当需要页置换时,必须选择要置换的帧。

评估一个置换算法,通常使用最小页错误率算法。针对特定内存引用序列,运行某个置换算法,并计算出页错误的数量。内存的引用序列称为引用串(reference string)。可以人工地生成引用串(例
如,通过随机数生成器),或可跟踪一个给定系统并记录每个内存引用的地址。后一方法产生了大量数据(以每秒数百万个地址的速度)。为了降低数据量,可利用以下两个事实:

  • 第一,对给定页大小(页大小通常由硬件或系统来决定),只需要考虑页码,而不需要完整地址。
  • 第二,如果有一个对页p的引用,那么任何紧跟着的对页p 的引用决不会产生页错误。页p在第一次引用时已在内存中,任何紧跟着的引用不会出错。

针对某一特定引用串和页置换算法,为了确定页错误的数量,还需要知道可用帧的数量。显然,随着可用帧数量的增加,页错误的数量会相应地减少。通常,期待着如图9.11所示的曲线。随着帧数量的增加,页错误数量会降低至最小值。当然,增加物理内存就会增加帧的数量。

在这里插入图片描述

9.4.2 FIFO页置换

最简单的页置换算法是FIFO算法。FIFO 页置换算法为每个页记录着该页调入内存的时间。当必须置换一页时,将选择最旧的页。注意并不需要记录调入一页的确切时间。可以创建一个 FIFO队列来管理内存中的所有页。队列中的首页将被置换。当需要调入页时,将它加到队列的尾部。

在这里插入图片描述

注意,即使选择替代一个活动页,仍然会正常工作。当换出一个活动页以调入一个新页时,一个页错误几乎马上会要求换回活动页。这样某个页会被替代以将活动页调入内存。因此,一个不好的替代选择增加了页错误率,且减慢了进程执行,但是并不会造成不正确执行

为了说明与 FIFO 页置换算法相关可能问题,考虑如下引用串∶

1,2,3,4,1,2,5,1,2,3,4,5

图9.13显示页错误对现有帧数的曲线。注意到4帧的错误数(10)比3帧的错误数(9)还要大。这种最为令人难以置信的结果称为 **Belady 异常(Belady’s anomaly)**∶对有的页置换算法,页错误率可能会随着所分配的帧数的增加而增加,而原期望为进程增加内存会改善其性能。在早期研究中,研究人员注意到这种推测并不总是正确的。因此,发现了Belady异常。

在这里插入图片描述

9.4.3 最优置换

Belady 异常发现的结果之一是对最优页置换算法(optimal page-replacement algorithm)的搜索。最优页置换算法是所有算法中产生页错误率最低的,且绝没有 Belady 异常的问题。这种算法确实存在,它被称为OPTMIN。它会置换最长时间不会使用的页。使用这种页置换算法确保对于给定数量的帧会产生最低可能的页错误率。

在这里插入图片描述

例如,针对上图的引用串样例,最优置换算法会产生9个页错误,如图9.14所示。头3个引用会产生错误以填满空闲帧。对页2的引用会置换页7,这是因为页7直到第18次引用时才使用,而页0在第5次引用时使用,页1在第14次引用时使用。有9个页错误的最优页置换算法要好于有15个页错误的 FIFO算法((如果忽视头3个页错误(所有算法均会有的),那么最优置换要比 FIFO置换好一倍)。事实上,没有置换算法能只用3个帧且少于9个页错误就能处理该引用串。

然而,最优置换算法难以实现,因为需要引用串的未来知识。因此,最优算法主要用于比较研究。例如,如果知道一个算法不是最优,但是与最优相比最坏不差于12.3%平均不差于4.7%那么也是很有用的。

9.4.4 LRU页置换

如果最优算法不可行,那么最优算法的近似算法或许成为可能。FIFO和 OPT 算法的关键区别在于,FIFO 算法使用的是页调入内存的时间,OPT 算法使用的是页将来使用的时间。如果使用离过去最近作为不远将来的近似,那么可置换最长时间没有使用的页(见图 9.15),这种方法称为最近最少使用算法(least-recently-used(LRU)algorithm)

在这里插入图片描述

LRU 置换为每个页关联该页上次使用的时间。当必须置换一页时,LRU选择最长时间没有使用的页。这种策略为**向后看(而不是向前看)**的最优页置换算法。若用 S R S^R SR 表示引用串 S S S 的倒转,那么针对 S S S 的OPT算法的页错误率与针对 S R S^R SR 的OPT算法的页错误率是一样的,类似的,针对 S S S 的LRU算法的页错误率与针对 S R S^R SR 的LRU算法的页错误率是一样的。

LRU 策略经常用做页置换算法,且被认为相当不错。其主要问题是如何实现 LRU 置换。LRU页置换算法可能需要一定的硬件支持。它需要为页帧确定一个排序序列,这个序列按页帧上次使用的时间来定。有两种可行实现∶

  • 计数器:为每个页表项关联一个使用时间域,并为CPU增加一个逻辑时钟或计数器。每次内存引用,计数器都会增加,且内存引用时,时间寄存器的值会被复制到相应页所对应的页表项的使用时间域内

    这种方案在每次置换时都需要搜索页表以查找LRU页,且每次内存访问都要写入内存(到页表的使用时间域)。在页表改变时(因CPU调度)也必须保持时间。必须考虑时钟溢出。

  • :实现LRU置换的另一个方法是采用页码栈。每当引用一个页,该页就从栈中删除并放在顶部。这样,栈顶部总是最近使用的页,栈底部总是LRU 页(图9.16)。由于必须从栈中部删除项,所以该栈可实现为具有头指针和尾指针的双向链表。这样,删除一页并放在栈顶部在最坏情况下需要改变6个指针。虽说每个更新有点费时,但是置换不需要搜索;尾指针指向栈底部,就是LRU页。对于用软件或微代码的LRU置换的实现,这种方法十分合适。

    在这里插入图片描述

最优置换和LRU置换都没有Belady异常。这两个实际上都属于同一类算法,称为栈算法(stack algorithm),都绝对不可能有Belady异常

注意,如果只有标准TLB寄存器而没有其他硬件支持,那么这两种LRU实现都是不可能的。每次内存引用都必须更新时钟域或栈。如果对每次引用都采用中断,以允许软件更新这些数据结构,那么它会使内存引用慢至少10倍,进而使用户进程运行慢10倍。几乎没有系统可以容忍如此程度的内存管理的开销。

9.4.5 近似LRU页置换

很少有计算机系统能提供足够的硬件来支持真正的LRU页置换。有的系统不提供任何支持,因此必须使用其他置换算法(如 FIFO算法)。然而,许多系统都通过引用位方式提供一定的支持。页表内的每项都关联着一个引用位(reference bit)。每当引用一个页时(无论是对页的字节进行读或写),相应页表的引用位就被硬件置位。

开始,操作系统会将所有引用位都清零。随着用户进程的执行,与引用页相关联的引用位被硬件置位(置为1)。之后,通过检查引用位,能够确定哪些页使用过而哪些页未使用过。虽然不知道使用顺序,但是知道哪些页用过而哪些页未用过。这信息是许多近似LRU页置换算法的基础。

9.4.5.1 附加引用位算法

通过在规定时间间隔里记录引用位的历史信息,可以获得额外顺序信息。可以为位于内存内的每
个表中的页保留一个8位的字节。在规定时间间隔(如,每100 ms)内,时钟定时器产生中断并将控制转交给操作系统。操作系统把每个页的引用位转移到其8位字节的高位,而将其他位向右移一位,并抛弃最低位。

这些8位移位寄存器包含着该页在最近8个时间周期内的使用情况。

如果移位寄存器含有00000000,那么该页在8个时间周期内没有使用,如果移位寄存器的值为1111111,那么该页在过去每个周期内都至少使用过一次。具有值为11000100 的移位寄存器的页要比值为01110111的页更为最近使用。如果将这 8位字节作为无符号整数,那么具有最小值的页为 LRU 页,且可以被置换。注意这些数字并不唯一。可以置换所有具有最小值的页,或在这些页之间采用FIFO来选择置换。

当然,历史位的数量可以修改,可以选择(依赖于可用硬件)以尽可能快地更新。在极端情况下,数量可降为 0,即只有引用位本身。这种算法称为第二次机会页置换算法(second-chance page-replacement algorithm)。

9.4.5.2 二次机会算法

二次机会置换的基本算法是 FIFO 置换算法。当要选择一个页时,检查其引用位。如果其值为0,那么就直接置换该页。如果引用位为1,那么就给该页第二次机会,并选择下一个FIFO页

当一个页获得第二次机会时,其引用位清零,且其到达时间设为当前时间。因此,获得第二次机会的页在所有其他页置换(或获得第二次机会)之前,是不会被置换的。另外,如果一个页经常使用以致其引用位总是被设置,那么它就不会被置换。

一种实现二次机会算法(有时称为时钟算法)的方法是采用循环队列。用一个指针表示下次要置换哪一页。当需要一个帧时,指针向前移动直到找到一个引用位为0的页。在向前移动时,它将清除引用位(见图 9.17)。一旦找到牺牲页,就置换该页,新页就插入到循环队列的该位置。注意∶在最坏情况下,所有位均已设置,指针会遍历整个循环队列,以便给每个页第二次机会。它将清除所有引用位后再选择页来置换。这样,如果所有位均已设置,那么二次机会置换就变成了FIFO置换

在这里插入图片描述

9.4.5.3 增强型二次机会算法

通过将引用位和修改位(在 9.4.1 小节中介绍过)作为一个有序对来考虑,可以改进二次机会算法。采用这两个位,有下面四种可能类型∶

  • (0, 0)最近既没有使用也没有修改——最佳的置换页
  • (0, 1)最近没有使用但修改过——不是很好的选择,因为置换之前需要将页写出到磁盘
  • (1, 0)最近使用过但没有修改——它可能很快又要被使用
  • (1, 1)最近既被使用过又被修改过——最坏的选择,它有可能很快又要被使用,且置换之前还要将页写出到磁盘

每个页面必然都属于上述这四种类型的集合。当需要页面置换时,可以采用与时钟算法一样的策略:检查页面的类型(不是仅仅检查引用位),我们替换掉最低类型中的一个页面(如果这一类型的页面有的话)。因为可能并不存在(0,0)类型的页面,这时就选择(0,1)类型的页面,依此类推。增强型第二次机会算法的亮点在于它赋予了那些被修改过的页面更高的优先级,从而降低了所需要I/O的数量。注意在找到要置换页之前,可能要多次搜索整个循环队列

9.4.6 基于计数的页置换

  • **最不经常使用页置换算法(least frequently used (LFU) page-replacement algorithm)**要求置换计数最小的页。这种选择的理由是活动页应该有更大的引用次数。

    这种算法会产生如下问题∶一个页在进程开始时使用很多,但以后就不再使用。由于其使用过很多,所以它有较大次数,所以即使不再使用仍然会在内存中。解决方法之一是定期地将次数寄存器右移一位,以形成指数衰减的平均使用次数

  • **最常使用页置换算法(most frequently used (MFU) page-replacement algorithm)**是基于如下理论∶具有最小次数的页可能刚刚调进来,且还没有使用。

MFU和LFU置换都不常用。这两种算法的实现都很费时,且并不能很好地近似OPT置换算法。

9.4.7 页缓冲算法

除了特定页置换算法外,还经常采用其他措施。例如,系统通常保留一个空闲帧缓冲池。当出现页错误时,会像以前一样选择一个牺牲帧。然而,在牺牲帧写出之前,所需要的页就从缓冲池中读到空闲内存。这种方法允许进程尽可能快地重启,而无须等待牺牲帧页的写出。当在牺牲帧以后写出时,它再加入到空闲帧池。

这种方法的扩展之一是维护一个已修改页的列表。每当调页设备空闲时,就选择一个修改页并写到磁盘上,接着重新设置其修改位。这种方案增加了当需要选择置换时干净页出现的概率,减少了写出。

另一种修改是保留空闲帧池中的数据,且要记住哪些页在哪些帧中。由于当帧写到磁盘上时其内容并没有修改,所以在该帧被重用之前如果需要使用原来页,那么原来页可直接从空闲帧池中取出来使用。这时并不需要 I/O。当一个页错误发生时,先检查所需要页是否在空闲帧池中。如果不在,那么才必须选择一个空闲帧来读入所需页。

这种技术与FIFO置换算法一起用于VAX/VMS系统中。当 FIFO置换算法错误地置换了一个常用页时,该页会从空闲帧池中很快调出,而不需要 I/O。这种空闲缓冲池提供了相对差但却简单的 FIFO 置换算法的弥补。

这可用来改进任何页替换算法,以降低因错误选择牺牲页而引起的开销

9.5 帧分配

下面研究如何分配的问题。如何在各个进程之间分配一定的空闲内存?如果有 93 个空闲帧和 2个进程,那么每个进程各得到多少帧?

最为简单情况是单用户系统。考虑一个单用户系统,其页大小为 1KB,其总内存为128KB。因此,共有128 帧。操作系统可能使用 35KB,这样用户进程可以使用93帧。如果采用纯按需调页,那么所有93 帧开始都放在空闲链表上。当用户进程开始执行时,它会产生一系列页错误。头 93页错误会从空闲帧链表中获得帧。当空闲帧链表用完后,必须使用页置换算法以从位于内存的 93个页中选择一个置换为第94 页,以此类推。当进程终止时,这 93个帧将再次放在空闲帧链表上

9.5.1 帧的最少数量

帧分配策略受到多方面的限制。例如,所分配的帧不能超过可用帧的数量(除非有页共享),也必须分配至少最少数量的帧。这里对后者作一讨论。

分配至少最少数量的帧的原因之一是性能。显然,随着分配给每个进程的帧数量的减少,页错误会增加,从而减慢进程的执行。另外,记住∶当在指令完成之前出现页错误时,该指令必须重新执行。因此,必须有足够的帧来容纳所有单个指令所引用的页

帧的最少数量是由给定计算机结构定义的。例如,PDP-11的移动指令的长度在一些寻址模式下为多个字长,因此指令本身可能跨在2个页上。另外,它有2个操作数,而每个操作数都可能是间接引用,因此,共需要6个帧。

最坏情况出现在如下结构的计算机中:它们允许多层的间接(例如,每个 16 位的字可能包括一个15位的地址和1位的间接标记符)。从理论上来说,一个简单 load指令可以引用另一个间接地址,而它可能又引用另一个间接地址(在另一页上),而它可能又再次引用另一个间接地址(又在另一页上),以此类推,直到所涉及的页都在虚拟内存中。因此,在最坏情况下,整个虚拟内存都必须在物理内存中。

为了解决这一困难,必须对间接引用加以限制(例如,限制一个指令只能有16级的间接引用)。当出现首次间接引用时,计数器设置为 16;对该指令以后的每次间接引用,该计数器要减1。如果计数器减为0,那么出现陷阱(过分间接引用)。这种限制使得每个指令的最大内存引用为 17,因而也要求同样数量的帧。

每个进程帧的最少数量是由体系结构决定的,而最大数量是由可用物理内存的数量来决定。在这两者之间,关于帧分配还是有很多选择的。

9.5.2 分配算法

在n个进程之间分配 m个帧的最为容易的方法是给每个一个平均值,即 m/n 帧。例如,如果有93个帧和5个进程,那么每个进程可得到18个帧,剩余3个帧可以放在空闲帧缓存池中。这种方案称为平均分配(equal allocation)

另外一种方法是要认识到各个进程需要不同数量的内存。可使用比例分配(proportional allocation)。根据进程大小,而将可用内存分配给每个进程。设进程 p i p_i pi 的虚拟内存大小为 s i s_i si ,且定义:
S = ∑ s i S = \sum{s_i} S=si
若可用帧的总数为 m m m,那么进程 p i p_i pi 可分配到 a i a_i ai 个帧,则 a i a_i ai 近似为:
a i = s i S ∗ m a_i = \frac{s_i}{S} * m ai=Ssim
当然,对于平均和比例分配,每个进程所分配的数量会随着多道程序的级别而有所变化。如果多道程序程度增加,那么每个进程会失去一些帧来提供给新进程使用。另一方面,如果多道程序程度降低,那么原来分配给离开进程的帧可以分配给剩余进程。

注意,对于平均或比例分配,高优先级进程与低优先级进程一样处理。然而,根据定义,可能要给高优先级更多内存以加快其执行,同时就会损害到低优先级进程。另一个方法是使用比例分配的策略,但是不根据进程相对大小,而是根据进程优先级,或大小和优先级的组合

9.5.3 全局分配和局部分配

为各个进程分配帧的另一个重要因素是页置换。当有多个进程竞争帧时,可将页置换算法分为两大类∶全局置换(global replacement)局部置换(local replacement)。全局置换允许一个进程从所有帧集合中选择一个置换帧,而不管该帧是否已分配给其他进程,即一个进程可以从另一个进程中拿到帧。局部置换要求每个进程仅从其自己的分配帧中进行选择

采用局部置换策略,分配给每个进程的帧的数量不变。采用全局置换,一个进程可能从分配给其他进程的帧中选择一个进行置换,因此增加了所分配的帧的数量(假定其他进程不从它这里选择帧来置换)。

例如,考虑这样一个分配方案∶允许高优先级进程从低优先级进程中选择帧以便置换。一个进程可以从自己的帧中或任何低优先级进程中选择置换帧。这种方法允许高优先级进程增加其帧分配而以损失低优先级进程为代价。

全局置换算法的一个问题是进程不能控制其页错误率。一个进程的位于内存的页集合不但取决于进程本身的调页行为,还取决于其他进程的调页行为。因此,相同进程由于外部环境不同,可能执行很不一样(有的执行可能需要0.5秒,而有的执行可能需要 10.3秒)。局部置换算法就没有这样的问题。在局部置换下,进程内存中的页只受该进程本身的调页行为所影响。但是,因为局部置换不能使用其他进程的不常用的内存,所以会阻碍一个进程。因此,全局置换通常会有更好的系统吞吐量,且更为常用

9.6 系统颠簸

如果低优先级进程所分配的帧数量少于计算机体系结构所要求的最少数量,那么必须暂停进程执行。接着应换出其他所有剩余页,以便使其重新分配所有的空闲帧。这引入了中程CPU调度的换进换出层。

事实上,需要研究一下没有"足够"帧的进程。如果进程没有它所需要的活跃使用的帧,那么它会很快产生页错误。这时,必须置换某个页。然而,其所有页都在使用,它置换一个页,但又立刻再次需要这个页。因此,它会一而再地产生页错误,置换一个页,而该页又立即出错且需要立即调进来。

这种频繁的页调度行为称为颠簸(thrashing)。如果一个进程在换页上用的时间要多于执行时间,那么这个进程就在颠簸。

9.6.1 系统颠簸的原因

颠簸将导致严重的性能问题。考虑如下情况,这是基于早期调页系统的真实行为。

操作系统在监视 CPU 的使用率。如果CPU使用率太低,那么向系统中引入新进程,以增加多道程序的程度。采用全局置换算法,它会置换页而不管这些页是属于哪个进程的。现在假设一个进程进入一个新执行阶段,需要更多的帧。它开始出现页错误,并从其他进程中拿到帧。然而,这些进程也需要这些页,所以它们也会出现页错误,从而从其他进程中拿到帧。这些页错误进程必须使用调页设备以换进和换出页。随着它们排队等待换页设备,就绪队列会变空,而进程等待调页设备,CPU使用率就会降低

CPU调度程序发现CPU使用率降低,因此会增加多道程序的程度。新进程试图从其他运行进程中拿到帧,从而引起更多页错误,形成更长的调页设备的队列。因此,CPU使用率进一步降低,CPU 调度程序试图再增加多道程序的程度,形成恶性循环。这样就出现了系统颠簸,系统吞吐量陡降,页错误显著增加。因此,有效内存访问时间增加。最终因为进程主要忙于调页,系统不能完成一件工作。

这种现象如图9.18所示,显示了 CPU 使用率与多道程序程度的关系。随着多道程序程度增加,CPU使用率(虽然有点慢)增加,直到达到最大值。如果多道程序的程度还要继续增加,那么系统颠簸就开始了,且CPU使用率急剧下降。这时,为了增加 CPU使用率和降低系统颠簸,必须降低多道程序的程度。

在这里插入图片描述

通过局部置换算法(local replacement algorithm)(或优先置换算法(priority replacement algorithm))能限制系统颠簸。采用局部置换,如果一个进程开始颠簸,那么它不能从其他进程拿到帧,且不能使后者也颠簸。然而这个问题还没有完全得到解决。如果进程颠簸,那么绝大多数时间内也会排队来等待调页设备。由于调页设备的更长的平均队列,页错误的平均等待时间也会增加。因此,即使对没有颠簸的进程,其有效访问时间也会增加

为了防止颠簸,必须提供进程所需的足够多的帧。但是如何知道进程"需要"多少帧呢?有多种技术。工作集合策略(9.6.2节)是研究一个进程实际正在使用多少帧。这种方法定义了进程执行的局部模型(locality model)

模型说明,当进程执行时,它从一个局部移向另一个局部。局部是一个经常使用页的集合(见图9.19)。一个程序通常由多个不同局部组成,它们可能重叠。

局部是由程序结构和数据结构来定义的。局部模型说明了所有程序都具有这种基本的内存引用结构。注意局部模型是本书到目前为止还未明说的原理,它是缓存的基础。如果对任何数据类型的访问是随机的而没有一定的模式,那么缓存就没有用了。

假设为每个进程都分配了可以满足其当前局部的帧。该进程在其局部内会出现页错误,直到所有页均在内存中,接着它不再会出现页错误直到它改变局部为止。如果分配的帧数少于现有局部的大小,那么进程会颠簸,这是因为它不能将所有经常使用的页放在内存中

9.6.2 工作集合模型

工作集合模型(working-set model)是基于局部性假设的。该模型使用参数 Δ \Delta Δ 定义工作集合窗口(working-set window)。其思想是检查最近 Δ \Delta Δ 个页的引用。这最近 Δ \Delta Δ 个引用的页集合称为工作集合(working set)(如图9.20所示)。如果一个页正在使用中,那么它就在工作集合内。如果它不再使用,那么它会在其上次引用的 Δ \Delta Δ 时间单位后从工作集合中删除。因此,工作集合是程序局部的近似。

在这里插入图片描述

例如,对于如图 9.20所示的内存引用序列,如果 Δ \Delta Δ 为 10个内存引用,那么 t 1 t_1 t1 时的工作集合为{1,2,5,6,7}。到 t 2 t_2 t2 时,工作集合则为{3,4}。

工作集合的精确度与 Δ \Delta Δ 的选择有关。如果 Δ \Delta Δ 太小,那么它不能包含整个局部,如果 Δ \Delta Δ 太大,那么它可能包含多个局部。

工作集合的最为重要的属性是其大小。如果经计算而得到系统内每个进程的工作集合为 W S S i WSS_i WSSi ,那么就得到 D = ∑ W S S i D = \sum{WSS_i} D=WSSi ,其中 D D D 为总的帧需求量。若总需求量大于可用帧数量,那么有的进程会得不到足够的帧,从而会出现颠簸。

一旦确定了 Δ \Delta Δ ,那么工作集合模型的使用就较为简单。操作系统跟踪每个进程的工作集合,并为进程分配大于其工作集合的帧数。如果还有空闲帧,那么可启动另一进程。如果所有工作集合之和的增加超过了可用帧的总数,那么操作系统会选择暂停一个进程。该进程的页被写出,且其帧可分配给其他进程。挂起的进程可以在以后重启

工作集合模型的困难是跟踪工作集合。工作集合窗口是移动窗口。在每次引用时,会增加新引用,而最老的引用会失去。如果一个页在工作集合窗口内被引用过,那么它就处于工作集合内

通过固定定时中断和引用位,能近似模拟工作集合模型。例如,假设 Δ \Delta Δ 为 10000个引用,且每 5000个引用会产生定时中断。当出现定时中断时,先复制再清除所有页的引用位。因此,当出现页错误时,可以检查当前引用位和位于内存内的两个位,从而确定在过去的 10 000 到 15000个引用之间该页是否被引用过。如果使用过,至少有一个位会为1。如果没有使用过,那么所有这3个位均为0。只要有1个位为1,那么就可认为处于工作集合中。注意,这种安排并不完全准确,这是因为并不知道在5000个引用的什么位置出现了引用。通过增加历史位的位数和中断频率(例如,10位和每1000个引用就产生中断),可以降低这一不确定性。然而,处理这些更为经常的中断的时间也会增加。

9.6.3 页错误频率

工作集合模型是成功的,工作集合知识能用于预先调页(参见9.9.1小节),但是用于控制颠簸有点不太灵活。一种更为直接的方法是采用**页错误频率(page-fault frequency,PFF)**策略。

颠簸具有高的页错误率。因此,需要控制页错误率。当页错误率太高时。进程需要更多帧。类似地。如果页错误率太低,那么进程可能有太多的帧。可以为所期望的页错误率设置上限和下限(见图9.21)。如果实际页错误率超过上限,那么为进程分配更多的帧;如果实际页错误率低于下限,那么可从该进程中移走帧。因此,可以直接测量和控制页错误率以防止颠簸

与工作集合策略一样,也可能必须暂停一个进程。如果页错误增加且没有可用帧,那么必须选择一个进程暂停。接着,可将释放的帧分配给那些具有高页错误率的进程

在这里插入图片描述

进程的工作集合和它的页错误率之间有直接的关系。当对一个新的局部按需调页时,页错误率进入波峰。一旦新局部的工作集合在内存内,页错误率开始下降。当进程进入一个新的工作集合,页错误率又一次升到波峰,然后随着工作集合载入到内存而再次降到波谷。从一个波峰的开始到下一个波峰的开始,这一时间跨度显示了工作集合的迁移

在这里插入图片描述

9.7 内存映射文件

内存映射(memory mapping)文件,是由一个文件到一块内存的映射,它允许一部分虚拟内存与文件逻辑相关联。Win32提供了允许应用程序把文件映射到一个进程的函数 (CreateFileMapping)。内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而且在对该文件进行操作之前必须首先对文件进行映射。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。

有些操作,如放弃“读”一个字符,在以前是相当复杂的,用户需要处理缓冲区的刷新问题。在引入了映射文件之后,就简单的多了。应用程序要做的只是使指针减少一个值。

使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。

多个进程可以允许将同一文件映射到各自的虚拟内存中,以允许数据共享。其中任一进程修改虚拟内存中的数据,都会为其他映射相同文件部分的进程所见。根据虚拟内存的相关知识,可以清楚地看到内存映射部分的共享是如何实现的∶每个共享进程的虚拟内存表都指向物理内存的同一页,该页有磁盘块的复制。

另外,实际工程中的系统往往需要在多个进程之间共享数据,如果数据量小,处理方法是灵活多变的,如果共享数据容量巨大,那么就需要借助于内存映射文件来进行。实际上,内存映射文件正是解决本地多个进程间数据共享的最有效方法

9.8 内核内存的分配

当用户态进程需要额外内存时, 可以从内核所维护的空闲页帧链表中获取页。该链表通常由页替换算法(如 9.4 节所述的)来更新,且如前所述,这些页帧通常分散在物理内存中。另外,请记住,如果用户进程只需要一个字节的内存,那么会产生内部碎片,这是因为进程会得到整个页帧。

但是,内核内存的分配通常是从空闲内存池中获取的,而并不是从满足普通用户模式进程的内存链表中获取的。这主要有两个原因∶

  • 内核需要为不同大小的数据结构分配内存,其中有的不到一页。因此,内核必须谨慎使用内存,并试图减低碎片浪费。这点非常重要,因为许多操作系统的内核代码与数据不受分页系统控制
  • 用户进程所分配的页不必要在连续的物理内存中。然而,有的硬件要直接与物理内存打交道,而不需要经过虚拟内存接口,因此需要内存常驻在连续的物理页中

下面讨论对内核进程进行内存管理的两个方法。

9.8.1 Buddy系统

"Buddy 系统"从物理上连续的大小固定的段上进行分配。内存按2的幂的大小来进行分配,即4 KB、8 KB、16KB等。如果请求大小不为 2 的幂,那么需要调整到下一个更大的 2 的幂。例如,请求大小11KB,那么会按16KB来请求。下面用一个简单的例子来解释 Buddy 系统的操作。

假定内存段的大小原来为256KB,而内核申请 21KB 的内存。这样原来的段就先分为两个段 A L A_L AL A R A_R AR ,其大小均为128KB。其中之一又分为 B L B_L BL ,和 B R B_R BR ,其大小均为64 KB。接着, B L B_L BL B R B_R BR 。又分为 C L C_L CL ,和 C R C_R CR ,其大小均为32 KB。如果再分,就得到16KB的段,而这太小了,不能满足 21KB的请求。因此,其中一个32 KB的段可用来满足 21KB。这种方案如图9.27所示,其中 C L C_L CL 用来满足21 KB的内存请求。

在这里插入图片描述

Buddy 系统的一个优点是可通过合并而快速地形成更大的段。例如,如果图 9.27中的 C L C_L CL 被释放,那么 C L C_L CL C R C_R CR 可合并成64 KB的段。而这个段 B L B_L BL 又可同 B R B_R BR 合并而得到128KB的段。最终,得到了原来的大小为256 KB的段。

Buddy 系统的一个明显缺点是由于调整到下一个2 的幂容易产生碎片。例如,33 KB的内存请求只能用64 KB的段来满足。事实上,可能有 50% 内存会因碎片而浪费。下面讨论另一种没有碎片损失的内存分配。

9.8.2 slab分配

内核分配的另一种方案是slab分配。slab是由一个或多个物理上连续的页组成的。高速缓存(cache)含有一个或多个slab每种内核数据结构都有一个cache,如进程描述符、文件对象、信号量等。每个 cache 中含有若干个内核数据结构的对象实例。例如,信号量 cache 存储着信号量对象,进程描述符 cache 存储着进程描述符对象。图9.28描述slab、cache 及对象三者之间的关系。该图中有两个3KB大小的内核对象和三个7KB大小的内核对象。它们分别位于各自的 cache 上。

在这里插入图片描述

slab 分配算法采用 cache 存储内核对象。当创建 cache 时,起初包括若干标记为空闲的对象。对象的数量与slab 的大小有关。例如,12KB的 slab(包括三个连续的页)可存储 6 个 2 KB大小的对象。开始,所有对象都标记为空闲。当需要内核数据结构的对象时,可以从 cache 上直接获取,并将该对象标记为使用(used)

下面考虑内核如何将 slab分配给表示进程描述符的对象。在Linux系统中,进程描述符的类型是 struct task struct,其大小约为1.7 KB。当Linux 内核创建新任务时,它会从cache中获得 struct task struct 对象所需要的内存。Cache 上会有已分配好的并标记为空闲的 struct task_struct 对象来满足请求

Linux的slab可有三种状态∶

  • 满的∶slab中的所有对象被标记为使用。
  • 空的∶slab中的所有对象被标记为空闲。
  • 部分∶ slab中的对象有的被标记为使用,有的被标记为空闲。

slab分配器首先从部分空闲的slab进行分配。如没有,则从空的slab 进行分配。如没有,则从物理连续页上分配新的 slab,并把它赋给一个cache,然后再从新 slab分配空间

slab分配器有两个主要优点∶

  • 没有因碎片而引起的内存浪费。碎片不是问题,这是因为每种内核数据结构都有相应的 cache,而每个cache 都由若干 slab 组成,而每个 slab 又分为若干个与对象大小相同的部分。因此,当内核请求对象内存时,slab 分配器可以返回刚好可以表示对象所需的内存
  • 内存请求可以快速满足。slab分配器对于需要经常不断分配内存、释放内存来说特别有效,而操作系统经常这样做。内存分配与释放可能费时。然而,由于对象预先创建,所以可从cache上快速分配。另外,当用完对象并释放时,只需要标记为空闲并返回给cache,以便下次再用
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值