操作系统概念 第九章 虚拟内存管理

第九章 虚拟内存管理

9.1 背景

在许多情况下并不需要将整个程序置于内存中,即使需要整个程序的情况下,也可能并不同时需要整个程序。分段能够执行只有部分处于内存的程序,可以带来许多好处:

  • 程序不再受物理内存的可用量所限制。用户可以为一个巨大的虚拟地址空间(virtual-address space)编写程序,从而简化了编程任务。
  • 由于每个用户程序可占用较少的物理内存,因此可以同时运行更多的程序,进而增加CPU利用率和吞吐量,但没有增加响应时间或周转时间。
  • 由于加载或交换每个用户程序到内存所需的I/O会更少,用户程序会运行得更快。

虚拟内存(virtual memory)将用户逻辑内存与物理内存分开,在现有物理内存有限的情况下,为程序员提供了巨大的虚拟内存。进程的虚拟地址空间(virtual address space)就是进程如何在内存中存放的*逻辑(或虚拟)*视图。除了将逻辑内存与物理内存分开外,虚拟内存允许文件和内存通过共享页而为多个进程所共享。

9.2 请求调页

假设程序开始时带有一组用户可选的选项,加载整个程序会导致所有选项的执行代码都加载到内存中,而不管这些选项是否最终使用。另一种策略时,仅在需要时才加载页面,这种技术被称为请求调页(demand paging)。当程序许哟啊执行时,它被交换到内存中,但不是整个进程交换到内存中,而是采用惰性交换器(lazy swapper)。惰性交换器除非需要某个页面,否则从不将它交换到内存中。在请求调页的上下文中,使用术语“交换器”在技术上是不正确的,交换器操纵整个进程,而雕爷程序(pager)只涉及进程的页面,因此在涉及请求调页时,我们使用“调页程序”,而不是交换器。

9.2.1 基本概念

调页程序只将要使用的页调入内存,这需要一定形式的硬件支持,以区分内存的页面和磁盘的页面,8.5.3所述的有效-无效位方案可用于这一目的。当该位被设置成“有效”时,相关联的页面是合法的,并且在内存中,“无效”时,页面无效(即不在进程的逻辑地址空间中),或有效但只在磁盘上。对于已调如内存的页面,它的页表条目照常设置;对于不在内存的页面,它的页表条目可简单标记为无效,或者包含磁盘上的页面地址。

当进程执行和访问那些内存驻留(memory resident)的页面时,执行会正常进行;但是当访问那些未调入内存的页面就会产生缺页错误(page fault)。分页硬件在通过页表转换地址时会注意到无效位被设置,从而陷入操作系统,处理这种缺页错误的程序很简单:

  • 1.检查这个进程的内部表(通常与PCB(Process Control Block,进程控制块)一起保存),以确定该引用是有效的还是无效的内存访问。
  • 2.如果引用无效,那么终止进程;如果引用有效但是尚未调入页面,那么现在就应调入。
  • 3.找到一个空闲帧(例如从空闲帧链表上得到一个)。
  • 4.调度一个磁盘操作,将所需页面读到刚分配的帧。
  • 5.读取完成时,修改进程的内部表和页表,以指示该页现在处于内存中。
  • 6.重启被陷阱中断的指令。该进程现在能访问所需的页面就好像它总是在内存中。

纯请求调页(pure demand paging):只有在需要时才将页面调入内存。从最开始,该进程没有一个页面位于内存中,发生缺页错误时才调入相应页面进内存。

请求调页的关键要求是在缺页错误后重新启动任何指令的能力。只要保存了被中断的进程状态(寄存器、条件码、指令计数器),就能在完全相同的位置和状态下,重新启动进程。当一条指令可以修改多个不同位置时,就会出现重要困难。如果任何一块(源或目的)跨越页边界,那么执行了部分移动时可能会出现缺页错误。此外如果源块和目的块有重叠,源块可能已被修改。对第一种情况,可以事先访问两块的两端,如果出现缺页错误,那么在这一步(在任何内容被修改之前)就会出现。对于第二种情况,使用临时寄存器来保存覆盖位置的值,如果有缺页错误,在陷阱发生之前将所有旧值写回内存中。该动作将内存恢复到指令启动之前的状态,这样就可以重复该指令。

