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

背景

  • 代码必须装入内存才能执行,但是并不是所有代码必须全部装入内存
    • 错误代码
    • 不常用的函数
    • 大的数据结构
  • 局部性原理:一个程序只要部分装入内存就可以运行
    • 整个程序不是同一时间都要运行
  • 程序部分装入技术优点
    • 进程大小不再受到物理内存大小限制,用户可以在一个虚拟的地址空间编程,简化了编程工作量
    • 每个进程需要的内存更小,因此更多进程可以并发运行,提供了CPU的利用率
    • I/O更少(载入的内容更少),用户程序运行更快

局部性原理

  • 即在一较短的时间内,程序的执行仅局限于某个部分;相应地,它所访问的存储空间也局限于某个区域
    • 程序执行时,除了少部分的转移和过程调用外,在大多数情况下仍然是顺序执行的
    • 过程调用将会使程序的执行轨迹由一部分区域转至另一部分区域,过程调用的深度一般小于5。程序将会在一段时间内都局限在这些过程的范围内运行
    • 程序中存在许多循环结构,多次执行
    • 对数据结构的处理局限于很小的范围

虚拟内存

  • 虚拟存储技术:当进程运行时,先将其一部分装入内存,另一部分暂留在磁盘,当要执行的指令或访问的数据不在内存时,由操作系统自动完成将它们从磁盘调入内存执行
  • 虚拟地址空间:分配给进程的虚拟内存
  • 虚拟地址:在虚拟内存中指令或数据的位置
  • 虚拟内存:把内存和磁盘有机结合起来使用,得到一个容量很大的“内存”,即虚存
    • 区分开物理内存和用户逻辑内存
    • 只有部分运行的程序需要在内存中
    • 逻辑地址空间能够比物理地址空间大
    • 必须允许页面能够被换入和换出
    • 允许更有效的进程创建
  • 虚存是对内存的抽象,构建在存储体系之上,由操作系统来协调各存储器的使用
  • 虚拟内存大于物理内存
  • 虚拟存储器的大小由2个因素决定:
    • 操作系统字长
    • 内存外存容量
  • 使用虚拟内存的共享库:
    • 通过将共享对象映射到虚拟地址空间,系统库可用被多个进程共享
    • 虚拟内存允许进程共享内存
    • 虚拟内存可允许在创建进程期间共享页,从而加快进程创建

写时复制

  • 写时复制允许父进程和子进程在初始化时共享页面
    • 如果其中一个进程修改了一个共享页面,会产生副本
    • 更加高效
    • 应用在Windows XP,Linux等系统
  • 当确定采用写时复制页面时,重要的是注意空闲页面的分配位置
    • 许多操作系统为这类请求提供了一个空闲的页面池
    • 当进程的堆栈或堆要扩展时或有写时复制页面需要管理时,通常分配这些空闲页面。
    • 操作系统分配这些页面通常采用按需填零的技术。按需填零页面在需要分配之前先填零,因此清除了以前的内容
  • vfork:fork()变形,不使用写时复制
    • 父进程被挂起,子进程使用父进程的地址空间
    • 如果子进程修改父地址空间的任何页面,那么这些修改过的页面对于恢复的父进程是可见的
    • 应谨慎使用vfork(),以确保子进程不会修改父进程的地址空间
    • 当子进程在创建后立即调用exec()时,可使用vfork()
    • 因为没有复制页面,vfork()是一个非常有效的进程创建方法
  • 例子:


虚拟内存的实现

  • 虚拟内存能够通过以下手段来执行实现:
    • 虚拟页式(虚拟存储技术+页式存储管理)
    • 虚拟段式(虚拟存储技术+段式存储管理)
  • 虚拟页式有两种方式:
    • 请求分页( Demand paging )
    • 预调页(Prepaging)
      • 以预测为基础,将预计不久后便会被访问的若干页面,预先调入内存

请求分页

