虚拟内存
前言
- 所有内存管理策略都是为了同时将多个进程存放在内存中,以便允许多道程序设计。不过,这些策略都需要在进程执行之前将整个进程放在内存中。
- 虚拟内存技术允许执行进程不必完全在内存中。这种方案的优点就是程序可以比物理内存大。而且,虚拟内存将内存抽象成一个巨大的、统一的存储数组,将用户看到的逻辑内存与物理内存分开。这允许程序员不受内存存储的限制。虚拟内存也允许进程容易地共享文件和地址空间,还为创建进程提供了有效的机制。但虚拟内存的实现并不容易,如果使用不当会大大地降低性能。
背景
许多情况下不需要将整个程序放到内存中。例如:
- 程序通常有处理异常错误条件的代码。这种代码几乎不执行。
- 数组、链表和表通常分配了比实际所需要的更多的内存
- 程序的某些选项或功能可能很少使用
- 即使在需要完整程序时,也并不是同时需要所有的程序。
能够执行只有部分在内存中的程序可带来很多好处:
- 程序不再受现有的物理内存空间限制。用户可以为一个巨大的虚拟地址空间(virtualaddress space)编写程序,简化了编程工作量。
- 每个用户程序使用更少的物理内存,更多程序可以同时执行,CPU使用率也相应增加,而响应时间或周转时间并不增加
- 由于载入或交换每个用户程序到内存内所需的IO会更少,用户程序会运行得更快。
虚拟内存(virtual memory〉将用户逻辑内存与物理内存分开。物理内存有限情况下,为程序员提供了巨大的虚拟内存。虚拟内存使编程更加容易,因为程序员不需要担心可用的有限物理内存空间,只需要关注所要解决的问题。
进程的虚拟地址空间就是进程如何在内存中存放的逻辑(或虚拟)视图。通常该视图为进程从某一逻辑地址(如地址0)开始,连续存放。内存管理单元(MMU)将逻辑页映射到内存的物理页帧。

除了将逻辑内存与物理内存分开,虚拟内存也允许文件和内存通过共享页而为两个或多个进程所共享。这带来了如下优点:
- 通过将共享对象映射到虚拟地址空间,系统库可为多个进程所共享。虽然进程都认为共享库是其虚拟地址空间的一部分,而共享库所用的物理内存的实际页是为所有进程所共享。通常,库是按只读方式来链接每个进程的空间。
- 虚拟内存允许进程共享内存。虚拟内存允许一个进程创建内存区域,以便与其他进程进行共享。共享该内存区域的进程认为它是其虚拟地址空间的一部分,而事实上是共享的。
- 虚拟内存可允许在用系统调用fork()创建进程期间共享页,从而加快进程创建。

按需调页
描述
- 按需调页(demand paging),常为虚拟内存系统所采用。对于按需调页虚拟内存,只有程序执行需要时才载入页,那些从未访问的页不会调入到物理内存。
- 按需调页系统类似于使用交换的分页系统,进程驻留在第二级存储器上(通常为磁盘)。当需要执行进程时,将它换入内存。不过,不是将整个进程换入内存,而是使用懒惰交换(lazy swapper)。懒惰交换只有在需要页时,才将它调入内存。由于将进程看做是一系列的页,而不是一个大的连续空间,因此使用交换从技术上来讲并不正确。交换程序(swapper)对整个进程进行操作,而调页程序( pager)只是对进程的单个页进行操作。
基本概念
- 当换入进程时,调页程序推测该进程换出之前会用到哪些页。调页程序把必需页调入内存。这样,调页程序就避免了读入那些不使用的页,也减少了交换时间和所需的物理内存空间。
- 对这种方案,需要一定形式的硬件支持来区分哪些页在内存里,哪些页在磁盘上。有效-无效位(valid-invalid bit)可以实现。不过,现在当该位设置为“有效”时,该值表示相关的页既合法且也在内存中。当该位设置为“无效”时,该值表示相关的页为无效(也就是,不在进程的逻辑地址空间内),或者有效但是在磁盘上。

但是当进程试图访问那些尚未调入到内存的页时,对标记为无效的访问会产生页错误陷阱( page-fault trap)。分页硬件发现无效位,会陷入操作系统。这种陷阱是由于操作系统未能将所需的页调入内存引起的。处理这种页错误的程序比较简单:
- 检查进程的内部页表(通常与PCB一起保存),以确定该引用是合法还是非法的地址访问。
- 如果引用非法,那么终止进程。如果引用有效但是尚未调入页面,那么现在应调入。
- 找到一个空闲帧。
- 调度一个磁盘操作,将所需要的页调入刚分配的帧。
- 当磁盘读操作完成后,修改进程的内部表和页表,以表示该页已在内存中。
- 重新开始因陷阱而中断的指令,进程现在能访问所需的页

- 一种极端情况是所有的页都不在内存中,就开始执行进程。当操作系统将指令指针指向进程的第一条指令时,由于其所在的页并不在内存中,进程立即出现页错误。当页调入内存时,进程继续执行,并不断地出现页错误直到所有所需的页均在内存中。这时,进程可以继续执行且不出现页错误。这种方案称为纯粹按需调页(pure demand paging):只有在需要时才将页调入内存。
按需调页性能
有效访问时间(effective access time)。对绝大多数计算机系统而言,内存访问时间(用ma表示)的范围为10~200 ns。只要没有出现页错误,那么有效访问时间等于内存访问时间。然而,如果出现页错误,那么就必须先从磁盘中读入相关页,再访问所需要的字。
设p 为页错误的概率(O≤p≤1)。希望p接近于0,即页错误很少。那么有效访问时间为:
- 有效访问时间=(1-p) ×ma +p ×页错误时间
为了计算有效访问时间,必须知道处理页错误需要多少时间。页错误主要有如下处理时间:处理页错误中断,读入页,重新启动进程。
当页错误率越高时,有效处理时间会增加,这会显著的降低进行的执行速度。
写时复制
- 传统上, fork为子进程创建一个父进程地址空间的副本,复制属于父进程的页。子进程在创建之后通常会执行系统调用exec(),所以父进程地址空间的复制没有必要。可以使用一种称为写时复制(copy-on-write)的技术。这种方法允许父进程与子进程开始时共享同一页面。这些页面标记为写时复制页,即如果任何一个进程需要对页进行写操作,那么就创建一个共享页的副本。

- 当确定一个页要采用写时复制时,从哪里分配空闲页是很重要的。许多操作系统为这类请求提供了空闲缓冲池(pool)。这些空闲页在进程栈或堆必须扩展时可用于分配,或用于管理写时复制页。操作系统通常采用按需填零(zero-fill-on-demand)的技术以分配这些页。按需填零页在需要分配之前先填零,因此清除了以前的内容。
页面置换
前言
- 如果增加了多道程序的程度,那么会过度分配(over-allocating)内存,一般指将少数内存分配给了较多进程,导致内存不够用。
- 内存的过度分配会出现以下问题。当一个用户进程执行时,一个页错误发生。操作系统会确定所需页在磁盘上的位置,但是却发现空闲帧列表上并没有空闲帧,所有内存都在使用。
- 这时操作系统会有若干选择。它可以终止用户进程,但并不是最佳选择。操作系统也可以交换出一个进程,以释放其所有帧,并降低多道程序的级别,这种选择在有些环境下是好的。一个更为常用的解决方法是页置换(page replacement)。
基本页置换
页置换采用如下方法。修改页错误处理程序包括页置换:
- 查找所需页在磁盘上的位置。
- 查找一个空闲帧:
- 如果有空闲帧,那么就使用它。
- 如果没有空闲帧,那么就使用页置换算法以选择一个“牺牲”帧(victim frame)。
- 将“牺牲”帧的内容写到磁盘上,改变页表和帧表。
- 将所需页读入(新)空闲帧,改变页表和帧表。
- 重启用户进程。
注意,如果没有帧空闲,那么需要采用两个页传输(一个换出,一个换入)。这种情况实际上把页错误处理时间加倍了,且也相应地增加了有效访问时间。
可以通过使用修改位(modify bit)或脏位(dirty bit)以降低额外开销。每页或帧可以有一个修改位,通过硬件与之相关联。每当页内的任何字或字节被写入时,硬件就会设置该页的修改位以表示该页已修改。如果修改位已设置,就可以知道自从磁盘读入后该页已发生了修改。如果该页被选择为替换页,就必须要把该页写到磁盘上去。如果修改位没有设置,则从磁盘读入后该页并没有发生修改。因此,磁盘上页的副本的内容没有必要重写,不需要写回磁盘上。这种技术也适用于只读页。这种页不能被修改。因此,如需要,可以放弃这些页。这种方案可显著地降低用于处理页错误所需要的时间,因为如果页没有修改它,可以降低一半的IO时间。
为实现按需调页,必须解决两个主要问题:必须开发帧分配算法(frame-allocationalgorithm)和页置换算法(page-replacement algorithm)。如果在内存中有多个进程,那么必须决定为每个进程各分配多少帧。而且,当需要页置换时,必须选择要置换的帧。磁盘IO非常费时,即使请求页面调度方面的很小改进也会对系统性能产生显著的改善。
有许多不同的页置换算法。每个操作系统可能都有其自己的置换算法。通常采用最小页错误率的算法。
初始时肯定有页错误,不可能降为零。

页置换算法
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):对有的页置换算法,页错误率可能会随着所分配的帧数的增加而增加,而原期望为进程增加内存会改善其性能。

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