9.2.2 请求调页的性能

请求调页可以显著影响计算机系统的性能。只要没有出现缺页错误,有效访问时间就等于内存访问时间。出现缺页错误时,处理时间有三个主要组成部分:

  • 处理缺页错误中断
  • 读入页面
  • 重新启动进程

有效访问时间与缺页错误率(page-fault rate)成正比。为了因缺页错误而产生的性能降低可以接受,需要使缺页错误率极低。

9.3 写时复制

考虑系统调用 fork() 为子进程创建一个父进程地址空间的副本,复制属于父进程的页面。然而考虑到许多子进程在创建之后立即调用系统调用 exec(),父进程地址空间的复制可能没有必要。因此可采用一种称为写时复制(copy-on-write)的技术,它通过允许父进程和子进程最初共享相同的页面来工作。这些共享页面标记为写时复制,如果任何一个进程写入共享页面,那么就创建共享页面的副本。

9.4 页面置换

如果增加了多道程度,那么可能会过度分配(over-allocating)内存。如果有40个帧的内存,运行6个进程,每个进程有10个页面,但实际上只使用5个页面,那么会有更高的CPU利用率和吞吐量,并且还有10个帧可作备用。但是对于特定数据集合,每个进程可能会突然试图使用其所有页面,从而共需要60帧,而只有40帧可用。还需考虑到内存还用于I/O缓存等其他方面。这时操作系统有多个选项,终止用户进程、交换出一个进程等。这里我们讨论最常见的解决方案:页面置换(page replacement)。

9.4.1 基本页面置换

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

  • 1.找到所需页面的磁盘位置。
  • 2.找到一个空闲帧:a.如果有空闲帧就使用。 b.若没有,则使用页面置换算法来选择一个牺牲帧(victim frame)。 c.将牺牲帧的内容写到磁盘上,修改对应的页表和帧表。
  • 3.将所需页面读入空闲帧,修改页表和帧表。
  • 从发生缺页错误位置,继续用户进程。

采用修改位(modify bit)(或脏位(dirty bit))可减少这种开销。采用这种方案,每个页面或帧都有一个修改位,两者之间的关联采用硬件。可通过修改位来判断页面是否被修改过,只有被修改过页面才用写回磁盘(因为磁盘本身存在这个页面,如果没有修改过则不用写回)。以此来减低I/O时间。

为实现请求调页,必须解决两个主要问题:设计帧分配算法(frame-allocation algorithm)和页面置换算法(page-replacement algorithm)。也就是说,如果有多个进程在内存中,必须决定要为每个进程分配多少帧;当需要页面置换时,必须选择要置换的帧。如何选择特定置换算法?一般来说,我们想要一个缺页错误率最低的算法。

可以这样评估一个算法:针对特定内存引用串,运行某个置换算法,并计算缺页错误的数量。内存引用的串称为引用串(reference string)。

9.4.2 FIFO页面置换

FIFO页面置换算法为每个页面记录了调到内存的时间,当必须置换页面时,选择最旧的页面。并不需要记录确切时间,只需创建一个FIFO队列来管理所有内存页面,置换的是队列的首个页面。当需要调入页面到内存时,就将它加到队列的尾部。FIFO页面置换算法易于理解和编程,但是它的性能并不总是十分理想。FIFO页面置换算法还可能出现 Belady异常(Belady’s anomaly):对于有些页面置换算法,随着分配帧数量的增加,缺页错误率可能会增加。

9.4.3 最优页面置换

