分页系统中的设计问题


我们将讨论为了使分页系统达到较好的性能,操作系统设计者必须仔细考虑的一些其他问题

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

怎样在相互竞争的可运行进程之间分配内存

在这里插入图片描述
如图a所示,三个进程A、B、C构成了可运行进程的集合。假如A发生了缺页中断,页面置换算法在寻找最近最少使用的页面时是只考虑分配给A的6个页面呢?还是考虑所有在内存中的页面?如果只考虑分配给A的页面,生存时间值最小的页面是A5,于是将得到图b所示的状态

另一方面,如果淘汰内存中生存时间值最小的页面,而不管它属于哪个进程,则将选中页面B3,于是将得到图c所示的情况。图b的算法被称为局部(local)页面置换算法,而图c被称为全局 (global)页面置换算法。局部算法可以有效地为每个进程分配固定的内存片段。全局算法在可运行进程之间动态地分配页框,因此分配给各个进程的页框数是随时间变化的

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

另一种途径是使用一个为进程分配页框的算法。其中一种方法是定期确定进程运行的数目并为它们分配相等的份额

这个算法看起来好像很公平,但是给一个10KB的进程和一个300KB的进程分配同样大小的内存块是很不合理的。可以采用按照进程大小的比例来为它们分配相应数目的页面的方法来取代上一种方法,这样300KB的进程将得到10KB进程30倍的份额。比较明智的一个可行的做法是对每个进程都规定一个最小的页框数,这样不论多么小的进程都可以运行

如果使用全局算法,根据进程的大小按比例为其分配页面也是可能的,但是该分配必须在程序运行时动态更新。管理内存动态分配的一种方法是使用PFF(Page Fault Frequency,缺页中断率)算法。它指出了何时增加或减少分配给一个进程的页面,但却完全没有说明在发生缺页中断时应该替换掉哪一个页面,它仅仅控制分配集的大小

正如我们上面讨论过的,有一大类页面置换算法(包括LRU在内),缺页中断率都会随着分配的页面的增加而降低,这是PFF背后的假定

在这里插入图片描述
测量缺页中断率的方法是直截了当的:计算每秒的缺页中断数,可能也会将过去数秒的情况做连续平均。一个简单的方法是将当前这一秒的值加到当前的连续平均值上然后除以2。虚线A对应于一个高得不可接受的缺页中断率,虚线B则对应于一个低得可以假设进程拥有过多内存的缺页中断率。在这种情况下,可能会从该进程的资源中剥夺部分页框。这样,PFF尽力让每个进程的缺页中断率控制在可接受的范围内

负载控制

即使是使用最优页面置换算法并对进程采用理想的全局页框分配,系统也可能会发生颠簸。事实上,一旦所有进程的组合工作集超出了内存容量,就可能发生颠簸。该现象的症状之一就是如PFF算法所指出的, 一些进程需要更多的内存,但是没有进程需要更少的内存。在这种情况下,没有方法能够在不影响其他进程的情况下满足那些需要更多内存的进程的需要。惟一现实的解决方案就是暂时从内存中去掉一些进程

减少竞争内存的进程数的一个好方法是将一部分进程交换到磁盘,并释放他们所占有的所有页面。例如,一个进程可以被交换到磁盘,而它的页框可以被其他处于颠簸状态的进程分享。如果颠簸停止,系统就能够这样运行一段时间。如果颠簸没有结束,需要继续将其他进程交换出去,直到颠簸结束。因此,即使是使用分页,交换也是需要的,只是现在交换是用来减少对内存潜在的需求,而不是收回它的页面

将进程交换出去以减轻内存需求的压力是借用了两级调度的思想,在此过程中一些进程被放到磁盘,此时用一个短期的调度程序来调度剩余的进程。很明显,这两种思路可以被组合起来,将恰好足够的进程交换出去以获取可接受的缺页中断率。一些进程被周期性地从磁盘调入,而其他一些则被周期性地交换到磁盘

另一个需要考虑的因素是多道程序设计的道数。当内存中的进程数过低的时候,CPU可能在很长的时间内处于空闲状态。考虑到该因素,在决定交换出哪个进程时不光要考虑进程大小和分页率,还要考虑它的特性(如它究竟是CPU密集型还是I/O密集型)以及其他进程的特性

页面大小