- 事实上,没有置换算法能只用3个帧且少于9个页错误就能处理该引用串。
- 然而,最优置换算法难以实现,因为需要引用串的未来知识。因此,最优算法主要用于比较研究。例如,如果知道一个算法不是最优,但是与最优相比最坏不差于12.3%,平均不差于4.7%,那么也是很有用的。
LRU页置换
- FIFO和OPT 算法的关键区别在于,FIFO算法使用的是页调入内存的时间,OPT算法使用的是页将来使用的时间。如果使用离过去最近作为不远将来的近似,那么可置换最长时间没有使用的页,这种方法称为最近最少使用算法(least-recently-used(LRU) algorithm)。

LRU置换为每个页关联该页上次使用的时间。当必须置换一页时,LRU选择最长时间没有使用的页。这种策略为向后看(而不是向前看)最优页置换算法。(奇怪的是,如果s'表示引用串S的倒转,那么针对S'的OPT算法的页错误率与针对S页错误率是一样的。类似地,针对S'的LRU算法的页错误率与针对S的页错误率是一样的。)
LRU策略经常用做页置换算法,且被认为相当不错。其主要问题是如何实现LRU置换。LRU页置换算法可能需要一定的硬件支持。它的问题是为页帧确定一个排序序列,这个序列按页帧上次使用的时间来定。有两种可行实现:
- 计数器:简单情况是,为每个页表项关联一个使用时间域,并为CPU增加一个逻辑时钟或计数器。对每次内存引用,计数器都会增加。每次内存引用时,时钟寄存器的内容会被复制到相应页所对应页表项的使用时间域内。可得到每页的最近引用时间。置换具有最小时间的页。这种方案需要搜索页表以查找LRU页,且每次内存访问都要写入内存(到页表的使用时间域)。在页表改变时(因CPU调度)也必须保持时间,必须考虑时钟溢出。
- 栈:另一个方法是采用页码栈。每当引用一个页,该页就从栈中删除并放在顶部。栈顶部总是最近使用的页,栈底部总是LRU页。由于必须从栈中部删除项,所以栈可实现为具有头指针和尾指针的双向链表。虽说每个更新有点费时,但是置换不需要搜索;尾指针指向栈底部,就是LRU页。
近似LRU页置换
- 很少有计算机系统能提供足够的硬件来支持LRU页置换。有的系统不提供任何支持,因此必须使用其他置换算法。然而,许多系统都通过引用位方式提供一定的支持。页表内的每项都关联着一个引用位(reference bit)。每当引用一个页时(无论是读或写),相应页表的引用位就被硬件置位。
- 开始,操作系统会将所有引用位都清零。随着用户进程的执行,与引用页相关联的引用位被硬件置位(置为1)。通过检查引用位,能够确定哪些页使用过而哪些页未使用过,但不知道使用顺序。这信息是许多近似LRU页置换算法的基础。
1.附加引用位算法
- 通过在规定时间间隔里记录引用位,可以获得额外顺序信息。可以为位于内存内的表中的每页保留一个8位的字节。在规定时间间隔内,时钟定时器产生中断并将控制转交给操作系统。操作系统把每个页的引用位转移到其8位字节的高位,而将其他位向右移一位,并抛弃最低位。这些8位移位寄存器包含着该页在最近8个时间周期内的使用情况。例如,如果移位寄存器含有00000000,那么该页在8个时间周期内没有使用;如果移位寄存器的值为11111111,那么该页在过去每个周期内都至少使用过一次。具有值为11000100 的移位寄存器的页要比值为01110111的页更为最近使用。如果将这8位字节作为无符号整数,那么具有最小值的页为LRU页,且可以被置换。注意这些数字并不唯一。可以置换所有具有最小值的页,或在这些页之间采用FIFO来选择置换。
- 当然,历史位的数量可以修改,可以选择(依赖于可用硬件)以尽可能快地更新。在极端情况下,数量可降为0,即只有引用位本身。这种算法称为第二次机会页置换算法( second-chance page-replacement algorithm)。
2.二次机会算法
- 二次机会置换的基本算法是 FIFO置换算法。当要选择一个页时,检查其引用位。如果其值为0,那么就直接置换该页。如果引用位为1,那么就给该页第二次机会,并选择下一个FIFO页。当一个页获得第二次机会时,其引用位清零,且其到达时间设为当前时间。因此,获得第二次机会的页在所有其他页置换(或获得第二次机会)之前,是不会被置换的。另外,如果一个页经常使用以致其引用位总是被设置,那么它就不会被置换。
- 一种实现二次机会算法(有时称为时钟算法)的方法是采用循环队列。用一个指针表示下次要置换哪一页。当需要一个帧时,指针向前移动直到找到一个引用位为0的页。在向前移动时,它将清除引用位。一旦找到牺牲页,就置换该页,新页就插入到循环队列的该位置。注意:在最坏情况下,所有位均已设置,指针会遍历整个循环队列,以便给每个页第二次机会。它将清除所有引用位后再选择页来置换。这样,如果所有位均已设置,那么二次机会置换就变成了FIFO置换。
3.增强型二次机会算法
通过将引用位和修改位作为一有序对来考虑,可以改进二次机会算法。采用这两个位,有下面四种可能类型:
- (0,0)最近没有使用且也没有修改—用于置换的最佳页。
- (0,1)最近没有使用但修改过---不是很好,因为在置换之前需要将页写出到磁盘。
- (1,0)最近使用过但没有修改---它有可能很快又要被使用。
- (1,1)最近使用过且修改过---它有可能很快又要被使用,且置换之前需要将页写出到磁盘。
每个页都属于这四种类型之一。当页需要置换时,可使用时钟算法,检查所指页属于哪个类型。置换在最低非空类中所碰到的页。注意在找到要置换页之前,可能要多次搜索整个循环队列。
这种方法与简单时钟算法的主要差别是这里给那些已经修改过的页以更高的级别,从而降低了所需IO的数量。
基于计数的页置换
还有许多其他算法可用于页置换。例如,可以为每个页保留一个用于记录其引用次数的计数器,并可形成如下两个方案。
- 最不经常使用页置换算法(least frequently used (LFU) page-replacement algorithm)要求置换计数最小的页。理由是活动页应该有更大的引用次数。问题:一个页在进程开始时使用很多,但以后就不再使用。由于其使用过很多,所以它有较大次数,所以即使不再使用仍然会在内存中。解决方法之一是定期地将次数寄存器右移一位,以形成指数衰减的平均使用次数。
- 最常使用页置换算法(most frequently used (MFU) page-replacement algorithm)是基于如下理论:具有最小次数的页可能刚刚调进来,且还没有使用。
可以想象,MFU和LFU置换都不常用。这两种算法的实现都很费时,且并不能很好地近似OPT置换算法。
页缓冲算法
- 除了特定页置换算法外,还经常采用其他措施。例如,系统通常保留一个空闲帧缓冲池。当出现页错误时,会像以前一样选择一个牺牲帧。然而,在牺牲帧写出之前,所需要的页就从缓冲池中读到空闲内存。这种方法允许进程尽可能快地重启,而无须等待牺牲帧页的写出。当在牺牲帧以后写出时,它再加入到空闲帧池。
- 另一种修改是保留一个空闲帧池,但要记住哪些页在哪些帧中。由于当帧写到磁盘上时其内容并没有修改,所以在该帧被重用之前如果需要使用原来页,那么原来页可直接从空闲帧池中取出来使用。这时并不需要IO。当一个页错误发生时,先检查所需要页是否在空闲帧池中。如果不在,那么才必须选择一个空闲帧来读入所需页。
应用程序与页置换
- 有的操作系统允许特殊程序将磁盘作为逻辑块数组使用,而不需要通过文件系统的数据结构。这种数组有时称为生磁盘(raw disk),而对数组的IO则称为生IO。生IO可以绕过所有文件系统服务。注意,虽然有些应用程序使用其特有磁盘存储服务更为高效,但是绝大多数程序使用通用文件系统服务会更好。
帧分配
- 下面研究如何分配的问题。如何在各个进程之间分配一定的空闲内存?基本策略是:用户进程会分配到任何空闲帧。
帧的最少数量
- 帧的最少数量是由给定计算机结构定义的,最大数量是由可用物理内存的数量来决定。必须有足够的帧来容纳所有单个指令所引用的页。
分配算法
- 在n个进程之间分配m个帧的最为容易的方法是给每个一个平均值,即m/n帧。例如,如果有93个帧和5个进程,那么每个进程可得到18个帧,剩余3个帧可以放在空闲帧缓存池中。这种方案称为平均分配( equal allocation)。
- 另外一种方法是要认识到各个进程需要不同数量的内存。可使用比例分配(proportional allocation)。根据进程大小,而将可用内存分配给每个进程。

全局分配和局部分配
- 为各个进程分配帧的另一个重要因素是页置换。当有多个进程竞争帧时,可将页置换算法分为两大类:全局置换(global replacement)和局部置换(local replacement)。全局置换允许一个进程从所有帧集合中选择一个置换帧,而不管该帧是否已分配给其他进程,即一个进程可以从另一个进程中拿到帧。局部置换要求每个进程仅从其自己的分配帧中进行选择。
- 例如:允许高优先级进程从低优先级进程中选择帧以便置换。这种方法允许高优先级进程增加其帧分配而以损失低优先级进程为代价。
- 采用局部置换策略,分配给每个进程的帧的数量不变。采用全局置换,一个进程可能从分配给其他进程的帧中选择一个进行置换,因此增加了所分配的帧的数量。
- 全局置换算法的一个问题是进程不能控制其页错误率。一个进程的位于内存的页集合不但取决于进程本身的调页行为,还取决于其他进程的调页行为。局部置换算法就没有这样的问题。在局部置换下,进程内存中的页只受该进程本身的调页行为所影响。但是,因为局部置换不能使用其他进程的不常用的内存,所以会阻碍一个进程。因此,全局置换通常会有更好的系统吞吐量,且更为常用。
系统颠簸
概念
- 研究一下没有“足够”帧的进程。如果进程没有它所需要的活跃使用的帧,那么它会产生页错误。这时,必须置换某个页。然而,所有页都在使用,它置换一个页,但又立刻再次需要这个页。因此,它会一而再地产生页错误。这种频繁的页调度行为称为颠簸(thrashing)。如果一个进程在换页上用的时间要多于执行时间,那么这个进程就在颠簸。
颠簸原因
颠簸将导致严重的性能问题。考虑如下情况,操作系统在监视CPU的使用率。如果CPU使用率太低,那么向系统中引入新进程,以增加多道程序的程度。采用全局置换算法,它会置换页而不管这些页是属于哪个进程的。现在假设一个进程进入一个新执行阶段,需要更多的帧。它开始出现页错误,并从其他进程中拿到帧。然而,这些进程也需要这些页,所以它们也会出现页错误,从而从其他进程中拿到帧。这些页错误进程必须使用调页设备以换进和换出页。随着它们排队等待换页设备,就绪队列会变空,而进程等待调页设备,CPU使用率就会降低。
CPU调度程序发现CPU使用率降低,因此会增加多道程序的程度。新进程试图从其他运行进程中拿到帧,从而引起更多页错误,形成更长的调页设备的队列。因此,CPU使用率进一步降低,CPU调度程序试图再增加多道程序的程度。这样就出现了系统颠簸,系统吞吐量陡降,页错误显著增加。因此,有效内存访问时间增加。最终因为进程主要忙于调页,系统不能完成一件工作。
随着多道程序程度增加,CPU使用率(虽然有点慢)增加,直到达到最大值。如果多道程序的程度还要继续增加,那么系统颠簸就开始了,且CPU使用率急剧下降。这时,为了增加CPU使用率和降低系统颠簸,必须降低多道程序的程度。
局部置换算法(local replacement algorithm)能限制系统颠簸。采用局部置换,如果一个进程开始颠簸,那么它不能从其他进程拿到帧,且不能使后者也颠簸。然而这个问题还没有完全得到解决。如果进程颠簸,那么绝大多数时间内也会排队来等待调页设备。由于调页设备的更长的平均队列,页错误的平均等待时间也会增加。因此,即使对没有颠簸的进程,其有效访问时间也会增加。为了防止颠簸,必须提供进程所需的足够多的帧。.但是如何知道进程“需要”多少帧呢?工作集合策略是研究一个进程实际正在使用多少帧。这种方法定义了进程执行的局部模型(locality model)。
局部模型说明,当进程执行时,它从一个局部移向另一个局部。局部是一个经常使用页的集合。一个程序通常由多个不同局部组成,它们可能重叠。
例如,当一个子程序被调用时,它就定义了一个新局部。在这个局部里,内存引用包括该子程序的指令、其局部变量和全局变量的子集。当该子程序退出时,因为子程序的局部变量和指令现已不再使用,进程离开该局部。也可能在后面再次返回该局部。
因此,可以看到局部是由程序结构和数据结构来定义的。局部模型说明了所有程序都具有这种基本的内存引用结构。局部模型是缓存的基础,如果对任何数据类型的访问是随机的而没有一定的模式,那么缓存就没有用了。
假设为每个进程都分配了可以满足其当前局部的帧。该进程在其局部内会出现页错误,直到所有页均在内存中;接着它不再会出现页错误直到它改变局部为止。如果分配的帧数少于现有局部的大小,那么进程会颠簸,这是因为它不能将所有经常使用的页放在内存中。
工作集合模型
- 前面提到,工作集合模型( working-set model)是基于局部性假设的。该模型使用参数△定义工作集合窗口( working-set window)。其思想是检查最近△个页的引用。这最近△个引用的页集合称为工作集合( working set)。如果一个页正在使用中,那么它就在工作集合内。如果它不再使用,那么它会在其上次引用的△时间单位后从工作集合中删除。因此,工作集合是程序局部的近似。
- 工作集合的精确度与△的选择有关。如果△太小,那么它不能包含整个局部;如果△太大,那么它可能包含多个局部。在最为极端的情况下,如果△为无穷大,那么工作集合为进程执行所接触到的所有页的集合。
- 最为重要的工作集合的属性是其大小。如果经计算而得到系统内每个进程的工作集合为WSSi,那么就得到
- 其中D为总的帧需求量。如果总的需求大于可用帧的数量(D>m),那么有的进程就会得不到足够的帧,从而会出现颠簸。
- 一旦确定了△,那么工作集合模型的使用就较为简单。操作系统跟踪每个进程的工作集合,并为进程分配大于其工作集合的帧数。如果还有空闲帧,那么可启动另一进程。如果所有工作集合之和的增加超过了可用帧的总数,那么操作系统会选择暂停一个进程。这种工作集合策略防止了颠簸,并尽可能地提高了多道程序的程度。因此,它优化了CPU使用率。
- 通过固定定时中断和引用位,能近似模拟工作集合模型。例如,假设△为10 000个引用,且每5000个引用会产生定时中断。当出现定时中断时,先复制再清除所有页的引用位。因此,当出现页错误时,可以检查当前引用位和位于内存内的两个位,从而确定在过去的10 000到15 000个引用之间该页是否被引用过。如果使用过,至少有一个位会为1。如果没有使用过,那么所有这3个位均为0。只要有1个位为1,那么就可认为处于工作集合中。注意,这种安排并不完全准确,这是因为并不知道在5 000个引用的什么位置出现了引用。通过增加历史位的位数和中断频率(例如,10位和每 1000个引用就产生中断),可以降低这一不确定性。然而,处理这些更为经常的中断的时间也会增加。
页错误频率
- 工作集合模型是成功的,工作集合知识控制颠簸有点不太灵活。一种更为直接的方法是采用页错误频率( page-fault frequency,PFF)策略。
- 可以为页错误率设置上限和下限。如果实际页错误率超过上限,那么为进程分配更多的帧;如果实际页错误率低于下限,可从该进程中移走帧。可以直接测量和控制页错误率以防止颠簸。

内存映射文件
- 考虑一下采用标准系统调用open()、read()和 write(),并在磁盘上对文件进行一系列的读操作。文件每次访问时都需要一个系统调用和磁盘访问。另外一种方法是,可使用所讨论的虚拟内存技术来将文件IO作为普通内存访问。这种方法称为文件的内存映射( memorymapping),它允许一部分虚拟内存与文件逻辑相关联。
- 文件的内存映射可将一磁盘块映射成内存的一页(或多页)。开始的文件访问按普通请求页面调度来进行,会产生页错误。这样,一页大小的部分文件从文件系统读入物理页(有的系统会一次读入多个一页大小的内容)。以后文件的读写就按通常的内存访问来处理,由于是通过内存操作文件而不是使用系统调用read()和write(),从而简化了文件访问和使用。
- 多个进程可以允许将同一文件映射到各自的虚拟内存中,以允许数据共享。其中任一进程修改虚拟内存中的数据,都会为其他映射相同文件部分的进程所见。
其他考虑
预调页
- 纯按需调页系统的一个显著特性是当一个进程开始时会出现大量页错误。这种情况是由于试图将最初局部调入到内存的结果。预调页( prepaging)试图阻止这种大量的初始调页。这种策略就是同时将所需要的所有页一起调入到内存中。
- 预调页有时可能比较好。关键问题是采用预调页的成本是否小于处理相应页错误的成本。
页面大小
并不存在单一的最佳页大小,有许多因素影响。页面大小总为2的幂,通常从4096(2^12)~4 194 304(2^22)B。
如何选择页大小呢?
- 一方面要考虑页表大小。对于给定虚拟内存空间,降低页大小增加了页的数量,也增加了页表大小。每个活动进程有其自己的页表,所以较大的页是比较理想的。
- 另一方面,较小的页可更好地利用内存。为了减少碎片,需要小的页。
- 另一个问题是页读写所需要的时间。为了最小化IO时间,需要较大的页。
- 然而,因为局部性得以改善,采用较小的页,总的IO就会降低。较小的页允许每个页更精确地匹配程序局部。更小页应用导致更少的IO和更少的总的分配内存。
TLB范围
- TLB命中率是指通过TLB而不是页表进行的虚拟地址转换的百分比。命中率与TLB 的条数有关,增加TLB的条数可增加命中率。代价并不小,因为用于构造TLB的相关内存既昂贵又费电。
- 与命中率相关的类似测量尺度是TLB范围(TLB reach)。TLB范围指通过TLB可访问的内存量,等于TLB条数与页大小之积。增加TLB范围的方法是增加页的大小或提供多种页大小。对于不需要这样页大小的应用程序,会产生碎片。另一种选择是,操作系统可使用不同大小的页。
倒置反向页表
- 因为它们保留了有关每个物理帧保存哪个虚拟内存页面的信息,反向页表降低了保存这种信息所需的物理内存。然而,反向页表不再包括进程逻辑地址空间的完整信息,为了提供这种信息,每个进程必须保留一个外部页表。外部表中包括每个虚拟页所在的位置。