基本思想(虚拟页式存储管理)

  • 进程开始运行之前,不是装入全部页面,而是装入一个或零个页面
    • 装入零个页面的调页为:纯请求调页
  • 运行之后,根据进程运行需要,动态装入其他页面
  • 当内存空间已满,而又需要装入新的页面时,则根据某种算法置换内存中的某个页面,以便装入新的页面

按需调页

  • 只有在一个页需要的时候才把它换入内存
    • 需要很少的I/O
    • 需要很少的内存
    • 快速响应
    • 支持多用户
  • 类似交换技术,粒度不同
    • 交换程序(swapper)对整个进程进行操作
    • 调页程序(pager)只是对进程的单个页进行操作
  • 需要页⇒ 查阅此页
    • 无效的访问 ⇒ 中止
    • 不在内存 ⇒ 换入内存
  • 懒惰交换:只有在需要页时,才将它调入内存
    • 交换程序(swapper)对整个进程进行操作
    • 调页程序(pager)只是对进程的单个页进行操作
  • 调页程序不是调入整个进程,而是把哪些要使用的页调入内存
    • 调页程序就避免了读入哪些不使用的页,也减少了交换时间和所需的物理内存空间
    • 有效-无效位方案实现

有效-无效位

  • 在每一个页表的表项有一个有效-无效位相关联,1表示在内存,0表示不在内存
  • 在所有的表项,这个位被初始化为0
  • 在地址转换中,如果页表表项位的值是0 ⇒缺页中断(page fault)

缺页中断(页错误)

  • 如果对一个页的访问,首次访问该页需要陷入OS ⇒ 缺页中断
  • 1.访问指令或数据
    • 发现有效无效位为0
  • 2.查看另一个表来决定
    • 无效引用 ⇒ 终止
    • 仅仅不在内存
  • 3.找到页在后备存储上的位置
  • 4.得到空闲帧,把页换入帧
  • 5.重新设置页表,把有效位设为v
  • 6.重启指令: 近未使用


请求分页讨论

  • 极端情况:进程执行第一行代码时,内存内没有任何代码和数据
    • 进程创建时,没有为进程分配内存,仅建立PCB
    • 导致缺页中断
    • 纯请求分页
  • 一条指令可能导致多次缺页(涉及多个页面)
    • 幸运的是,程序具有局部性(locality of reference)
  • 请求分页需要硬件支持
    • 带有效无效位的页表
    • 交换空间
    • 指令重启(请求调页的关键要求是在缺页错误后重新启动任何指令的能力)

请求分页的性能

  • 缺页率(缺页的概率):0 <= p <= 1.0
    • 如果 p = 0,没有缺页
    • 如果 p = 1,每次访问都缺页
  • 有效访问时间(EAT):
    • EAT = (1 – p) x 内存访问时间+ p x 页错误时间
  • 对于请求调页,降低缺页错误率是极为重要的。否则,会增加有效访问时间,从而极大地减缓了进程的执行速度
  • 页错误时间(包含多项处理的时间,主要有三项):
    • 处理缺页中断时间
    • 读入页时间
    • 重启进程开销
    • [页交换出去时间](不是每次都需要)
    • (注:不包含内存访问时间:内存访问时间与页错误时间相比可忽略不计内存访问时间被包含在其中)

请求分页性能优化

  • 页面转换时采用交换空间,而不是文件系统
    • 交换区的块大,比文件系统服务快速
  • 在进程装载时,把整个进程拷贝到交换区
    • 基于交换区调页
    • 早期的BSD Unix
  • 对于二进制文件的请求调页,利用文件系统进行交换
    • Solaris和当前的BSD Unix
    • 对于与文件无关的页面部分内容仍旧需要交换区(堆栈等)

页面置换