页面大小是操作系统可以选择的一个参数。例如,即使硬件设计只支持512字节的页面,操作系统也可以很容易通过总是为页面对0和1、2和3、4和5等分配两个连续的512字节的页框,而将其作为1KB的页面

要确定最佳的页面大小需要在几个互相矛盾的因素之间进行权衡。从结果看,不存在全局最优。首先, 有两个因素可以作为选择小页面的理由。随便选择一个正文段、数据段或堆栈段很可能不会恰好装满整数个页面,平均的情况下,最后一个页面中有一半是空的。多余的空间就被浪费掉了,这种浪费称为内部碎片 (internal fragmentation)。在内存中有n个段、页面大小为p字节时,会有np/2字节被内部碎片浪费。从这方面考虑,使用小页面更好

选择小页面还有一个明显的好处,如果考虑一个程序,它分成8个阶段顺序执行,每阶段需要4KB内存。如果页面大小是32KB,那就必须始终给该进程分配32KB内存。如果页面大小是16KB,它就只需要16KB。如果页面大小是4KB或更小,在任何时刻它只需要4KB内存。总的来说,与小页面相比,大页面使更多没有用的程序保留在内存中

在另一方面,页面小意味着程序需要更多的页面,这又意味着需要更大的页表

在某些机器上,每次CPU从一个进程切换到另一个进程时都必须把新进程的页表装入硬件寄存器中。这样,页面越小意味着装入页面寄存器花费的时间就会越长,而且页表占用的空间也会随着页面的减小而增大

分离的指令空间和数据空间

大多数计算机只有一个地址空间,既存放程序也存放数据,如图a所示。如果地址空间足够大,那 么一切都好。然而,地址空间通常太小了,这就使得程序员对地址空间的使用出现困难

在这里插入图片描述
首先在PDP-11(16位)上实现的一种解决方案是,为指令(程序正文)和数据设置分离的地址空间, 分别称为I空间和D空间,如图b所示。每个地址空间都从0开始到某个最大值,比较有代表性的是216 -1 或者232 -1。链接器必须知道何时使用分离的I空间和D空间,因为当使用它们时,数据被重定位到虚拟地址0,而不是在程序之后开始

在使用这种设计的计算机中,两种地址空间都可以进行分页,而且互相独立。它们分别有自己的页表, 分别完成虚拟页面到物理页框的映射。当硬件进行取指令操作时,它知道要使用I空间和I空间页表。类似地,对数据的访问必须通过D空间页表。除了这一区别,拥有分离的I空间和D空间不会引入任何复杂的设计,而且它还能使可用的地址空间加倍

共享页面

另一个设计问题是共享。在大型多道程序设计系统中,几个不同的用户同时运行同一个程序是很常见的。显然,由于避免了在内存中有一个页面的两份副本,共享页面效率更高。这里存在一个问题,即并不是所有的页面都适合共享。特别地,那些只读的页面(诸如程序文本)可以共享,但是数据页面则不能共享

如果系统支持分离的I空间和D空间,那么通过让两个或者多个进程来共享程序就变得非常简单了,这些进程使用相同的I空间页表和不同的D空间页表

每个进程在它的进程表中都有两个指针:一个指向I空间页表,一个指向D空 间页表,如图所示。当调度程序选择一个进程运行时,它使用这些指针来定位合适的页表,并使用它们来设立MMU。即使没有分离的I空间和D空间,进程也可以共享程序(或者有时为库),但要使用更为复杂的机制
在这里插入图片描述
在两个或更多进程共享某些代码时,在共享页面上存在一个问题。假设进程A和进程B同时运行一个编辑器并共享页面。如果调度程序决定从内存中移走A,撤销其所有的页面并用一个其他程序来填充这些空的页框,则会引起B产生大量的缺页中断,才能把这些页面重新调入

类似地,当进程A结束时,能够发现这些页面仍然在被使用是非常必要的,这样,这些页面的磁盘空间才不会被随意释放。查找所有的页表,考察一个页面是否共享,其代价通常比较大,所以需要专门的数据结构记录共享页面

共享数据要比共享代码麻烦,但也不是不可能。特别是在UNIX中,在进行fork系统调用后,父进程和子进程要共享程序文本和数据。在分页系统中,通常是让这些进程分别拥有它们自己的页表,但都指向同一 个页面集合。这样在执行fork调用时就不需要进行页面复制。然而,所有映射到两个进程的数据页面都是只读的