发现Belady异常的一个结果是寻找最优页面置换算法(optimal page-replacement algorithm),这个算法具有所有算法最低的缺页错误率,对于给定数量的帧会产生最低的可能的缺页错误率,并且不会遭受Belady异常。然而最优置换算法难以实现,因为需要引用串的未来知识。因此,最优算法主要用于比较研究。

这种算法确实存在,它被称为OPT或MIN。简单地说:置换最长时间按不会使用的页面。

9.4.4 LRU 页面置换

如果最优算算法不可行,那么最优算法的近似或许称为可能。FIFO和OPT算法的关键区别在于除了时间上向前看或向后看之外,FIFO算法使用的时页面调入内存的时间,OPT算法使用的时页面将来使用的时间。如果我们使用最近的过去作为不远将来的近似,那么可以置换最长时间没有使用的页。这种方法称为最近最少使用算法(Least-Recent-Used algorithm,LRU algorithm)。

LRU策略通常用作页面置换算法,并被认为是不错的策略,它的主要问题是,如何实现LRU置换。LRU页面置换算法可能需要重要的硬件辅助。它的问题是,确定由上次使用时间定义的帧的顺序。两个实现是可行的:

  • 计数器:为每个页表条目关联一个使用时间域,每次引用时更新引用的“时间”。
  • 堆栈:每当一个页面被引用时,它就从堆栈中移除,并放在顶部。这样最近使用的页面总在堆栈的顶部,最近最少使用的页面总在底部。

像最优置换一样,LRU置换没有Belady异常。这两个属于同一类算法,称为堆栈算法(stack algorithm),都绝不可能由Belady异常。

9.4.5 近似 LRU 页面置换

很少有计算机系统能够提供足够的硬件来支持真正的LRU页面置换算法。然而,许多系统都通过引用位(reference bit)的形式提供一定的支持。每当引用一个页面时,它的页面引用位就被硬件置位。页表内的每个条目都关联着一个引用位。最初所有引用位由操作系统清零(置0),当用户进程执行时,引用到的页面引用位由硬件置1。一段时间后可通过检查引用位判断页面的使用与否,虽然不知道使用顺序。这种信息是许多近似LRU页面置换算法的基础。

9.4.5.1 额外引用位算法

通过定期记录引用位,我们可以获得额外的排序信息。例如,可以为内存中的页表的每个页面保留一个8位的字节。定时器中断定期地(如每100ms)将控制传到操作系统,操作系统将每个页面引用位移到其8位字节的高位,将其他位右移一位,并丢弃最低位。可将这个8位二进制数看作无符号数,数值越大代表最近使用的时间离现在越近,反之表示最后一次引用时间离现在越远。所以置换时选择具有最小值的页面,但不能保证数字是唯一的,这时可以在这些页面之间采用FIFO来选择置换。当然,移位寄存器的历史数位可以改变,并可以选择以便使更新尽可能快

9.4.5.2 第二次机会算法

第二次机会置换的基本算法是一种 FIFO 算法。当选择一个页面时检查其引用位,如果值0,则直接置换此页面;如果值为1,那么给此页面第二次机会,并继续选择下一个 FIFO 页面。当一个页面获得第二次机会时,其引用位被清楚,并且到达时间被设为当前时间。因此获得第二次机会的页面,在所有其他页面被置换(或获得第二次机会)之前,不会被置换。此外,如果一个页面经常使用以至于其引用位总是得到设置,那么它不会被置换。

实现第二次机会算法(有时称为时钟算法(clock algorithm))的一种方式时采用循环队列。当需要一个帧时,指针向前移动直到找到一个引用位为0的页面。向前移动时,它会清除引用位。一旦找到牺牲页面,就置换该页面,并且在循环队列的这个位置上插入新页面。

9.4.5.3 增强型第二次机会算法