无空闲页的办法

  • 解决方法
    • 终止进程
    • 交换进程
    • 页面置换,又称页置换、页淘汰
  • 页面置换
    • 找到内存中并没有使用的一些页,换出
    • 算法
    • 性能——找出一个导致小缺页数的算法
    • 同一个页可能会被装入内存多次
  • 页面置换讨论
    • 如果发生页置换,则缺页处理时间加倍
    • 通过修改缺页服务例程,来包含页面置换,防止分配过多
    • 修改(脏)位modify (dirty) bit来防止页面转移过多——只有被修改的页面才写入磁盘
    • 页置换完善了逻辑内存和物理内存的划分——在一个较小的物理内存基础之上可以提供一个大的虚拟内存
  • 需要页面置换的情况


基本页面置换

  • 为了实现请求调页,必须开发两个算法:
    • 如果在内存中有多个进程,那么\(\color{red}{帧分配算法}\)决定为每个进程各分配多少帧
    • 当发生页置换时,\(\color{red}{页置换算法}\)决定要置换的帧是哪一个
  • 基本页面置换方法
    • 1.查找所需页在磁盘上的位置
    • 2.查找一空闲页框
      • 如果有空闲页框,就使用它
      • 如果没有空闲页框,使用页置换算法选择一个“ 牺牲”页框(victim frame)
      • 将“牺牲”帧的内容写到磁盘上,更新页表和帧表
    • 3.将所需页读入(新)空闲页框,更新页表和帧表
    • 4.重启用户进程
  • 页面置换


页面置换算法

  • 目的
    • 最小的缺页率
    • 通过运行一个内存访问的特殊序列(访问序列),计算这个序列的缺页次数
  • 要求
    • 掌握设计思想、算法应用
    • 了解部分算法的实现
  • 优置换置换算法(OPT)
  • 先进先出置换算法(FIFO)
  • 最少最近使用置换算法(LRU)
  • 近似LRU算法
    • 二次机会法

先进先出算法(FIFO)

  • 置换在内存中驻留时间长的页面
  • 容易理解和实现、但性能不总是很好
  • 实现:使用FIFO队列管理内存中的所有页
  • FIFO算法可能会产生Belady异常
    • 更多的页框——更多的缺页


最优置换算法(OPT)

  • 被置换的页是将来不再需要的或最远的将来才会被使用的页
  • 作用:作为一种标准衡量其他算法的性能


最少最近使用置换算法(LRU)

  • 置换长时间没有使用的页
  • 性能接近最优置换算法(OPT)
  • 实现
    • 每一个页表项有一个计数器(时间戳)或栈
    • 开销大,需要硬件支持


LRU近似算法

  • 在没有硬件支持的系统中,可使用LRU近似算法
  • 访问位(引用位):
    • 每个页都与一个位相关联r位,初始值位0
    • 当页访问时设位1
  • 基于引用位的算法
    • 附加引用位算法
    • 二次机会算法
    • 增强型二次机会算法
  • 附加引用位算法
    • 为内存中的每个页设置一个8位字节
    • 在规定时间间隔内,把每个页的引用位转移到8位字节的高位,将其他位向右移一位,并舍弃低位
    • 这8位移位寄存器包含近8个时间周期内的页面使用情况
    • 小值的页为近少使用页,可以被淘汰
  • 二次机会算法
    • 需要引用位(访问位)
    • 如果引用位为0,直接置换
    • 如果将要(以顺时针)交换的页访问位是1,则:
      • 把引用位(访问位)设为0
      • 把页留在内存中
      • 以同样的规则,替换下一个页
    • 实现:时钟置换(顺时针方向,采用循环队列)
    • FIFO的增强算法


基于计数的页面的置换

  • 不经常使用(NFU)算法
    • 需要一个初值为0的软件计数器与每页相联
    • 每次时钟中断时,操作系统扫描内存中的所有页面,将每页的R位(其值为0或1)加到其计数器上
    • 缺页时淘汰计数器值最小的页
    • 实质:跟踪每页被访问的频繁程度
  • 老化算法
    • NFU算法修改
    • 先将计数器右移一位
    • 把R位加到计数器的最左端


页框分配(帧分配)

