最近对操作系统的内存管理比较感兴趣,所以重学了一下内存管理(参考王道操作系统)
也可以直接看我的笔记:点击访问
内存的基本概念
存储单元的编址方式
- 按字节编址,每个存储单元大小为1B
- 按字编址,每个存储单元大小为1个字,具体字长看计算机
逻辑地址和物理地址
- 逻辑地址:又叫相对地址,用于表示程序在内存中的相对位置
- 物理地址:又叫绝对地址,即程序在内存中的真实地址
一个程序装入内存的步骤
- 源代码
编译
为目标代码 链接
(多个目标代码以及所需库函数需要链接在一起成为装入模块
,装入模块的地址一般从0开始),这时有了完整的逻辑
地址装入程序
将装入模块装入
内存,这时才有物理
地址
目标代码装入内存的3种方式(逻辑地址转换为物理地址的3种方式)
- 绝对装入:编译链接时就确定了物理地址,灵活性低,只适用于
单道程序环境
- 静态重定位:由装入程序负责逻辑地址到物理地址的转换
- 动态重定位:程序真正被执行时才进行地址转换,所以需要一个重定位寄存器,也叫基址寄存器。动态重定位的好处:
允许程序在内存中移动,允许程序分配到不连续的内存
多个目标模块链接的3种方式
- 静态链接:在程序运行之前链接成一个完整的可执行模块,具有完整的逻辑地址
- 装入时动态链接:一边装入内存,一边链接
- 运行时动态链接:程序执行过程中需要某个目标模块时,才进行链接
操作系统如何保证各个进程在各自存储空间内运行?(内存保护)
- 设置上、下限寄存器
- 设置基址寄存器、界地址寄存器,进行越界检查
内部碎片和外部碎片
- 内部碎片:分配给进程的内存空间中空闲的部分
- 外部碎片:整个内存中空闲的部分
覆盖和交换技术
- 覆盖和交换都用于解决
程序大小超过内存大小
的问题
覆盖
- 只存在于早期的OS,不用了解了
交换
- 在内存紧张时,将某些进程暂时换出到外存,将外存中具备运行条件的进程换入内存
- 中级调度,即内存调度
- 在具备交换功能的OS中,一般将磁盘分为
文件区和交换区
,文件区追求存储空间利用率,交换区追求交换速度,所以交换区的IO速度一般比文件区快 - 注:PCB不会被换出内存
内存空间的3种连续分配方式
- 连续分配:进程必须分配连续的存储空间
- 已经过时
单一连续分配
- 内存被分为系统区和用户区,只能有一个用户进程
- 优点:实现简单,无外部碎片
- 缺点:只能用于单用户、单进程的OS,有内部碎片
固定分区分配
- 在单一连续分配的基础上,将用户区分为多个分区(大小不一定相等),每个分区只能装入一个作业
- 这是最早的可运行多道程序的内存管理方式
- 需要维护一个分区说明表,每个表项包括:分区号、大小、起始地址、是否分配
- 优点:实现简单,无外部碎片
- 缺点:分区大小固定;有内部碎片
动态分区分配
- 不会预先划分分区,而是在进程装入内存时根据进程大小动态建立分区
- 同样需要维护一个
空闲
分区表/链,当进程装入、进程结束时要对其进行调整 - 当多个空闲分区都能满足需求时,如何选择,这涉及动态分区分配算法
- 优点:没有内部碎片
- 缺点:有外部碎片(可通过紧凑技术解决)
动态分区分配算法
- 用于在多个满足条件的空闲分区中选择一个分区来装入
首次适应算法(first fit)
- 空闲分区表按地址递增排列,选择第一个满足大小的分区
- 缺点:导致低地址部分产生较多小碎片
邻近适应算法(next fit)
- 和首次适应算法类似,但每次查找时从上次查找结束的位置开始,而不是从头开始,这样就避开了低地址部分的碎片
- 缺点:导致无论低地址高地址的空闲分区都有相同概率被使用,最后就没有较大的空闲分区了
最佳/小适应算法(best fit)
- 空闲分区表按容量递增排列,选择第一个满足大小的分区(也就是优先使用更小的空闲区)
- 缺点:会产生很多很小的外部碎片
最坏/大适应算法(worst fit)
- 与最佳适应算法相反,空闲分区表按容量递减排列
- 缺点:导致较大的空闲分区很快就用完,之后有大进程装入时,就没有可用分区了
内存空间的3种非连续分配方式
- 连续分配的缺点很明显:不管怎样都会产生内存碎片,且进程必须连续存储
基本分页存储管理
- 将内存分为大小相等的分区,每个分区只占很小的内存,如4KB,称为
页框
或页帧
或物理块
或内存块
,页框号从0
开始 - 同时,进程的地址空间也分为和页框大小相等的区域,称为
页
或页面
,页号也从0开始- 每个页的起始地址存在
页表
中,每个进程都会维护一个页表
- 每个页的起始地址存在
- 一个进程的页在内存中不需要连续存储
- CPU需要2次访问内存:查页表、访问最终物理地址
分页存储如何实现逻辑地址到物理地址的转换?
- 求逻辑地址对应的页号
- 从页表获取该页号对应页面在内存的起始地址
- 求逻辑地址在页面内的偏移量
- 物理地址 = 页面始址 + 页内偏移量
比如逻辑地址=80,页面长度=50,那么页号=80/50=1,偏移量=80%50=30,物理地址=30+第1页的起始地址
一般来说,页面大小为2的整数幂,这是为了方便计算机运算
- 逻辑地址的结构由
页号+页内偏移量
组成 - 如果一个页的大小为 2 K 2^K 2K个内存单元,那么就有K位表示偏移量
- 而一个进程最多允许有 2 M 2^M 2M个页面,对应M位页号
页表的组成
- 页表项由
页号+页框号
组成 - 通过页表计算某一页的物理起始地址=
对应页框号*页框大小
基本分页地址变换机构
页表寄存器PTR
由页表起始地址、页表长度
(即有多少个页表项)组成- 当进程未执行时,页表起始地址和页表长度放在
PCB
中,当进程被运行时,就会被加载到PTR - 下图是基本地址变换机构将逻辑地址转化为物理地址的过程:
具有快表的地址变换机构
- 局部性原理
- 时间局部性:某条指令或数据被访问后,不久之后,很可能会再次被访问
- 空间局部性:某个存储单元被访问后,不久之后,其附近的存储单元也很可能被访问
- 快表就利用了局部性原理
- 快表其实就是页表的一部分副本(存放最近访问过的页表项),快表属于
高速缓存
,命中快表后,CPU只需要1次内存访问(由于局部性原理,命中率可以达到90%以上);快表满了后需要根据页面置换算法
对旧的页表项进行替换
两级页表
- 单级页表存在的问题:
- 需要分配连续的页框来存放页表,如果页表很大,就会占用很多连续页框
- 整个页表需要常驻内存
- 解决第1个问题:
将页表项分组,每个分组大小刚好占用一个页框,分组可以离散存储。然后为这些分组建立一张页表
,称为页目录表
,或称外层页表
- 此时逻辑地址将由3部分组成:一级页号+二级页号+页内偏移量
- 解决第2个问题:
- 在需要访问某个页时才将其调入内存(
虚拟内存技术
) - 在页表项中增加一个标志,表示该项是否在内存中,如果访问的页不在内存,则产生
缺页中断
,然后将其调入内存
- 在需要访问某个页时才将其调入内存(
n级页表的缺点:需要n+1次访问内存(一级页表 + 二级页表 + ··· + 最终物理地址)
基本分段存储管理
- 分段,和分页差不多,将某个进程划分为多个
大小不等
的段,每个段可以离散存储 - 需要维护一个段表,段表项包括:
段号(隐含)、段长、段基址
GDTR
寄存器(48位),用于存储当前段表项:段上限(段长) + 段的起始地址,通过LGDT
指令给该寄存器赋值- 分段和单级页表一样,也需要2次访存:访问段表+最终物理地址
- 分段也可以使用
快表
提高访问速度 - 分段存储地址转换过程如下:
分页和分段的比较
- 分页的目的是提高内存利用率,并实现离散存储,页是一个物理单位,分页是系统行为,用户不可见
- 分段的目的是满足用户需求,一个段通常包含属于一个逻辑模块的信息,段是逻辑单位,对用户是可见的
- 分段更容易实现
信息共享、保护
- 分段会产生外部碎片
段页式存储管理
- 段页式存储综合了分页和分段的优点
- 段页式存储的逻辑地址结构:
段号 + 页号 + 页内偏移量
- 段号位数决定了每个进程最多可以分几个段
- 页号位数决定了每个段最多有多少个页
- 偏移量位数决定了页的大小
- 每个进程都要维护一个段表,段表项包含:
段号(隐含)、页表长度(有多少个页)、页表起始地址
- 然后页表项包含:页号、页框号
- 也就是说,一个进程要维护
一个段表 + 多个页表
- CPU需要3次访存:访问段表、访问页表、最终物理地址
- 段页式存储同样可以引入快表
- 段页式存储的地址转换过程:
虚拟内存技术
虚拟内存基本概念
- 当进程开始运行时,先将一部分程序装入内存,另一部分暂时留在外存;当要执行的指令不在内存时,由系统自动完成将它们
调入
内存的工作;当没有足够的内存时,系统自动选择部分内存(暂不执行的程序)空间,将其中原有的内容交换到磁盘上,并释放这些内存空间供其他进程使用 - 这样做的结果使程序的运行丝毫不受影响,使程序在运行中感觉到拥有一个不受内存容量约束的、虚拟的、能够满足自己需求的存储器
- 虚拟内存的最大容量:由
计算机的地址结构(CPU寻址范围)
决定,而实际容量为min(内外存总容量,CPU寻址范围)
- 比如某计算机地址结构为32位,按字节编址,内存=512MB,外存=2GB,那么最大容量= 2 32 2^{32} 232B=4GB,实际容量=min(4GB,512MB+2GB)
- 虚拟内存技术是建立在
非连续分配存储
的基础上的,主要区别在于:- 增加了请求调页(调段)功能(用于缺页时换入)
- 增加了页置换(段置换)功能(用于内存不够时换出)
- 因此,相应的,虚拟内存技术分为3种实现:
- 请求分页存储管理
- 请求分段存储管理
- 请求段页式存储管理
虚拟内存的页表机制
- 虚拟内存中的页表称为
请求页表
,页表项包括:- 页框号
状态位
:是否已经调入内存访问字段
:最近被访问过几次、上次访问时间(供置换算法换出页面时参考)修改位
:该页调入内存后是否被修改(未修改则不需要写回外存)外存地址
缺页中断机构的原理
- 在请求分页系统中,每当所要访问的页面不在内存时,便产生一个
缺页中断
,OS调用缺页中断处理程序,此时缺页的进程阻塞,进入阻塞队列,调页完成后再唤醒,进入就绪队列 - 若内存中有空闲块,则分配一个块,将所缺页装入该块,并修改页表中相应页表项
- 若此时内存中没有空闲块,则页面置换算法选择某个页面淘汰,若被淘汰页在内存期间被修改过,则要将其写回外存
- 内中断和外中断的区别
- 内中断:中断信号源于CPU内部,如缺页中断、系统调用
- 外中断:中断信号源于CPU外部,如IO中断、键盘中断
虚拟内存页面置换算法
- 终于来到页面置换算法了😄
- 页面换入换出是IO操作,好的置换算法
应该追求更少的缺页率
最佳置换算法(OPT)——简称:不可能的算法
- 最佳 (Optimal, OPT) 置换算法所选择的被淘汰页面将是
以后永不使用的
,或者是在最长时间内不再被访问的
页面,这样可以保证获得最低的缺页率 - 但由于目前
无法预知哪个是未来最长时间内不再被访问的页面
,因而该算法无法实现
- 但最佳置换算法可以用来评价其他算法
先进先出置换算法(FIFO)
- 优先淘汰最早进入内存的页面,亦即在内存中驻留时间最久的页面。该算法实现简单,只需把调入内存的页面根据先后次序链接成队列,设置一个指针总指向最早的页面。
- FIFO算法会产生
当进程所分配的物理块数增大而缺页率不减反增
的异常现象,这是由 Belady 于1969年发现,故称为Belady 异常
。只有 FIFO 算法可能出现 Belady 异常
,而 LRU 和 OPT 算法永远不会出现 Belady 异常
最近最久未使用置换算法(LRU)
- 选择最近最长时间未访问过的页面予以淘汰,它认为过去一段时间内未访问过的页面,在最近的将来可能也不会被访问
- OS会参考请求页表项中的访问字段
- LRU 性能较好,但需要寄存器和栈的硬件支持。LRU 是堆栈类的算法。理论上可以证明, 堆栈类算法不可能出现 Belady 异常。FIFO 算法基于队列实现 ,不是堆栈类算法。
时钟置换算法(CLOCK)
- 该算法将页表项链接成一个
循环队列
,循环检查各页面的情况,像时钟的针一样转动,所以叫CLOCK算法,又称为最近未用(Not Recently Used, NRU)算法 - 步骤:
循环扫描,第一轮淘汰访问位=0的页,若扫描过的页的访问位=1,将其置为0,暂不换出。若未找到,则进行第二轮
- 最多2轮扫描
改进型的时钟置换算法
- 增加了修改位,用
(访问位,修改位)
来表示页面状态 - 这是因为要优先淘汰没有访问过且没有修改过的页,这样就不用IO写出外存
- 该算法最多扫描4轮,步骤如下:
页面和页框的补充知识
驻留集
- 驻留集就是OS分配给一个进程的页框的集合
- 驻留集一般小于进程总大小
- 驻留集太小,会导致缺页频繁,太大,会导致多道程序并发度下降
- 一般来说,驻留集大小不能小于工作集大小,否则会频繁缺页
工作集
- 指在某个窗口尺寸内,进程实际访问页面的集合
- 工作集大小一般小于窗口大小
页框分配策略、页面置换策略
- 固定分配:驻留集大小不变
- 可变分配:驻留集大小可变
- 局部置换:进程缺页时只能选自己的驻留集中的空闲页框置换
- 全局置换:进程缺页时可以选其他进程的空闲页框置换
- 不存在固定分配+全局置换的组合,其他组合都是可以的
- 可变分配+局部置换的综合性能更好
调页策略
- 预调页策略:进程首次运行前调入一些相邻的页
- 请求调页策略:即运行时缺某个页才调页
抖动现象
- 抖动是指某些页面频繁的换入换出
- 主要原因:分配给进程的物理块(页框) < 频繁访问的页数