我们现在在PC上,可以开任意个进程,很大程度得益于虚拟内存机制的实现。当然也得益于现在的大内存。它们两个就像是战术与力量的关系。“穷则战术开花,富则给老子炸”。
虚拟内存技术允许执行进程不必完全在内存中。这种方案的一个显著优点是程序可以比物理内存大。而且,虚拟内存将内存抽象成一个巨大的、统一的存储数组,进而将用户看到的逻辑内存与物理内存分开。这种技术允许程序员不受内存存储的限制,只需要关注所要解决的问题。
我们第八章所介绍的内存管理算法都是基于一个基本要求:执行指令必须在物理内存中。实际上,在许多情况下并不需要将整个程序放到内存中。比如:
只有部分程序在内存中带来的优势:
时间局部性: 若程序的某条指令被执行,则很可能不久后会再次被执行。
空间局部性: 程序访问了某个储存单元,那么它附近的存储单元就有不久被访问的倾向。
操作系统看到这里,我认为有一个点需要特别强调一下:就是一个知识点在思考与叙述时,一定要知道它们是基于什么的,最好的情况时能和前面的知识连起来。比如最开始的进程管理,有些章节基于进程,有些基于线程;有些对应单CPU,有些对应多CPU;还有第八章的内存管理,交换是基于整个进程,虚拟内存马上要学到的按需调页是基于进程的页,也正是我们说的进程的一部分。操作系统难在专有名词,不仅要理解,还要构建整个操作系统结构。希望有时间可以画一份思维导图。
按需调页: 常为虚拟内存系统所采用。对于按需调页虚拟内存,只有程序执行需要时才载入页(这种行为叫做懒惰交换(lazy swapper)),哪些从未访问过的页不会调入物理内存。
按需调页与交换: 交换对整个进程进行操作,而调页程序只是对进程的单个页进行操作。
下面讨论按需调页具体如何实现
首先,我们需要硬件来帮我们支持哪些页在内存里,哪些页在磁盘上。
在这里,我们利用以前提到过的有效-无效位来实现。
设 p p p为页错误概率,内存访问时间为 m a ma ma,那么, 有 效 访 问 时 间 = ( 1 − p ) ∗ m a + p ∗ 页 错 误 时 间 有效访问时间 = (1-p)*ma + p*页错误时间 有效访问时间=(1−p)∗ma+p∗页错误时间。
写时复制以fork()和exec()为例,助于理解。
在写时复制之前,fork()为子进程创建一个父进程地址空间的副本,复制属于父进程的页。然而,由于许多子进程在创建之前通常会马上执行系统调用exec(),所以父进程地址空间的复制可能没有必要。
写时复制允许父进程与子进程开始共享同一界面。这些页面标记为写时复制页,如果任何一个进程需要对页进行写操作,那么就创建一个共享页的副本。
修改页C之前:
修改页C之后:
当一个页要使用写时复制时,从哪里分配空闲页时很重要的。许多操作系统为这类请求提供了空闲缓冲池。这些空闲页在进程栈或堆必须扩展时可用于分配。
在以上的有关页错误的讨论中,我们假定每页最多只会出现一次错误。这种描述既不严格也不准确。在多道程序环境下,还有可能会过度分配内存。即当一个用户执行进程时,发生页错误,操作系统处理完成页错误中断后,却发现没有空闲帧可用。
如何处理没有空闲帧可用的情况:
页置换的流程:
修改位或脏位(dirty bit): 每页或每帧可以设置一个修改位,当选择牺牲帧的时候,如果某页被修改过,就说明和磁盘中存放的页数据不同,需要写回磁盘。但如果牺牲帧没被修改过,就不需要写回磁盘,因为数据一样。所以最好选择未修改过的帧做牺牲帧,大大减少处理页错误所需要的时间。
下面就是本章的两个主要的问题,如果内存中有多个进程,那么如何给每个进程分配帧? 以及当选择牺牲页的时候,应当遵循什么算法?
帧分配算法和页置换算法
引用串: 引用串可以理解为地址的简化,引用串包含多个引用,对于每个引用,进程可能引用多次。
最小错误率: 对于同一个引用串,我们尽量设计算法使发生页错误的次数最少,以提高性能。
随着帧数量的增加,页错误数量会降低至最小值。
备注:以下所有实例均使用引用串(7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1)
当系统没有足够的硬件来支持LRU页置换算法时,可以利用近似LRU置换。
算法思想: 给页表的页表项增加引用位,每次引用该页时引用位被置树。近似算法有多种实现形式。
算法思想: 为位于内存以内的每一个表中的页保留一个8位的字节。在规定的时间间隔内,时钟定时器产生中断并将控制权交给操作系统。操作系统把每个页的引用位传递到其8位字节的高位,而将其他3位向右移一位,并抛弃最低位。这些8位移位寄存器包含着该页在8个时间周期内的使用情况。
举例:
也是利用引用位,不过引用位只有1bit。
算法思想: 二次机会算法的基本算法是FIFO置换算法。当要选择一个页时,检查其引用位。如果其值为0,那么就直接置换该页。如果引用位为1,那么就给该页二次机会,并选择下一个FIFO页,将引用位置0。
实现: 可以利用循环队列实现。
将引用位和修改位综合来考虑,需要2bit的附加位。
2bit的附加位一共有四种情况:
使用: 为上面的四种情况分级,置换级别最低的页。
算法思想: 给每个页保留一个用于记录其引用次数的计数器。
算法思想: 计数器计数最小的是访问次数最少的,应该作为牺牲页。
缺点: 有些页一开始使用很多,之后便不再使用。有些页是刚装入内存的,以后会用到但是计数小。
算法思想: 和上面的那个相反,认为计数器计数最小的是访问次数最大的,应该作为牺牲页。
缺点: 有些页一开始使用很多,之后便不再使用。有些页是刚装入内存的,以后会用到但是计数小。
算法思想: 系统通常保留一个空闲帧缓冲池。当出现页错误时,会像以前一样选择一个牺牲帧。然而,在牺牲帧写出之前,所需要的页就从缓冲池中读到空闲内存。这种方法允许进程尽可能快地重启,而无须等待牺牲帧页的写出。当在牺牲帧以后写出时,它再加入到空闲帧池。
扩展1: 当调页设备空闲时选择一个被改写过的页写回到磁盘上,并更新其修改位。
扩展2: 保留一个空闲帧池,但是这些帧所对应的页号要记住,由于这些帧的内容未被修改,所以当再次需要这些页时可以直接从空闲帧池中取出包含所需页的帧。
算法思想: 有的操作系统允许特殊程序将磁盘作为逻辑块数组使用,而不需要通过文件系统的数据结构。这种数组有时称为生磁盘(raw disk)。对于某些应用程序,这样更为高效,但是绝大多数程序使用通用文件系统服务会更好。
就是对于每个进程,操作系统应该分配给多少帧。
帧的数量: 分配给进程的帧的最少数量是由体系结构决定的。最大数量是由物理内存的数量决定的。
全局分配: 允许一个进程从所有帧的集合中选择一个置换帧,而不管该帧是否已分配给其他进程,即一个进程可以从另一个进程中拿到帧。
实例: 给进程分级,高优先级的进程可以从低优先级进程中选择帧以便置换。一个进程可以从自己的帧中或任何低优先级进程中选择置换帧。这种方法允许高优先级进程增加其帧分配而以损失低优先级进程为代价。
缺点: 进程无法控制其页错误率。
优点: 进程可以利用其它进程不常用的内存,所以系统吞吐量更大,更为常用。
局部置换: 要求每个进程仅从其自己的分配帧中进行选择。
颠簸: 频繁的页调度行为称为颠簸。如果一个进程在换页上用的时间要多于执行时间,那么这个进程就在颠簸。
对于全局置换算法: 随着不断向内存中添加新进程,将出现页错误,并从其他进程中拿到帧。然而,被拿走帧的进程也会需要帧,它们也会出现页错误,从而继续从其他进程中拿走帧。并且,这些页错误进程必须使用调页设备以换进和换出页。随着它们排队等待换页设备,就绪队列就会变空,而进程等待调页设备,会使CPU使用率变低。之后,CPU调度程序发现CPU使用率降低,因此就会增加多道程序的程度。不断循环,导致CPU使用率急剧下降。
对于局部置换算法: 可以限制系统颠簸。采用局部置换,如果一个进程开始颠簸,那么它不能从其他进程拿到帧,且不能使后者也颠簸。但这个问题并没有得到完全解决,因为颠簸的进程大多数时间会排队来等待调页设备,调页设备更长的平均队列将延长所有进程的页错误的平均等待时间。因此,对于没有颠簸的进程,其有效访问时间也会增加。
局部: 一个经常使用的页的集合,一个进程由多个不同的局部构成,局部可能交叠。
局部模型: 进程由多个局部构成,进程执行时从一个局部移到另一个局部。
防止颠簸的原理: 为了防止颠簸,需要提供给进程所需的足够多的帧。如果分配的帧数少于现有局部的大小,那么进程会颠簸,这是因为它不能将所有经常使用的页放在内存中。
工作集合模型原理: 工作集合模型基于局部性假设。即基于局部性原理,每个进程近期使用的帧数作为将要使用的帧数的近似值。
工作集合窗口(working-set window): 使用参数 Δ Δ Δ定义,是固定数目的页引用集合。
工作集合(working set, WS): 最近 Δ Δ Δ个引用的页集合。若一个页正在使用中,那么它就在工作集合内。工作集合是程序局部的近似。工作集合的精确度与 Δ Δ Δ的选择有关。如果 Δ Δ Δ太小,那么它不能包含整个局部;如果 Δ Δ Δ太大,那么它可能包含多个局部。
工作集合数目(WSS): 表示工作集所包含的页的数目。
通过固定定时中断和引用位,能近似模拟工作集合模型。
工作集合模型是成功的,工作集合知识能用于预先调页,但是用于控制颠簸有点不太灵活。一种更为直接的方法是采用页错误频率(page-fault frequency,PFF)。
具体过程:
原理: 利用我们的虚拟内存技术将文件I/O作为普通内存访问。这种方法称为文件的内存映射(memory mapping),它允许一部分虚拟内存与文件逻辑相关联。
每个I/O控制器包括存放命令及传送数据的寄存器,通常,专用I/O指令允许寄存器和系统内存之间进行数据传递。
内存映射I/O: 一组内存地址专门映射到设备寄存器,对这些内存地址的读写如同对设备寄存器的读写。
内核内存分配的特点:
如上图所示,如果内核需要30kb,则分配 C L C_L CL。如果需要33kb,则分配 B L B_L BL,依次类推。
优点: 可以通过合并而快速形成更大的段。
缺点: 容易产生内部碎片。
**优点: ** 没有因碎片而引起的内存浪费。内存请求可以快速满足。
纯粹按需调页系统的显著特性是当进程开始时会出现大量页错误,预调页目的在于阻止这种大量的初始调页,采用同时将所需的所有页一起调入内存。类似于指令预取,在进程引用前将需要的所有或部分页准备好。
页大小的确定需要考虑以下四点:
TLB范围: TLB范围是指通过TLB可以访问的内存量,等于TLB的条目与页大小的乘积。
增加TLB范围(提高命中率)的办法:
在使用请求页面调度时,允许有些页在内存中被锁住。
锁住即该页不能被页面置换给置换到磁盘上。
考虑这个问题: 一个进程发出I/O请求,并加入到I/O设备的等待队列上,而同时CPU被交给了其它进程。这些进程引起页错误,采用全局置换算法,其中之一置换了等待进程用于I/O的缓存页,这些页被换出。之后,当I/O请求移到设备队列的头部时,就针对指定地址进行I/O。然而,这是该帧已被属于另一个进程的不同页所使用。
解决方法: