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