持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情
注意:下面标星*的表示会在以后补充
Virtual Memory
将用户逻辑内存与物理内存分离,因为只有部分程序需要在内存中才能执行。因此,逻辑地址空间可以比物理地址空间大得多。
实现方法:Paging、Segmentation
所需支持
- 硬件支持:
- 内存管理单元 Memory Management Unit(MMU)
- 转换检测缓冲区 Translation Lookaside Buffer(TLB)
- 操作系统支持:
- 用于控制MMU和TLB的虚拟内存系统。
虚拟地址:
- 虚拟地址是进程用来访问自己的地址空间的内存地址。
- 当进程访问虚拟地址时,MMU硬件将虚拟地址转换为物理地址
- 操作系统确定从虚拟地址到物理地址的映射。
MMU 和 TLB:
- MMU
- 将虚拟地址转换为物理地址的硬件。
- 每个内存引用都通过MMU传递。
- TLB
- MMU虚拟到物理地址转换的缓存
- 这只是一个用来加速的优化,但很重要!
Paging
物理地址空间被分为大小相同的page frame(页框),与页大小通常相同
操作系统职责
- 维护页表。
- 页表是从页转换为帧的数组。
- 从空闲帧中分配足够的页面以执行程序。
- 要运行大小为n页的程序,需要找到n个可用页框并加载程序。
- 操作系统跟踪所有可用帧。
- 设置页表以将逻辑地址转换为物理地址
优势:
- 允许进程的物理地址空间不连续,并根据需要进行分配。
- 易于分配物理内存。
- 易于“分页”/交换内存块。
问题:内部碎片 Internal Fragmentation
地址转换策略
对于逻辑地址空间和页大小
通过页码去页表当中找映射如图,将虚拟地址映射为物理地址(虚拟页码VPN变物理页码PPN)
页表
OS管理,每个进程一个
Structure of Page Table Entry(PTE)页表项结构
- 页框编号:映射框编号
- 存在/不存在位:1/0表示有效/无效条目
- 保护位:允许什么类型的访问,0表示读/写,1表示只读
- 修改(脏位):在修改和写入磁盘时设置
- 引用:设置页面被引用的时间(帮助决定退出哪个页面)
- 缓存已禁用(禁止位):保证不断从设备读取数据而不是缓存中的副本
问题和改进方案:
- 快速访问问题:
- 通常每个指令需要进行2个或更多页表引用,因此快速访问是必要的
- 解决:TLB translation look-aside buffer
- 大内存页表占用问题:
- 由于内部碎片,每个进程的平均浪费为½页,因此需要较小的页面大小。
- 但是,小页面->多页面->大页面表->页面表(对于每个进程)将占用大量内存空间
- 解决:多级页表 Multi-level Page Tables、倒排页表Inverted Page Tables
TLB translation look-aside buffer
通过在转换查找缓冲区(TLB)中缓存最近的映射来消除在页表中遍历查找映射的过程(相当于是映射的Cache),加快映射速度
- 大多数程序经常引用少量页面
- TLB是MMU的一部分。
- 每个TLB条目都是一个页表条目(具有与PTE(page table entry 页表项)相同的元素)。
- TLB是一种特殊的硬件(称为关联存储器)
针对大内存的页表
多级页表 Multi-level Page Tables
例子:二级页表逻辑地址结构:
通过p1查找一级页表中的映射,从而映射到二级页表
然后再通过p2查找该二级页表中的映射,从而映射到物理页框
倒排页表 Inverted Page Table
64位地址空间需要多级页表级数过多,内存访问次数过多,因此需要改进。
在物理内存当中,每个页框有一个表项,而不是每个虚拟页一个,这些表项构成倒排页表,再使用TLB加速
线性倒排页表:
hashed倒排页表:
- 哈希表的输入是PID和VPN(虚拟页码),输出是反向页表的索引。
- 使用哈希表进行哈希时,可能会发生冲突,可以使用链地址方法解决。
- 在反向页面表项中添加下一个字段以形成链表(标题的索引在哈希表中)
比较进程ID和虚拟页码 如果匹配,则找到。
如果不匹配,请在下一个指针中检查另一个页表条目,然后再次检查。
页面替换算法
发生缺页中断(page fault)往往需要替换页表中的已有表项
页面替换步骤:
- 1、查找页面在磁盘上的位置。
- 2、查找空闲页框。
- 1) 如果有空闲页框,请使用它。
- 2) 否则,请使用页面替换算法选择victim帧。
- 3) 将所选页面写入磁盘并更新任何必要的表。
- 3、从磁盘读取请求的页面。
- 4、重启用户进程。
如何选择victim frame? 希望能替换一个不太有用的页面。 脏页需要write,干净页不需要。 硬件的每个页框都有一个脏位,指示此页面是否已更新。 问题:如何确定效用? 启发式Heuristic:存在时间局部性 删除不太可能再次使用的页面
目标:
- 实现较低的页面错误率(page fault rate)。(主要目标)
- 确保大量使用的页面保留在内存中。
- 替换一段时间内不需要的页面。
- 减少page fault的延迟。
- 高效的代码。
- 替换不需要写出的页面。
The Optimal Algorithm (OPT or MIN) 最优页面替换
选择到下一个引用的时间最长的页面进行替换
其中ABC为引用字符串Reference String,代表被引用的页面序列
但是因为不可能准确预测下一引用的时间,所以是理想状况
First-in First-Out (FIFO) Algorithm
替换主存中存在时间最长的页(队头),新的页在队尾
Belady anomaly(异常):页面帧越多,页面错误不一定就越少。
为什么FIFO可能是好的? 可能很久以前分配的页面不再使用了。 为什么FIFO可能不那么好? 不考虑引用的位置
遭受Belady异常:随着物理内存大小的增加,应用程序的性能可能会变得更差
Second Chance Algorithm
FIFO的变体,寻找最老的未被访问的页面进行替换
发生缺页中断,检查最老页面的R(reference)位:
- 如果R=0,既老又未被使用,替换掉
- 如果R=1,把R清0放入链表尾端,重新设置该页的“装入时间”,继续检查队头,直到遇到页面的R=0
Clock Algorithm
Second Chance Algorithm的更好的实现:
页面保存在环形链表(提高效率),表针指向最老页面
发生缺页中断,先检查指向的最老页面,如果R=0,替换掉,R=1,把该页的R置0并且移动表针到“第二老”
NRU(Not-Recently-Used)最近未使用
增强second chance,除了R位代表是否被引用,加入一个M(Modified)位代表页是否被修改
发生计时器中断时,定期清除R位(但清除不了M位)
分成4类页,状态转移如下:
算法:优先找第0类中最老的,如果页表中没有页属于第0类则找第1类,以此类推
优点:易于理解和实施, 性能足够(但不是最佳)。
LRU Least Recently Used 最近最少使用
替换最长时间未使用的页面,区别于NRU的“未使用”,并不区分R和M
优点:效率比较高,接近OPT。 不会遭受Belady的异常 缺点:实现需要大量硬件,代价高(链表实现、计数器实现)
*LRU实现
NFU Not Frequently Used 最少使用(最不常用)
类似LRU,不同点是没了“最近”,且记录了次数。
- 每次时钟中断扫描所有页面,如果R=1就在计数器当中加1,然后清除R;
- 缺页中断时删除计数器值最低(访问次数最少)的页面
问题:可能很久以前某个页被大量使用,但最近不怎么用,不如LRU
Aging Algorithm 老化算法
修饰NFU,在软件上模拟LRU,不同点是这次的粒度granularity是时钟滴答(时钟中断),而不是内存引用次数
实现:
- 在时钟中断时:
- 将计数器右移(除以2)
- 将R位添加到左侧(MSB)
- 发生缺页中断时,删除计数器最小的(比如一个页面4次时钟中断都未访问,则前4位为0)
问题:老化算法计数器只有有限位,限制了对以往页面信息记录
Working Set Algorithm
Fetch Policy
什么时候应该将页面加载到主存?
- 请求分页(demand-paging)策略
- 页面按需加载,而不是提前加载。
- 进程启动时,内存中没有加载任何页面;页面错误会将页面加载到内存中。
- 预分页(pre-paging)策略
- 在进程运行之前加载页面到工作集页面。
- 需要工作集,即进程正在使用(当前需要)的页面集合中的页面来加载上下文切换。
- 很少有系统使用纯按需分页,而是使用预分页
颠簸(Thrashing)
- 如果一个进程每执行几条指令就发生一次缺页中断,就称这个程序发生颠簸
- 原因:内存太小(页框太少)无法容纳工作集,产生大量缺页中断
- 后果:如果进程没有“足够”的页面,则页面错误率非常高。这导致:
- CPU利用率低。
- OS认为需要提高多道程序设计的程度。
- 将向系统中添加另一个进程
- 解决方案:
- 工作集:根据需要提供尽可能多的frame
- 页面故障频率 Page Fault Frequency
工作集 Working Set
如果进程的工作集无法保存在内存中,请不要运行该进程,否则将发生大量缺页中断
工作集是任意时刻t,最近k次内存访问所访问过的页面集。
- 函数w(k,t)是时间t时工作集的大小。
- w(k,t)是k的单调非减函数。
- w(k,t)的极限是有限的
近似工作集:过去τ秒内使用的进程页面集。
工作集替换算法
基本思想:出现页面错误时,替换不在工作集中的页面。
- 近似值:放弃倒数k个内存引用的想法,使用执行时间。
- 当前虚拟时间:进程自启动以来实际使用的CPU时间量称为其当前虚拟时间。
- 进程的近似工作集是它在过去的τ秒虚拟时间内引用的页面集。
实现:
- 在每个时钟滴答中,清除所有R位并记录进程虚拟时间t。
- 出现缺页中断,查找逐出候选项时,请扫描物理内存中进程的所有页面:
- 如果R==1,则将t存储在PTE的LTU(最近使用时间)中,并清除R。
- 如果R==0:
- 如果(t–LTU)>τ,则逐出页面(因为它不在工作集中)
- 如果(t–LTU)<=τ,则记录最大年龄的页面。年龄=当前虚拟时间–上次使用的时间(即age=t-LRU)
- 如果扫描整个表时没有找到要逐出的候选对象
- 如果找到一个或多个R=0的页面,则会逐出最长时间(age最大)的页面。
- 最坏的情况是:没有R=0的页面,随机选择一个页面(最好是干净的页面)。
WS Clock Algorithm\
基于时钟算法的改进工作集算法,使用了M位
以前的WS算法要求在每个页面错误时扫描整个页面表。
改进WSClock算法:
- 所有页面都保存在环形队列中。
- 随着页面的添加,它们进入到环中。
- “时钟指针”绕着环前进。
- 每个条目包含R、M、上次使用时间(LRU)
实现: 出现缺页中断时
- 如果R=1,则设置R=0并更新“上次使用时间”字段,继续寻找。
- 如果R=0
- if age<=τ,则向前移动指针并继续寻找。
- if age>τ
- if clean,则清除。
- 如果脏,则计划磁盘写入(因为该页面被修改过,在磁盘上没有有效副本,避免调度写磁盘操作引起的进程切换),继续寻找。
- 如果指针一直绕到起点
- 案例1:至少安排了一次写入,遇到的第一个干净页面被逐出。
- 案例2:未计划写入(所有页面都在工作集中)
- 清除任何干净的页面并使用它。
- 如果不存在干净的页面,则选择当前页面并将其写回磁盘。
小结
最好的实践中可使用的两种是老化和工作集时钟
分页系统设计问题
Resident Set Management 驻留集管理
操作系统必须决定分配给每个进程的帧数:
- 分配较小的数字允许同时在内存中执行多个进程。
- 帧太少会导致页面错误率高。
- 太多的帧/进程导致多道程序设计级别低。
- 超过某个最低级别后,添加更多帧对页面错误率几乎没有影响。
驻留集:主内存中的一组进程页。
驻留集管理 替换范围 局部替换 全局替换 驻留集大小 固定分配Fixed Allocation Policy 可变分配
Replacement Scope
发生页面错误时要考虑替换的帧集:
- 局部替换
- 仅在分配给出现故障的进程的帧中进行选择。
- 全局替换
- 内存中任何未锁定的帧都可以替换
- 高优先级进程可能能够在自己的帧或低优先级进程的帧中选择帧
优劣对比:
- 局部替换
- 可能会不必要地减慢进程,因为使用较少的内存无法替换。
- 全局替换
- 进程无法控制自己的页面错误率–还取决于其他进程的分页行为(不稳定的执行时间)。
- 通常会导致更大的吞吐量。
- 但这是更常用的方法。
Resident Set Size
为进程分配的帧的数量的策略
固定分配策略:根据某些条件,在加载时为进程分配固定数量的帧:
- 平等分配 Equal allocation
- 例如,有12416个可用页面帧和10个进程,每个进程得到1241个帧,其余6个进入池中,在出现页面错误时使用。
- 按比例分配 Proportional allocation
- 例如:一个300-KB的进程得到的分配是一个10-KB进程的30倍。
- 优先级分配 Priority allocation
- 使用流程优先级而非规模的比例分配方案。
可变分配策略: 一个进程的帧数可能随时间而变化。
- 如果页面错误率较高,则增加。
- 如果页面错误率非常低,则降低。
- 需要操作系统开销来评估活动进程的行为。
PFF Page Fault Frequency Algorithm
- 定义页面错误率的上限U和下限L。
- 如果故障率高于U,则为进程分配更多帧。
- 如果故障率<L,则分配更少的帧。
- 常驻集大小应接近工作集大小W。
- 如果PFF>U且没有更多可用帧,则暂停进程。添加帧减少帧抖动
*负载控制
Page Size 页大小
Small Page Size
- 优势
- 更少的内部碎片,即更好的内存利用率
- 对于局部引用来说更好,便于捕获未知
- 缺点
- 较大的页表
- 更多页面错误
- 读/写页的开销更大
- I/O 延迟高,吞吐量小
Big Page Size的优缺点相反
页表和内部碎片导致损失的开销(Overhead):
s=平均进程大小(字节)
p=页面大小(字节)
e=页表项大小
当时,开销最小
*分离的指令空间和数据空间
解决地址空间太小的问题,分为I地址空间和D地址空间
Shared Pages 共享页面
共享代码:可以共享只读页面
- 指令和数据空间分开,易于共享代码。
- 示例:两个进程通过使用相同的I-space页表和不同的D-space页表共享相同的程序。
共享数据:
- 在UNIX中,在fork系统调用之后,父级和子级需要共享程序文本和数据。
- fork时不复制页面。
- 为父进程和子进程提供自己的页表。
- 让两个进程指向同一组页面。
- 共享数据页必须为只读。
Copy on write写入时复制(COW): 如果进程写入物理内存中的frame
- trap到内核。
- 内核分配新frame。
- 复制旧frame。
- 重新启动写入指令。
*Shared Libraries 共享库
*Implementation Issues for Paging
Segmentation
一维地址空间问题:
- page挨在一起时,扩缩容复杂
- 分页是一种“平面”内存模型,这意味着程序员可以看到从地址0开始到某个最大地址的地址。
- 对于某些应用程序,允许在单个进程中使用完全不同的地址空间可能很有用
为机器提供许多完全独立的地址空间,称为Segment。
特点:
- 段通常不同时包含不同类型的内容
- 每个段的长度可以在大小上有所不同,从0到允许的最大值(线性地址序列)。
- 段长度在执行过程中可能会发生变化,但不会相互影响。
- 段是一个逻辑实体,程序员知道并将其作为单个逻辑实体使用。
地址转换
虚拟段编号(s) 偏移量(d)
- 段表(由操作系统维护)
- 映射二维物理地址。
- 虚拟段编号用作段表的索引。
- 每个表条目都有:
- base–包含段驻留在内存中的起始物理地址
- limit–指定段的长度
实现:
- 当CPU生成逻辑地址时,该地址被发送到MMU。MMU使用逻辑地址的段号作为段表的索引。
- 将偏移量与段限制limit进行比较,如果偏移量更大,则会生成无效地址错误。
- 否则,偏移量将加到段base以形成物理地址。
示例:
- 使用以下段表,计算由段和偏移组成的逻辑地址的物理地址,如下所示:
- 段2和偏移247,段4和偏移439
- 段=2,偏移=247: 从段表中,段2的limit=780,base=2200,由于偏移量247小于限制780, 物理地址=偏移量+段基=247+2200=2447
- 段=4,偏移=439:从段表中,段4的限值=400,段底值=1650, 由于偏移量大于限制,因此会生成无效地址错误。
优点:
- 可以收缩/增长段,大小浮动的段很容易提供
- 可以为每个段提供自己的保护信息,这是一种比试图保护内存的每一页容易得多的保护方案。
- 链接程序是一项微不足道的任务。
- 代码可以在进程之间轻松共享。只需加载一次代码段。同一程序的副本访问同一段
缺点:
- 程序员必须知道正在使用的内存模型(在汇编级别)。
- 就像交换系统一样,碎片可能会浪费大量内存。
- 段可能太大,无法放入物理内存。
Segmentation with Paging
- 将虚拟地址空间视为任意大小的段(逻辑单元)的集合。
- 将物理内存视为一系列固定大小的页帧。
- 段通常大于页框,⇒ 通过分页将逻辑段映射到多个页面框架上。
- 物理内存只包含一个段的所需页面,而不包含整个段。
进程、段、页结构:
- 每个进程都需要一个段表。
- 段被划分为若干页,为了跟踪这些页面,将为每个段维护一个页表。
- 段表中的每个条目都指向该段的页表。
地址转换策略
虚拟地址划分:
- 段编号:用于索引段表,其表项给出该段的页表的起始地址。
- 页编号:用于索引该页表以获取相应的frame编号。
- 偏移量:用于在frame内定位字段
段表项 Segment Table Entry
页表项 Page Table Entry
映射(虚拟地址到物理地址转换)策略:
- 段号索引到段表中,该段表生成该段的页表的base address。
- 对照段的limit检查地址的其余部分(页码和偏移量)。
- 使用页码索引页表得到页框位置
- 加上偏移量以获取Word(字)的物理地址。