基本概念

  • 必须满足:每个进程所需要最少的页数
    • 随着分配给每个进程的帧数量的减少,缺页错误率增加,从而减慢进程执行。此外,若在执行指令完成之前发生缺页错误,应重新启动指令。因此,必须有足够的帧来容纳任何单个指令可以引用的所有不同的页面
    • 最小帧数由计算机架构定义
    • 尽管每个进程的最小帧数是由体系结构决定的,但是最大帧数是由可用物理内存的数量决定的
  • 两个主要的分配策略:
    • 固定分配
    • 优先级分配

固定分配

  • 为每个进程分配固定数量的页框
  • 两种分配方式:
    • 平均分配(均分法)
      • 在n个进程中分配m个帧的最容易的方法,给每个进程一个平均值,即m/n帧(忽略操作系统所需的帧)
    • 按比率分配(根据每个进程的大小来分配)
      • 基于各个进程需要不同数量的内存
  • 对于平均分配和比例分配,每个进程分得的数量可以因多道程序而变化。如果多道程序增加,则每个进程会失去一些帧,以提供新进程所需的内存。相反,如果多道程度较低,则原来分配给离开进程的帧会分配给剩余进程

优先级分配

  • 根据优先级而不是进程大小来使用比率分配策略
  • 如果进程Pi产生一个缺页
    • 选择替换其中的一个页框
    • 从一个较低优先级的进程中选择一个页面来替换

全局置换和局部置换

  • 全局置换
    • 进程在所有的页框中选择一个替换页面;一个进程可以从另一个进程中获得页框
  • 局部置换
    • 每个进程只从属于它自己的页框中选择
  • 采用局部置换,分配给每个进程的页框数量不变;采用全局置换,可能增加所分配页框的数量,因为可能从分配给其他进程的页框中选择一个置换
  • 全局置换的问题,进程不能控制其缺页率,局部置换没有这个问题。但局部置换不能使用其他进程不常用的内存
  • 全局置换有更好的系统吞吐量,更为常用

系统抖动

基本概念

  • 如果一个进程没有足够的页,那么缺页率将很高,这将导致:
    • CPU利用率地下
    • 操作系统认为需要增加多道程序设计的道数
    • 系统中将加入一个新的进程
  • 抖动(颠簸):一个进程的页面经常换入换出,进程的调页时间多于它的执行时间
    • 原因:系统内存不足,页面置换算法不合理

局部置换算法

  • 通过局部置换算法可以限制系统抖动(颠簸)
  • 如果一个进程开始颠簸,那么它不能置换其他进程的页框
  • 局部模型
    • 进程从一个局部移到另一个局部
    • 局部可能重叠
  • 局部是由程序结构和数据结构来定义的。局部模型指出,所有程序都具有这种基本的内存引用结构
    • 局部性模型是缓存讨论的背后原理
    • 如果对任何数据类型的访问是随机的而没有规律模式,那么缓存就没有用了
  • 颠簸发生的原因:分配的页框数<局部大小之和

工作集模型

  • ≡ 工作集窗口 ≡ 固定数目的页的引用
  • WSSi(Pi进程的工作集) = 最近中所有页的引用数目(随时间变化)
    • 如果太小,那么它不能包含整个局部
    • 如果太大,那么它可能包含多个局部
    • 如果 = ∞,那么工作集合为进程执行所接触到的所有页的集合
  • D = ∑WSSi ≡ 总的帧需求量
  • 如果D>m ⇨ 抖动(颠簸)
  • 策略:如果D>m,则暂停一个进程
  • 优点:可防止抖动,同时保持尽可能高的多道程序,优化了CPU利用率
  • 困难:跟踪工作集
    • 工作集窗口是一个移动窗口。对于每次内存引用,新的引用出现在一端,最旧的引用离开另一端。如果一个页面在工作集窗口内的任何位置被引用过,那么它就在工作集窗口中
    • 通过定期时钟中断和引用位,能够近似工作集模型