通过将引用位和修改位作为有序对,可以改进二次机会算法。有了这两个位,就有下面四种可能的类型:

  • (0,0):最近未使用、未修改,最佳的页面置换。
  • (0,1):最近未使用但修改过的页面,不太好替换,因为替换之前需要将页面写出。
  • (1,0):最近使用过但未修改的页面,可能很快再次使用。
  • (1,1):最近使用过且修改过,可能很快再次使用,且置换之前需要将页面写出到磁盘。

可使用与时钟算法一样的方案,检查页面属于哪个类型,替换非空的最低类型中的第一个页面。可能需要多次扫描循环队列才能找到要置换的页面。这种算法与更简单的时钟算法的主要区别在于:这里为那些已修改页面赋予更高级别,从而降低了所需I/O数量。

9.4.6 基于计数的页面置换

可以为每个页面的引用次数保存一个计数器,并且开发以下两个方案:

  • 最不经常使用(Least Frequently Used,LFU):置换具有最小计数的页面。有一种可能是一个页面在进程的初始阶段被大量的使用,此时它有较的计数,但随后不在使用,将会因为计数较大而一直被保留在内存中,一种解决方案是定期的将计数右移一位,以形成指数衰减的平均使用计数。
  • 最经常使用(Most Frequently Used,MFU)。

MFU 和 LFU 置换都不常用,它们的实现是昂贵的,并且不能很好的近似 OPT 置换。

9.4.7 页面缓冲算法

除了特定页面置换算法,还经常采用其他措施。例如保留一个空闲帧缓冲池,出现缺页错误时仍然会选择一个牺牲帧,但在写出牺牲帧之前,所需页面就哟啊读到来自缓冲池的空闲帧,这样允许进程尽快重新启动。当牺牲帧被写以后,它被添加到空闲帧池。

这种方法的扩展之一是维护一个修改页面的列表,每当调页设备空闲时,就选择一个修改页面以写到磁盘上,然后重置它的修改位。

另一种修改是,保留一个空闲帧池,记录哪些页面在哪些帧内,因为在帧被写到磁盘后帧内容并未被修改,所以当该帧重用之前,如果再次需要,可以直接从空闲帧直接取出并被使用,这种情况不需要I/O。

9.4.8 应用程序与页面置换

某些情况下,通过操作系统的虚拟内存访问数据的应用程序比操作系统根本没有提供缓冲区更差。一个典型的例子是数据库,它提供自己的内存管理和I/O缓冲 ,类似这样的程序比提供通用目的算法的操作系统更能理解自己的内存与磁盘使用。

9.5 帧分配

9.5.1 帧的最小数

每个进程的最小帧数是由体系结构决定的。

9.5.2 分配算法

最简单的方式是平均分配(equal allocation)。
还可以使用比例分配(proportional allocation)。

注意,对于上面两种分配方法,高优先级进程与低优先级进程同样处理,事实上我们可能希望给予高优先级进程更多内存以加速执行,同时损害低优先级进程。一种解决方案是根据进程的优先级或大小和优先级组合分配内存。

9.5.3 全局分配与局部分配

为各个进程分配帧的一个重要因素是页面置换。由于多个进程竞争帧,可以将页面置换算法分为两大类:全局置换(global replacement)(允许一个进程从所有帧的集合中选择一个置换帧)和局部置换(local replacement)(每个进程只从它自己分配的帧中进行选择)。

考虑这样一种分配方案,可以允许高优先级进程从低优先级进程中选择帧用于置换,当然,进程也可以从自己的帧中选择帧进行置换。这样会产生的一个问题是,进程不能控制它自己的缺页错误率,一个进程的内存页面的集合不但取决于进程本身的调页行为,同时取决于其他进程的调页行为。对于局部置换算法,进程的内存页面集合仅受该进程本身的调页行为影响,然而局部置换由于不能使用其他进程的较少使用的内存页面,可能会阻碍一个进程。这样全局置换通常会有更好的系统吞吐量,因此是更常用的方法。

