操作系统内存管理

来源:互联网 发布:js获取手机唯一标识码 编辑:程序博客网 时间:2024/06/05 20:52

本文是Modern Operating Systems第三章的笔记。


没有内存抽象


最简单的抽象就是没有抽象,早期的个人电脑上运行的程序可以直接看到物理内存。这导致了不太可能在一个电脑上运行两个程序,因为一个程序可能破坏另一个程序保存的数据(因为它们可以直接看到物理内存,便可以覆盖另一个程序的数据)。

这里写图片描述

早期个人电脑的内存模型如上图,一个用户程序运行完之后才能运行另一个。




内存抽象:地址空间


将物理内存暴露给进程有很多的弊端。用户进程可能会导致操作系统崩溃,无法并发运行多个程序等等。

地址空间的概念

为了能够并发运行多个程序,使得它们不相互干扰,我们必须解决两个问题:保护和重定位。

地址空间是一种内存抽象,它能够很好的解决以上两个问题。一个地址空间就是进程有权使用的内存范围内的地址集合。每个进程相对于其他进程有独立的地址空间。

困难的是如何给予每个进程各自的地址空间。


基地址和界限寄存器

一个简单的方案是使用动态重定位,它简单地将每个进程的地址空间映射到物理内存中的另一个地方。我们需要两个硬件寄存器,它们经常被称作基地址寄存器界限寄存器

每次进程引用内存,执行指令,或者是读写数据,CPU自动将基地址的值添加到访问的内存地址上。同时,它会检测访问的地址是否超过界限寄存器中的值,如果是,那么会抛出错误。

使用基地址和界限寄存器的重定位方法的缺点是每次访问内存时都需要执行加法和比较。


交换

因为电脑的物理内存大小不足以容纳所有的进程,我们必须采取一些措施处理这个情况,有两个方法可供选择。

一种叫做交换,运行程序就把整个程序加载到内存,运行一段时间后就把程序放回到磁盘上。交换会在内存中制造很多空隙,但通过内存压缩可以将所有进程移动到一边,注意内存压缩需要大量CPU时间。

另一种策略叫做虚拟内存,允许程序在只加载一部分的情况下运行。


管理内存

可以使用位图和空闲链表管理内存。

当时用空闲链表管理内存时,为创建的进程分配内存有以下几种算法:

  1. first fit:从链表头部开始搜索,直到发现第一个足够大的空闲块,空闲块分为两部分,一部分用于加载进程,另一部分放回链表。
  2. next fit : 搜索的起始位置是上一次找到空闲块的位置,其他都和first fit一样。
  3. best fit:搜索整个链表,选取最小的又足够大的内存块。
  4. quick fit:拥有几个独立的空闲链表,每个链表链接的空闲块大小固定。比如,它可能是一个有多个选项的表,第一个选项是一个指针,指向4KB大小空闲块的链表,第二个选项是指向8KB大小空闲块的链表,等等。

Best fit速度比first fit要慢,而且相比first fit和second fit会造成更多内存的浪费,因为每次挑选的空闲块的大小都最接近所需内存,使用后剩余的空闲块太小了以至于很难被重新利用。

quick fit的缺点在于如果进程终止了,或者被交换出去,找到相邻的空闲块或进程,查看是否可以合并的代价是高昂的。如果合并没有发生,那么内存将会很快分裂成大量的无法被进程使用的小空闲块。




虚拟内存


应用程序比内存大的问题很早就有了,一个解决方法就是将程序切割成内存碎片,叫做overlays.当一个程序开始时,所有需要加载到内存中的只是overlay manager,然后它立马加载并运行overlay 0。接着加载overlay 1,overlay1可能会覆盖overlay0。

另一个是虚拟内存,虚拟内存的基本理念是每个程序都有它的地址空间,地址空间被分割成很多块,那些块被称为。这些页被映射到物理内存,但运行程序不需要所有的页都映射。可以说,虚拟内存是基地址和界限寄存器思想的泛化。通过使用虚拟内存,整个地址空间可以以相当小的单元被映射到物理内存。


页面调度

这里写图片描述

程序形成的地址叫做虚拟地址,形成虚拟地址空间。
在没有虚拟内存的电脑上,虚拟地址被直接放在总线上,这会造成相同地址的物理内存字会被读写。当使用虚拟内存的时候,虚拟地址会先经过内存管理单元(MMU),内存管理单元将虚拟地址映射为物理内存地址,如上图所示。

虚拟地址空间被分割成固定大小的单元,那些单元叫做页,对应的在物理内存中分割的单元叫做页帧。

在实际的硬件中,Present/absent bit被用来跟踪那些页存在于内存中。如果页没有在内存中,那么会造成CPU抛出页错误