缺页率(PFF)策略

  • 抖动具有高缺页率,可以控制缺页错误率来避免抖动
  • 设置可接收的缺页率
    • 如果缺页率太低,回收一些进程的页框
    • 如果缺页率太高,就分给进程一些页框
  • 可能不得不换出一个进程。如果缺页错误率增加并且没有空闲帧可用,那么必须选择某个进程并将其交换到后备存储。然后,再将释放的帧分配给具有高缺页错误率的进程

内核内存分配

基本概念

  • 用户态进程需要内存时,可以从空闲页框链表中获得空闲页,这些页通常是分散在物理内存中的,进程最后一页可能产生内碎片
  • 内核内存的分配不同于用户内存
  • 通常从空闲内存池中获取,其原因是:
    • 内核需要为不同大小的数据结构分配内存,其中有的小于一页。因此,内核应保守地使用内存,并努力最小化碎片消费
    • 一些内核内存需要连续的物理页。然而,有的硬件设备与物理内存直接交互,即无法享有虚拟内存接口带来地便利,因而可能要求内存常驻在连续物理内存中
  • 内核在使用内存块时有如下特点
    • 内存块的尺寸比较小
    • 占用内存块的时间比较短
    • 要求快速完成分配和回收
    • 不参与交换
    • 频繁使用尺寸相同的内存块,存放同一结构的数据
    • 要求动态分配和回收

伙伴(Buddy)系统

  • 主要用于Linux早期版本中内核底层内存管理
  • 一种经典的内存分配方案
  • 从物理上连续的大小固定的段上分配内存
  • 主要思想:内存按2的幂的大小进行划分,即4KB、8KB等,组成若干空闲块链表;查找链表找到满足进程需求的最佳匹配块
    • 满足要求是以2的幂为单位的
    • 如果请求不为2的幂,则需要调整到下一个更大的2的幂
    • 当分配需求小于现在可用内存时,当前段就分为两个更小的2的幂段,继续上述操作直到合适的段大小
  • 算法
    • 首先将整个可用空间看作一块:2n
    • 假设进程申请的空间大小为s,如果满足 2n-1<s<=2n,则分配整个块,否则将块划分为两个大小相等的伙伴,大小为2n-1
    • 一直划分下去直到产生大于或等于s的最小块分配给进程
  • 优点
    • 可通过合并而快速形成更大的段
  • 缺点
    • 调整到下一个2的幂容易产生碎片

Slab分配

  • 内核分配的另一方案
  • Slab也称为板块,是由一个多个物理上连续的页或内存卡组成,Slab中的内存块需要一起建立或撤销
  • 高速缓存Cache,又称为板块组,含有一个或多个Slab;系统具有多个Cache,分别对应多种尺寸和结构相同的内存块
  • 每个内核数据结构都有一个Cache,如进程描述符、文件对象、信号量等
    • 每个Cache含有内核数据结构的对象实例。例如信号量Cache存储着信号量对象,进程描述符Cache存储着进程描述符对象
  • 当创建Cache时,包括若干个标记为空闲的对象,对象的数量与Slab的大小有关
    • 12KB的Slab(包括3个连续的页)可以存储6个2KB大小的对象。开始所有的对象都标记为空闲
    • 当需要内核对象时,可从cache上直接获取,并标识对象为已使用
  • Slab有三种状态:
    • 满的:Slab中所有对象被标记为使用
    • 空的:Slab中所有对象被标记为空闲
    • 部分:Slab中有的对象被标记为使用,有的对象被标记为空闲
  • 当一个Slab充满了已使用的对象时,下一个对象的分配从空闲的Slab开始分配
    • 如果没有空闲的Slab,则从物理连续页上分配新的Slab,并赋给一个Cache
  • 优点
    • 没有因碎片而引起的内存浪费
      • 因为每个内核数据结构都有相应的Cache,而每个Cache都由若干Slab组成,每个Slab又分为若干与对象大小相同的部分
    • 内存请求可以快速满足
      • 由于对象预先创建,所以可以快速分配,刚用完对象并释放时,只需要标记为空闲并返回,以便下次使用
  • 简单块列表(SLOB):分配器用于有限内存的系统,例如嵌入式系统。SLOB工作采用3个对象列表:小(用于小于256字节的对象)
    、中(用于小于1024字节的对象)和大(用于小于页面大小的对象)。内存请求采用首先适应策略,从适当大小的列表上分配对象