只要这两个进程都仅仅是读数据,而不做更改,这种情况就可以保持下去。但只要有一个进程更新了一点数据,就会触发只读保护,并引发操作系统陷阱。然后会生成一个该页的副本,这样每个进程都有自己的专用副本。两个复制都是可以读写的,随后对任何一个副本的写操作都不会再引发陷阱。这种策略意味着那些从来不会执行写操作的页面(包括所有程序页面)是不需要复制的,只有实际修改的数据页面需要复制。 这种方法称为写时复制,它通过减少复制而提高了性能

共享库

一个更加通用的技术是使用共享库(在Windows中称作DLL或动态链接库)。为了清楚地表达共享库的思想,首先考虑一下传统的链接。当链接一个程序时,要在链接器的命令中指定一个或多个目标文件,可能还包括一些库文件。以下面的UNIX命令为例:

ld*.o-lc-lm

这个命令会链接当前目录下的所有的.o(目标)文件,并扫描两个库:/usr/lib/libc.a和/usr/lib/libm.a。任何在目标文件中被调用了但是没有被定义的函数(比如,printf),都被称作未定义外部函数(undefined externals)。链接器会在库中寻找这些未定义外部函数。如果找到了,则将它们加载到可执行二进制文件中。任何被这些未定义外部函数调用了但是不存在的函数也会成为未定义外部函数。例如,printf需要 write,如果write还没有被加载进来,链接器就会查找write并在找到后把它加载进来。当链接器完成任务 后,一个可执行二进制文件被写到磁盘,其中包括了所需的全部函数。在库中定义但是没有被调用的函数则不会被加载进去。当程序被装入内存执行时,它需要的所有函数都已经准备就绪了

假设普通程序需要消耗20~50MB用于图形和用户界面函数。静态链接上百个包括这些库的程序会浪费大量的磁盘空间,在装载这些程序时也会浪费大量的内存空间,因为系统不知道它可以共享这些库。这就是引入共享库的原因。当一个程序和共享库(与静态库有些许区别)链接时,链接器没有加载被调用的函数, 而是加载了一小段能够在运行时绑定被调用函数的存根例程(stub routine)。依赖于系统和配置信息,共享库或者和程序一起被装载,或者在其所包含函数第一次被调用时被装载。当然,如果其他程序已经装载了某个共享库,就没有必要再次装载它了——这正是关键所在。值得注意的是,当一个共享库被装载和使用时, 整个库并不是被一次性地读入内存。而是根据需要,以页面为单位装载的,因此没有被调用到的函数是不会被装载到内存中的

共享库还有一个优点:如果共享库中的一个函数因为修正一个bug被更新了,那么并不需要重新编译调用了这个函数的程序。旧的二进制文件依然可以正常工 作。这个特性对于商业软件来说尤为重要,因为商业软件的源码不会分发给客户,例如,如果微软发现并修复了某个标准DLL中的安全错误,Windows更新会下载新的DLL来替换原有文件,所有使用这个DLL的程序 在下次启动时会自动使用这个新版本的DLL

共享库带来了一个必须解决的小问题,如图所示。我们看到有两个进程共享一个20KB大小的库(假设每一方框为4KB)。但是,这个库被不同的进程定位在不同的地址上,大概是因为程序本身的大小不相同。在进程1中,库从地址36K开始;在进程2中则从地址12K开始。假设库中第一个函数要做的第一 件事就是跳转到库的地址16。如果这个库没有被共享,它可以在装载的过程中重定位,就会跳转(在进程1 中)到虚拟地址的36K+16。注意,库被装载到的物理地址与这个库是否为共享库是没有任何关系的,因为所有的页面都被MMU硬件从虚拟地址映射到了物理地址

在这里插入图片描述
但是,由于库是共享的,因此在装载时再进行重定位就行不通了。毕竟,当进程2调用第一个函数时 (在地址12K),跳转指令需要跳转到地址12K+16,而不是地址36K+16。这就是那个必须解决的小问题。 解决它的一个办法是写时复制,并为每一个共享这个库的进程创建新页面,在创建新页面的过程中进行重定位。当然,这样做和使用共享库的目的相悖

一个更好的解决方法是:在编译共享库时,用一个特殊的编译选项告知编译器,不要产生使用绝对地址的指令。相反,只能产生使用相对地址的指令。例如,几乎总是使用向前(或向后)跳转n个字节(与给出具体跳转地址的指令不同)的指令。不论共享库被放置在虚拟地址空间的什么位置,这种指令都可以正确工作。通过避免使用绝对地址,这个问题就可以被解决。只使用相对偏移量的代码被称作位置无关代码