这里写图片描述


页表

从数学的角度出发,页表就是一个函数,虚拟地址是变量,物理地址是输出。

页表项的大小可能根据机器不同而不同,但32位是常见的大小。

这里写图片描述

最重要的是Page frame number,毕竟这是要输出的值。
如果Present/absent 是1,那么选项就是有效的,如果是0,虚拟页则不在内存中。
Protection bit告诉哪种访问会被允许[wrx]。
如果一个页被写入,硬件会自动设置Modified,如果一个页被修改了(dirty),那么它必须写回到磁盘里。如果它没被修改,那么它直接被抛弃,因为磁盘上的备份依旧有效。
Referenced位当一个页被引用的时候设置,可能是读或者是写。这个值帮助操作系统在发生页错误的时候选择一个页从内存中剔除。
最后一个位允许禁止该页的缓存。

总结一下,虚拟内存做的事情就是创建一个物理内存的抽象——地址空间,就像是进程是CPU的抽象。虚拟内存通过将虚拟地址空间分割成页,将每个页映射到页帧或者不映射来实现。所以这一章主要讲操作系统创建的抽象,以及抽象是如何管理的。


加速页面调度

任何页面调度系统中都面临两个问题:1从虚拟地址到物理地址的隐射必须要快。2如果虚拟地址空间很大,那么页表会很大。

第1个问题是基于这样的事实:在每次内存引用时,虚拟地址到物理地址的页面调度必须完成。所有的指令最终都从内存中来,如果一条指令的执行,假如说需要1纳秒,那么页表查询必须在0.2纳秒内完成避免页面调度成为主要的瓶颈。

第2个问题是由于现代操作系统至少使用32位的虚拟地址,更常见的是64位。比如说,页大小是4KB,32位地址空间会有1百万个页。页表必须有一百万个选项,并且每个进程需要自己的页表。


翻译后备缓冲区

我们来看一下怎么加速页面调度,绝大多数优化的技术都从 “页表位于内存中”这点入手。
如果没有页面调度,那么指令只需要执行一次内存引用,来获取指令。但如果使用了页面调度,至少需要额外一次内存引用来访问页表。因为指令的执行速度通常由CPU从内存获得数据的速度决定。每一次内存引用却需要引用内存两次,这使得性能下降了一半。这就是需要后备缓冲区的原因
·
电脑的设计者提出了一个解决方案,这个方案基于这样的观察——绝大多数的程序会多次访问小部分的页(程序运行的局部性)。于是他们给电脑配备了一个小的硬件设备,这个设备可以不经过页表,将虚拟地址转化为物理地址。这个设备叫做TLBTranslation Lookaside Buffer,翻译后备缓冲区)。它经常在MMU内部,由少量的选项构成。每个选项包含一个页的信息,与页表中的信息一一对应,除了虚拟页号,这在页表中不需要,还有一个标识选项是否有效(是否正在被使用)的位。

这里写图片描述

当一个虚拟地址被传递给MMU翻译时,硬件首先会通过比较这个地址和所有的选项来检查是否该虚拟页在TLB中。如果发现了有效的匹配,并且访问权限与保护位兼容,页帧会直接从TLB中拿出来,而不需要进入页表。

如果虚拟页编号不在,MMU检测到miss,会进行正常的页表查询。然后它会从TLB中剔除一个选项,用刚刚查到的代替。当一个选项从TLB中被剔除的时候,modified bit会被拷贝回内存中的页表选项。除了引用位,其他的值都已经在那。当TLB从页表中加载时,从内存中复制所有的域。


多级页表

多级页表的作用是避免所有的页表总是在内存中。那些不需要的页表就不应该在内存中。
这里写图片描述

一级页表有1024个选项对应10位的PT1域。当一个虚拟地址传递给MMU时,它会提取PT1域,然后使用这个值作为一级页表的索引,这1024个选项中的任意一个都代表着4M。一级页表中的选项会产生二级页表的地址。


反置页表

对于32位虚拟地址空间,多级页表表现得相当好。但对于64位电脑,情况就大大不同了。如果地址空间是2^64字节,使用4KB大小的页,我们就需要一个有2^52个选项的页表。如果一个选项是8字节,那么这个页表大于30PB,很明显这并不是一个好的主意。

一种解决方案就是反置页表,这个解决方案,只在物理内存中存在一个页表,而不是每个地址空间一个页表。举个例子,64位虚拟地址,4KB大小的页,1GB的RAM,反置页表只需要262144个选项。