9.5.4 非均匀内存访问

具有明显不同的内存访问时间的系统称为非均匀内存访问(Non-Uniform Memory Access,NUMA)系统。对于具有多个CPU的系统,给定的 CPU 可以比其他CPU更快的访问内存的某些部分,这样的性能差异是由于 CPU 和内存在系统中互联造成的(例如,多个系统板的系统,每个系统板包含多个 CPU 和一定的内存)。 这样的系统要慢于内存和 CPU 位于同一主板的系统。

管理哪些页面帧位于哪些位置能够明显影响NUMA系统的性能。管理的目的是让分配的内存帧“尽可能靠近”运行进程的 CPU 。“靠近”的定义是“具有最小的延迟”,这通常意味着与 CPU 一样位于同一系统板。

9.6 系统抖动

高度的页面调度活动称为抖动(thrashing)。如果一个进程的调页时间多于它的执行时间,那么这个进程就在抖动。

9.6.1 系统抖动的原因

操作系统监视 CPU 利用率,如果 CPU 利用率太低,那么通过向系统引入新的进程来增加多道程度。采用全局置换算法会置换任何页面,不管这些页面属于哪个进程。在这两个前提下,考虑如下情况:进程在执行中进入一个新阶段,并且需要更多的帧,它开始出现缺页错误,并从其他进程获取帧。然而,这些进程也需要这些页面,因此它们也会出现缺页错误,并且从其他进程那里获取帧。这些缺页错误进程必须使用调页设备以将页面换进和换出。当它们为调页设备排队时,就绪队列清空,随着进程等待调页设备,CPU利用率会降低。此时系统又会引入新的进程来增加多道程度,提高 CPU 利用率,然而,新引入的进程试图从其他运行进程中获取帧来启动,从而导致更多的缺页错误和更长的调页设备队列。因此 CPU 利用率进一步下降,CPU 调度程序还会试图再次增加多道程度。这样就出现了抖动,系统吞吐量陡降,缺页错误率显著增加。结果有效内存访问时间增加。没有工作可以完成,因为进程总在忙于调页。

通过局部置换算法(local replacement algorithm)或优先权置换算法(priority replacement algorithm)可以限制系统抖动。如果一个进程开始抖动,由于采用局部置换,那么它不能从另一个进程中获取帧,不能导致后者抖动。但是抖动的进程,大多数时间会排队等待调页设备,由于调页设备的平均队列更长,缺页错误的平均等待时间也会增加。即使不再抖动的进程,有效访问时间也会增加。

为了防止抖动,应为进程提供足够多的所需帧数。但是如何知道进程“需要”多少帧?有多种技术。工作集策略研究一个进程实际使用多少帧。这种方法定义了进程执行的局部性模型(locality model)。局部性模型指出,随着进程执行,它从一个局部移向另一个局部。局部性是最近使用页面的一个集合。一个程序通常由多个不同的可能重叠的局部组成。

9.6.2 工作集模型

工作集模型(working-set model)是基于局部性假设的。这个模型采用参数 Δ 定义工作集窗口(working-set window)。它的思想是检查最近 Δ 个页面引用。这最近 Δ 个页面引用的页面集合称为工作集(working-set)。如果一个页面处于活动使用状态,那么它处于工作集中。如果它不再使用,那么它在最后一次引用的 Δ 时间单位后,会从工作集中删除。

如果系统内的每个工作集通过计算为 WSS_i,那么 D = ∑ WSS_i,D为帧的总需求量。每个进程都使用其工作集内的页面。因此,进程 i 需要 WSS_i 帧。如果总需求大于可用帧的总数,则将发生抖动,因此有些进程得不到足够的帧数。一旦选中了 Δ ,工作集模型的使用就很简单。

工作集模型的困难是跟踪工作集。

9.6.3 缺页错误频率

