操作系统-第九章-虚拟内存管理
背景
- 代码必须装入内存才能执行,但是并不是所有代码必须全部装入内存
- 错误代码
- 不常用的函数
- 大的数据结构
- 局部性原理:一个程序只要部分装入内存就可以运行
- 整个程序不是同一时间都要运行
- 程序部分装入技术优点:
- 进程大小不再受到物理内存大小限制,用户可以在一个虚拟的地址空间编程,简化了编程工作量
- 每个进程需要的内存更小,因此更多进程可以并发运行,提供了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的幂容易产生碎片
- 调整到下一个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完成时,页被解锁
程序结构
- 数据结构和程序结构可能影响系统性能
- 其他因素(编译器、载入器、程序设计语言)对调页都有影响
作者:方知有
Motto: 热爱可抵岁月漫长
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。
所有评论和私信都会在第一时间处理,如有问题或建议,请多多赐教!
支持博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是作者坚持原创和持续写作的最大动力!