其他注意事项

预先调页

  • 在进程启动初期,减少大量的缺页中断
    • 例如:当重启一个换出进程时,由于所有的页都在磁盘上,每个页都必须通过缺页中断调入内存
  • 在引用前,调入进程的所有或一些需要的页面
    • 例如:对于采用工作集的系统,为每个进程保留一个位于其工作集内的页的列表
  • 如果预调入的页面没有被使用,则内存被浪费
  • 在有些情况下,预调页面可能具有优点。问题在于,采用预调页面的成本是否小于处理相应缺页错误的成本。通过预调页面而调进内存的许多页面也有可能没有使用

页面尺寸选择

  • 页面大小总是2的幂,通常是4KB-4MB
  • 页表大小——需要大的页
    • 对于给定的虚拟内存空间,降低页大小,也就是增加了页的数量,因此也增加了页表大小
    • 因为每个活动进程必须有自己的页表,所以期望大的页面
  • 碎片——需要小的页
    • 较小的页可能更好的利用内存,最小化内部碎片
  • I/O开销——需要大的页
    • I/O时间包括寻道、延迟和传输时间,尽管传输时间和传输量(即页的大小)成正比,需要小的页,但是寻道时间和延迟时间远远超过传输时间
  • 程序局部——需要小的页
    • 较小的页允许每个页更精确匹配程序局部,而采用较大的页不但传输所需要的,还会传输在页内的其他不需要使用的内容
    • 采用较小页面,会有更好的精度,以允许只隔离实际需要的内存
    • 采用较大页面,不仅必须分配并传输所需要的内容,而且还包括其他碰巧在页面内的且并不需要的内容
    • 较小的页面应导致更少的I/O和更少的总的分配内存
  • 缺页次数——需要大的页
    • 由于每个缺页会产生大量的额外开销,为了降低缺页次数,需要较大的页
  • 其他因素
    • 如页大小和调页设备的扇区大小的关系等
  • 没有最佳答案,总的来说,趋向更大的页

TLB范围

  • TLB范围——通过TLB所访问的内存量
  • TLB范围 = (TLB大小)×(页大小)
  • 理想情况下,一个进程的工作集应存放在TLB中,否则会有大量的缺页中断
    • 如果把TLB条数加倍,那么TLB的范围就加倍,但是对于某些使用大量内存的应用程序,这样做可能不足以存放工作集
  • 增加页的大小
    • 对于不需要大页的应用程序而言,这将导致碎片的增加
  • 提供多种页的大小
    • 这允许需要大页的应用程序有机会使用大页而不增加碎片的大
    • 要求操作系统(而不是硬件)来管理TLB
    • 通过软件而不是硬件来管理TLB会降低性能。然而,命中率和TLB范围的增加会提升性能
    • 最近的趋势倾向于由软件来管理TLB和由操作系统来支持不同大小的页面

反向页表

  • 反向页表降低了保存的物理内存
  • 不再包括进程逻辑地址空间的完整信息
  • 为了提供这种信息,进程必须保留一个外部页表
  • 外部页表可根据需要换进或换出内存

I/O互锁

  • 允许某些页在内存中被锁住
  • 为了防止I/O出错,有两种解决方案:
    • 不对用户内存进行I/O,即I/O只在系统内存和I/O设备间进行,数据在系统内存和用户内存间复制
    • 允许页所在内存,锁住的页不能被置换,即正在进程I/O的页面不允许被置换算法置换出内存,当I/O完成时,页被解锁

程序结构

  • 数据结构和程序结构可能影响系统性能
  • 其他因素(编译器、载入器、程序设计语言)对调页都有影响

posted @ 2020-11-24 20:30  方知有  阅读(1407)  评论(0编辑  收藏  举报