内存映射文件

共享库实际上是一种更为通用的机制——内存映射文件(memory-mapped file)的一个特例。这种机制的思想是:进程可以通过发起一个系统调用,将一个文件映射到其虚拟地址空间的一部分。在多数实现中, 在映射共享的页面时不会实际读入页面的内容,而是在访问页面时才会被每次一页地读入,磁盘文件则被当作后备存储。当进程退出或显式地解除文件映射时,所有被改动的页面会被写回到文件中

内存映射文件提供了一种I/O的可选模型。可以把一个文件当作一个内存中的大字符数组来访问,而不用通过读写操作来访问这个文件

如果两个或两个以上的进程同时映射了同一个文件,它们就可以通过共享内存来通信。一个进程在共享内存上完成了写操作,此刻当另一个进程在映射到这个文件的虚拟地址空间上执行读操作时,它就可以立刻看到上一个进程写操作的结果。因此,这个机制提供了一个进程之间的高带宽通道,而且这种应用很普遍 (甚至扩展到用来映射无名的临时文件)。很显然,如果内存映射文件可用,共享库就可以使用这个机制

清除策略

如果发生缺页中断时系统中有大量的空闲页框,此时分页系统工作在最佳状态。如果每个页框都被占用,而且被修改过的话,再换入一个新页面时,旧页面应首先被写回磁盘。为保证有足够的空闲页框,很多分页系统有一个称为分页守护进程(paging daemon)的后台进程,它在大多数时候睡眠,但定期被唤醒以检查内存的状态。如果空闲页框过少,分页守护进程通过预定的页面置换算法选择页面换出内存。如果这些 页面装入内存后被修改过,则将它们写回磁盘

在任何情况下,页面中原先的内容都被记录下来。当需要使用一个已被淘汰的页面时,如果该页框还没有被覆盖,将其从空闲页框缓冲池中移出即可恢复该页面。保存一定数目的页框供给比使用所有内存并在需要时搜索一个页框有更好的性能。分页守护进程至少保证了所有的空闲页框是“干净”的,所以空闲页框在被分配时不必再急着写回磁盘

一种实现清除策略的方法就是使用一个双指针时钟。前指针由分页守护进程控制。当它指向一个脏页面时,就把该页面写回磁盘,前指针向前移动。当它指向一个干净页面时,仅仅指针向前移动。后指针用于页面置换,就像在标准时钟算法中一样。现在,由于分页守护进程的工作,后指针命中干净页面的概率会增加

虚拟内存接口

到现在为止,所有的讨论都假定虚拟内存对进程和程序员来说是透明的,也就是说,它们都可以在一台只有较少物理内存的计算机上看到很大的虚拟地址空间。对于不少系统而言这样做是对的,但对于一些高级系统而言,程序员可以对内存映射进行控制,并可以通过非常规的方法来增强程序的行为

允许程序员对内存映射进行控制的一个原因就是为了允许两个或者多个进程共享同一部分内存。如果程序员可以对内存区域进行命名,那么就有可能实现共享内存。通过让一个进程把一片内存区域的名称通知另一个进程,而使得第二个进程可以把这片区域映射到它的虚拟地址空间中去。通过两个进程(或者更多)共享同一部分页面,高带宽的共享就成为可能——一个进程往共享内存中写内容而另一个从中读出内容

页面共享也可以用来实现高性能的消息传递系统。一般地,传递消息的时候,数据被从一个地址空间复制到另一个地址空间,开销很大。如果进程可以控制它们的页面映射,就可以这样来发送一条消息:发送进程清除那些包含消息的页面的映射,而接收进程把它们映射进来。这里只需要复制页面的名字,而不需要复制所有数据

另外一种高级存储管理技术是分布式共享内存,该方法允许网络上的多个进程共享一个页面集合,这些页面可能(而不是必要的)作为单个的线性共享地址空间。当一个进程访问当前还没有映射进来的页面时,就会产生缺页中断。在内核空间或者用户空间中的缺页中断处理程序就会对拥有该页面的机器进行定位,并向它发送一条消息,请求它清除该页面的映射,并通过网络发送出来。当页面到达时,就把它映射进来,并重新开始运行引起缺页中断的指令

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值