虽然反置页表节约了空间,但从虚拟地址到物理地址的转化变得十分困难。每一次转换都需要搜索整个页表。我们可以使用TLB缓解情况,但这并没有从根本上解决问题。一个可行的解决方案是通过hash表来完成搜索。




换页算法


当页错误发生的时候,操作系统必须在页表中选择一个页剔除为新来的页提供空间。如果要剔除的页已经被修改,那么它就要写回到内存中,如果该页没有改变,可以直接剔除。

这里写图片描述


最优换页算法

最优换页算法就是剔除将来最后才用到的页。

唯一的问题就是难以实现。但可以作为其他换页算法的参考。


先进先出置换算法

操作系统会维护当前一个在内存中的所有页的链表,最近到达的页在尾部,最早到达的页在头部。当发生页错误的时候,头部的页被移除,新的页被添加到链表尾部。


第二次机会算法

为了避免丢弃一个经常被使用的页,一个对于先进先出置换算法简单的修改是检查最老的页的引用位。如果是0,那么页又老又没有被使用,可以被丢弃。但如果引用位是1,那么引用位被清零,页面被放到链表的尾部,并且更新它的载入时间。


时钟置换算法

虽然第二次机会算法是合理的算法,但它没有效率,因为它不断地在链表上移动页。更合适的方法是保持所有的页帧以时钟的形式在一个循环链表上。句柄指向最老的页。

当发生页错误的时候,句柄指向的页会被检查。如果引用位是0,页面被剔除,新页面插入到这个位置,句柄指向下一个位置。如果引用位是1,引用位清零,指针指向下一个页。这个过程会一直循环直到找到引用位为0的页。


最近最少使用算法

最近最少使用算法非常接近最优换页算法的效率,它基于这样的事实——最近执行的指令通常会再一次被执行。
当一个页错误发生的时候,剔除最长时间没有使用的那个页。这个策略叫做最近最少使用算法(LRU,Least Recently Used)。

虽然LRU理论上可行,但代价昂贵。为了完整的实现LRU,有必要维护内存中所有页的链表,最近使用的页在前端,最少使用的页在末端。难点在于每次内存引用后链表必须更新。在链表中找到该页,删掉它,然后把它移动到前端,是一个非常消耗时间的操作。


最不经常使用算法

因为LRU算法理论可行,难以实践,所以人们提出了NFU(Not Frequently Used)算法。它要求每一个页都有与之联系的计数器,初始值为0。在每一个时钟周期,操作系统会浏览内存中的所有页。对于每一页,它们的引用位,0或者1,会被添加到计数器上。计数器粗略地跟踪了每个页被引用的频繁程度。当发生页错误时,最低计数器的页会被替换。

但NFU的主要问题是它从不会忘记任何事情。比如,在多遍扫描编译程序,在第一轮时经常使用的页,它的计数器在后续的操作中依旧有很高的值。


老化算法

针对NFU做了2点改进。首先,在引用位被加到计数器前,计数器的值会右移1位。其次,引用位会被添加最左位(leftmost bit)而不是最右位。


工作集换页算法

页面调度的基本方法是,程序启动时,没有一个页在内存中。当CPU取指令的时候,它会产生页错误,导致操作系统去取包含第一个指令的页。其他由于访问局部变量和栈的页错误紧接着发生。这种策略成为请求页面调度(demand paging),因为页只有在需求的时候加载,然不是提前加载。

在任何时刻,程序总是引用一小部分页。程序当前正在使用的页集合被成为工作集(working set)。如果物理内存太小,无法容纳整个工作集,那么会造成频繁的换页操作,影响程序性能,这被成为页抖动(thrashing).

许多页面调度系统会跟踪每个进程的工作集,并确保进程运行前,工作集在内存中。这个方法被称为工作集模型。在进程运行前加载页被称为预约式页面调度(prepaging)

这里写图片描述

因为工作集随着时间变化很小,我们可以根据程序上一次停止运行时候的工作集来猜测程序重启时需要哪些页。

我们可以得出一个可能的页面调度算法:当发生页错误的时候,找到一个不在工作集中的页面替换掉它。

直接实现工作集的定义代价十分昂贵,所以有很多种近似的方法。一种通用的近似就是定义工作集为过去100毫秒中使用的页集合而不是之前1000万次内存引用中使用的页集合。

现在让我们看一下基于工作集的换页算法。(当前虚拟时间是进程从运行到现在使用的CPU时间)
这里写图片描述


工作集时钟换页算法

原始的工作集算法很笨重,因为每当发生页错误,要想找到合适的替换页,它要遍历整个页表。一个基于时钟换页算法但使用了工作集信息的算法,称为工作集时钟换页算法。

需要的数据结构是页帧的循环链表。

//还没写完。。