用于控制抖动,工作集模型似乎有点笨拙,采用缺页错误频率(Page-Fault Frequency,PPF)的策略是一种更为直接的方法抖动具有高缺页错误率,因此需要控制缺页错误率。当缺页错误率太高时说明该进程需要更多帧;当缺页错误率太低时说明该进程拥有太多的帧。我们可以设置所需缺页错误率的上下限,超过上限则分配一帧,低于下限则删除一帧。如果缺页错误率增加并且没有空闲帧可用,那么也不得不换出一个进程。

9.7 内存映射文件

采用标准系统调用 open(), read(), write() 来顺序读取磁盘文件,每个文件访问都需要系统调用和磁盘访问。或者采用虚拟内存技术,以将文件作为常规内存访问。这种方法称为内存映射(memory mapping)文件,允许一部分虚拟内存与文件进行逻辑关联。这可能导致显著的性能提高。

9.7.1 基本机制

实现文件的内存映射是,将每个磁盘块映射到一个或多个内存页面。文件的读写就按常规的内存访问来处理。内存映射文件的写入不一定是堆磁盘文件的即时(同步)写入。有的操作系统定期检查文件的内存映射页面是否已被修改,以便选择是否更新到物理文件。当关闭文件时,所有内存映射的数据会写到磁盘,并从进程虚拟内存中删除。多个进程可以允许并发地内存映射同一文件,以便允许数据共享。

9.7.3 内存映射 I/O

在 I/O 的情况下,每个 I/O 控制器包括保存命令和传输数据的寄存器。通常,专用 I/O 指令允许在这些寄存器和系统内存之间进行数据传输。为了更方便地访问 I/O 设备,许多计算机体系结构提供了内存映射 I/O(memory-mapped I/O)。这种情况下,一组内存地址专门映射到设备寄存器,对这些内存地址的读取和写入,导致数据传到获取自设备寄存器。

9.8 分配内核内存

下面讨论两个用于管理内核进程的策略:伙伴系统和 slab 分配。

9.8.1 伙伴系统

伙伴系统(buddy system)从物理连续的大小固定的段上进行分配。从这个段上分配内存,采用2的幂分配器(power-of-2 allocator)来满足请求分配单元的大小为2的幂。考虑一个简单的例子:假设内存段的大小最初是256KB,内核请求21KB的内存。最初这个段分为两个伙伴,称为 AL 和 AR ,每个的大小为128KB;这两个伙伴之一进一步分成两个64KB的伙伴,即 BL 和 BR,从21KB开始的下一个大的2的幂是32KB,因此 BL 或 BR 再次划分为两个32KB的伙伴 CL 和 CR。其中一个分配给内核。伙伴系统的一个优点是通过称为合并(coalesce)的技术,可以将相邻伙伴快速组合以形成更大分段。伙伴系统的明显缺点是:由于元整到下一个2的幂,很可能造成分配段内的碎片。

9.8.2 slab 分配

每个 slab 由一个或多个物理连续的页面组成。每个 cache 由一个或多个 slab 组成。每个内核数据结构都有一个 cache,每个 cache 含有内核数据结构的对象实例(称为 object)。当 cache 请求内存时, slab 分配器首先尝试在部分为空的 slab 中用空闲对象来满足请求,如果不存在,则从空的 slab 中分配空闲对象,如果没有空的 slab 可以,则从连续物理页面分配新的 slab ,并将其分配给 cache;从这个 slab 上再分配对象内存。

9.9 其他注意事项

预调页面、页面大小、TLB 范围、倒置页表、程序结构、I/O联锁与页面锁定

9.9.1 预调页面

预调页面(prepaging)试图阻止进程启动或重启时由于缺页错误发生的大量最初调页。预调页面在有些情况下可能具有优点,问题在于,采用预调页面的成本是否小于处理相应缺页错误的成本。

9.9.2 页面大小

减小页面大小增加了页面数量,从而增加了页表的大小,然而较小的页面可以更好的利用内存。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值