内存空间的分配与回收
连续分配方式
连续分配方式——要求进程占用的必须时一整段的连续的内存区域
1. 单一连续分配:
- 内存会分为系统区和用户区。系统区通常位于低地址部分,用于存放操作系统相关数据;用户区用于存放用户进程相关数据
- 内存中只能有一道用户程序,用户程序独占整个用户空间
- 优点:实现简单,⭐无外部碎片;可以采用覆盖技术扩充内存,不一定需要采用内存保护(因为只有一道程序)
- 缺点:只能用于单用户、单任务的操作系统中,有内部碎片(分配给某进程的内存区域中,有些部分并没有用上);存储器利用率低
2. 固定分区分配:
- 为了能装入多道程序,且这些程序不会互相干扰,于是将用户空间划分为若干个固定大小的分区,在每个分区中只装入一道作业
- 分类:
- 分区大小相等: 缺乏灵活性,但是很适合用于同一台计算机控制多个相同对象的场合(比如钢铁厂有n个炼钢炉,把内存分为n个分区存放n个炼钢炉控制程序)
- 分区大小不等: 增加灵活性,可以满足不同大小的进程需求
- 操作系统需要建立一个数据结构——分区说明表,来实现各个分区的分配与回收。每个表项对应一个分区,通常按分区大小排列。每个表项对应分区的大小、起始地址、状态(是否已分配)
- 优点:实现简单,⭐无外部碎片
- 缺点:
- 当用户程序太大时,可能所有的分区都不能满足要求,此时不得不采用覆盖技术来解决,这样会降低性能
- 会产生内部碎片,内存利用率低
3.⭐动态分区分配:
-
1. 原理
不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的 -
2. 思考
- 问题一:系统需要什么样的数据结构来记录内存中的使用情况?
两种常用的数据结构:- 空闲分区表:每个空闲分区对应一个表项。表项包含分区号、分区大小、分区起始地址等信息 (注意:各表项的顺序不一定按照地址递增的顺序排列)
- 空闲分区链:每个分区的起始部分和末尾部分分别设置前向指针和后向指针。起始部分处还可记录分区大小等信息
- 问题二:当有很多空闲分区都能满足需求时,应该选择哪个分区进行分配?
- 把一个新作业装入内存时,需按照动态分区分配算法,从空闲分区表中选出一个分区分配个该作业
- ⭐动态分区分配算法:
- 首次适应算法:
- 算法思想:每次都从低地址开始查找,找到第一个能够满足大小的空闲分区
- 实现:空闲分区按地址递增的次序排列,每次分配内存时顺序查找空闲分区表(链)
- 综合看 性能最好 ,算法开销小,且回收分区后不需要对空闲分区进行重新排列(因为若按照容量排序,分配或回收完需要对新的空闲表排序)
- 最佳适应算法:
- 算法思想:由于动态分区分配是一种连续分配方式(为各个进程分配的空间必须是连续的一整片区域),因此为了保证大进程到来时,有足够大的连续的空间,可以尽可能的留下大片的空闲区,优先使用更小的空闲区
- 实现:空闲分区按容量递增的次序排列,每次分配内存时顺序查找空闲分区表(链),分配完成后还需要对新的空闲分区表(链)按容量递增排序(导致算法开销大)
- 优点:会有更多的大分区被保留下来,满足大进程
- 缺点: 每次优先放入小的空闲分区,会留下越来越多的很小的内存块(如:往5MB空间里放入4MB进程,往4MB空间里放入3MB进程),产生很多很小的外部碎片,无法利用
- 最坏适应算法:
- 算法思想:为了解决最佳算法产生大量外部小碎片的问题,可以尽可能的留下小片的空闲区,优先使用更大的空闲区,这样剩余的外部碎片就不会太小
- 实现:空闲分区按容量递减的次序排列,每次分配内存时顺序查找空闲分区表(链),分配完成后还需要对新的空闲分区表(链)按容量递减排序(导致算法开销大)
- 优点:可以减少难以利用的小外部碎片
- 缺点: 导致较大的空闲区很快被用完,如果之后有大进程到达,就没有内存分区可用了
- 邻近适应算法:
- 算法思想:由首次适应算法演变而来,每次从上次查找结束的位置开始查找
- 实现:空闲分区按地址递增的次序排列(同首次适应,算法开销小),每次从上次查找结束的位置开始查找
- 优点:不用每次都从低地址的小分区开始查找,算法开销小
- 缺点:会使高地址的大分区,更容易被使用完,不利于大进程
- 首次适应算法:
- 问题三:如何进行分区的分配与回收?
- 分配情况:以空闲分区表为例:
- 情况一:
- 情况二:
- 回收情况:以空闲分区表为例:
- 情况一:回收区前有一个空闲分区,将两个空闲分区合并
- 情况二:回收区后有一个空闲分区,将两个空闲分区合并
- 情况三:回收区前后各有一个空闲分区,将三个空闲分区合并
- 情况四:新增一个表项
- 分配情况:以空闲分区表为例:
- 问题一:系统需要什么样的数据结构来记录内存中的使用情况?
-
3. 性能
⭐没有内部碎片,但是有外部碎片内部碎片:分配给某进程的内存区域中,如果有些部分没有用上
外部碎片:是指内存中的某些空闲分区由于太小而难以利用
如果需要新装入一个20MB的新进程,而内存中空闲区域的大小总和也是20MB,但是这空闲的20MB是离散的碎片(外部碎片),不是一整块连续的内存空间,因此这些“碎片”并不能满足进程的要求
可以通过 紧凑 技术来解决外部碎片,移动已分配的分区,间接地将零散的空闲空间挪到一起
非连续分配方式
思考:连续分配方式的缺点:
- 固定分区分配:灵活性差,会产生大量的内部碎片,内存的利用率极低
- 动态分区分配:会产生很多外部碎片,虽然可以用紧凑技术来解决,但紧凑技术的时间代价很高
如果允许将一个进程分散地装入到许多不相邻的分区中,便可充分利用内存
1. 基本分页存储管理
-
1. 基本思想
把内存分为一个个相等的小分区,再按照分区大小把进程拆分成一个个小部分
-
2. 基本概念
- 将 内存空间 分为一个个大小相等的分区(如:每个分区4KB),每个分区就是一个 页框(或页帧、内存块、物理快),每个页框有一个编号即,即页框号,页框号从0开始
- 将 用户进程的地址空间 也分为一页框大小相等的一个个区域,称为页或页面,每个页面也有一个编号,即页号,也是从0开始
- 注意:进程最后一个页面可能没有一个页框那么大,因此页框不能太大,否则容易产生过大的内部碎片
- 操作系统以页框为单位为各个进程分配内存空间,进程的每一个页面放到一个页框中,也就是页面与页框一一对应
- 各个页面不必连续存放,也不必按先后顺序存放,可以放到不相邻的页框中
-
3. ⭐如何实现由逻辑地址到物理地址的转换?
利用动态重定位方式:
思想:目标内存单元的物理地址 = 模块(目标单元所属页面)在内存中的 “起始地址”(100)+ 目标内存单元相对于所属模块的起始地址的 “偏移量”(80)
- CPU执行指令1,需要访问逻辑地址为80的内存单元,如何确定该内存单元的物理地址?
- 分析:每页大小为50B,逻辑地址为80的内存单元应该在1号页面中,而一号页面(模块)在内存中的起始地址为450,逻辑地址80的内存单元,在一号页面内相对于一号页面起始地址的页内偏移量为30,故实际物理地址为450+30=480
- ⭐计算步骤:
- 算出目标内存单元所属页面的编号:
页号 = 逻辑地址 / 页面长度 (取模) - 获得所属页面在内存中的起始物理地址
根据页号找到由操作系统用某种数据结构保存的进程各个页面在内存中的起始地址 - 算出目标内存单元逻辑地址在页面内的偏移量
页内偏移量 = 逻辑长度 % 页面长度 (取余) - 物理地址 = 起始地址 + 页内偏移量
- 算出目标内存单元所属页面的编号:
-
4. ⭐计算机实现地址转换
- 假设用32个二进制位表示逻辑地址,页面大小为210 B = 1024B = 1KB (为了方便计算页号、页内偏移量,页面大小一般要为2的整数幂
- 0号页的逻辑地址空间应该是0~1023,用二进制表示为:
0000 0000 0000 0000 0000 0000 0000 0000 ~ 0000 0000 0000 0000 0000 0011 1111 1111
1号页的逻辑地址空间应该是1024~2047,用二进制表示为:
0000 0000 0000 0000 0000 0100 0000 0000 ~ 0000 0000 0000 0000 0000 0111 1111 1111 - 结论:如果页面大小为2k B,用二进制表示逻辑地址,则 末尾k位为页内偏移量,其余部分就是页号
- 分页式存储管理的逻辑地址结构如下:
地址结构包含两部分:前一部分为页号P,后一部分为页内偏移量W,在上图所示的例子中,地址长度为32位,其中0 ~ 11位为页内偏移量、12 ~ 31位为页号, - 如果由 k 位表示页内偏移量,则说明系统中一个页面大小为 2k 个内存单元
如果由 m 位表示页号,则说明系统中最多允许有 2m 个页面
-
5. 页表
- 为了能知道进程的每一页面在内存中存放的位置,操作系统要为每个进程建立一张页表
- 结构:
- 一个进程对应一张页表
- 进程的每一页对应一个页表项
- 每个页表项由页号和块号组成
- 页表记录进程页面和实际存放的内存块之间的对应关系
- M号内存块的起始地址就是 M*内存块大小
- 每个页表项的长度是相同的,但页号是隐藏的
例如:假设某系统物理内存为4GB,页面大小为4KB,则每个页表项至少应该为多少字节?
4GB = 232 B ,4KB = 212 B
因此4GB的内存总共会被分成 232/ 212 = 220 个内存块,因此内存块号的范围应该是 0 ~ 220 -1
因此至少需要20位二进制位才能表示这么多块号,又1 B(字节)= 8 个二进制位
故块号至少要3个字节表示
各个页表项会按顺序连续地存放在内存中
如果该页表的起始地址在内存中位X,则M号页对应的页表项一定存放在内存地址为 X + 3 * M的地址中
因此只需要知道页表的存放起始位置和页表项的长度,就可以得到已知页号的所需页表所对应块号
故页表项中不需要再存储页号,只要按顺序存储块号就可,所以本例中页表若有n个页面,则其大小为 3 * n B
小拓展:但在实际情况中,若页面大小为4KB,且取3B大小的页表项。这样一个页表内并不能全部由页表项塞满,会有内部碎片,故有时会适当的对页表项大小进行扩充,如扩充成4B,这样就可以被4KB整除,即将一个页面用页表项塞满,装下整数个页表项,方便页表的查询
-
6. ⭐基地址变换机构
- 原理:
- 通常系统中设置一个页表寄存器,存放页表在内存中的起始地址F 和页表长度M
- 进程未执行时,页表的起始地址和页表长度放在进程控制块(PCB)中,当进程被调度时,操作系统会将它放到页表寄存器当中
- 进程切换相关的内核程序负责恢复进程的运行环境,将PCB中的信息放到页表寄存器中,页表寄存器中存放着页表的起始地址F 和页表长度M(这里页表长度就是页表项的数量)
- 同时程序计数器PC也是需要恢复的,它指向下一条要执行的指令的逻辑地址A
- 首先根据逻辑地址A,将页号与页内偏移量切分出来
- 越界中断:判断切分出来的页号P 是否 ≥ 页表长度M(是否合法),如果在此范围,说明该页号不合法
- 如果页号是合法的,则根据页号、页表的起始地址和页表项的长度,查询出需要的页号在页表中对应的内存块号
- 在通过内存块号和页内偏移量W 得到最终的内存地址
- 计算步骤:
注:页面大小是2的整数幂,设页面大小为L,由逻辑地址A 到实际物理地址E 的过程如下- 计算页号P 和 页内偏移量 W。(①十进制情况下:P=A/L、 W=A%L;②二进制情况下:根据块号范围定出表示所有块号所需的二进制位数,从而对逻辑地址切分,直接读出 P、W)
- 比较页号P和页表长度M,判断 是否越界( P<M 则合法,注页号是从0开始的,P=W时也时非法越界)
- 计算页表中页号P对应的页表项地址:页表项地址 = 页表起始地址 + 页号P * 页表项长度,取出该页表项的内容 b 即为内存块号
- 计算实际物理地址:①十进制情况下:E = b * 页面大小L + 页内偏移量W;②二进制情况下:将内存块号b 和页内偏移量W 直接拼接起来
例题:
若页面大小L为1K字节,页号2对应的内存块号b=8,将逻辑地址A=2500转化为物理地址E。
解:
① 页面大小为1K=210 B = 1024 B
故由十进制计算方法得:P=2500/1024 =2、W=2500%1024=452
② 2号页表可以查到,合法,并且对应得内存块号为8,
③ 由于内存块号大小=页面大小=1K=210 B = 1024 B,故存逻辑地址A对应得物理地址为8 * 1024 +452 = 8644 - 小细节:
- ⭐页内偏移量位数与页面大小之间是有关系的(要能已知其中一个互推另一个)
- ⭐页式管理中地址是一维的
- ⭐整个过程中,会访问两次内存,第一次:查页表;第二次:访问实际的目标内存单元
- 实际应用中,通常调整页表项大小,使一个页框刚好可以装下整数个页表项
- 为了方便找到页表项,页表一般是存放在连续的内存块当中
- 原理:
-
7. 具有快表的地址变换机构(基地址变换的改进)
- 局部性原理
- 时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某条数据被访问过,那么不久之后该数据很有可能再次被访问(因为程序中存在大量的循环)
- 空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问(因为很多数据在内存中都是连续存放的)
- 之前的基本地址变换机构中,每次要访问一个逻辑地址,都需要查询内存中的页表。由于局部性原理,可能连续很多次查到的都是同一个页表项,故引入了快表的概念,减少对页表的访问次数
- 时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某条数据被访问过,那么不久之后该数据很有可能再次被访问(因为程序中存在大量的循环)
- 什么是块表(TLB)
- 快表:又称联想计数器(TBL),是一种访问速度比内存快很多的高速缓冲存储器,用来存放当前访问的若干页表项,以加速地址变化的过程,与此对应,内存中的页表项常称为慢表
- 引入块表后,地址的变换过程:
- CPU给出逻辑地址,有某个硬件得到页号和页内偏移量,将页号与快表中的所有页号进行比较
- 如果在快表中找到匹配的页号,说明要访问的页表项在快表中有副本,则之间从中取出该页的内存块号,再将内存块号与页内偏移量拼接成物理地址,最后,访问该物理地址对应的内存单元
- 因此若快表命中,则访问某个逻辑地址只需要一次访问内存
- 如果在快表中没有找到匹配的页号,则需要访问内存中的页表,查找页表得到块号,此时需要访问内存两次
- 注意:找到页表项之后,应同时将其存入快表,以便后面可能的再次访问。若快表已满,则必须按照一定算法对旧的快表项进行替换
- 优点:
由于查询快表的速度比查询页表的速度快很多,因此只要快表命中,就可以节省很多时间。由于局部性原理,快表的命中率一般在90%以上例:
某系统使用基本分页存储管理,并采用了具有快表的地址变换机构,访问一次快表时耗时1us,访问一次页表时,耗时100us。若快表的命中率为90%,那么访问一个逻辑地址的平均耗时是多少?
解:
① (1+100)* 0.9 + (1+100+100)* 0.1 = 111 us
② 若支持快表与慢表同时访问则解为:(1+100)* 0.9 + (100+100)* 0.1 = 110.9 us
③ 若未采用快表机构:100+100 = 200 us
- 局部性原理
-
8. 两级页表
- 单级页表存在的问题:
- 问题一: 页表必须连续地存放,也因此当页表很大时,需要占用很多个连续的页框
- 问题二: 没有必要让整个页表常驻内存,因为进程在很长一段时间内,可能只访问某几个特定的页面
- 解决问题一:
- 解决方法:可以将长的页表进行分组,使每个内存块刚好可以放入一组,另外,再为离散分配的页表分组再建立一张页表,称为 页目录表 或外层页表、顶层页表
- 两级页表的原理、地址结构
- 设32位逻辑地址,页表项大小为4B,页面大小为4KB,则页内地址占12位
- 单级页表的逻辑地址结构:
- 进程最多有220 个页面,页号范围为0 ~ 220 -1,故单级页表中需要220 个页表项来保存220 个页面,又因为页框大小 = 页面大小 = 4 KB,一个页表项大小为4B,所以一个页框中刚好可以装下4KB/4B = 1K=210个页表项,所以,为了让页框装页表项刚好能装满,故将220 个页表项以210个为一组分开,可以分为210 组
- 两级页表的逻辑地址结构:
- 如何实现地址转换:
- 按照地址结构将逻辑地址拆分成三部分
- 从PCB中读出页目录起始地址,再根据一级页号查找页目录表,找到下一级页表在内存中的存放位置
- 根据二级页号查找二级页表,找到最终访问的内存块号
- 结合页内偏移量得到最终物理地址
例:将逻辑地址(0000 0000 00,00 0000 0001,1111 1111 1111 )转换为物理地址
解:
① 由一级页号0000 0000 00得0号页表,查找页目录表得0号页表存储在3号内存块中
② 访问3号内存块将0号页表读出来,由二级页号00 0000 0001查找0号页表的0号页面,得到最终要访问4号内存块
③ 4号内存块的起始地址为4 * 4KB = 4 * 212 = 4 * 4096 = 16384,又页内偏移量为1023
④ 故最终的物理地址为16384+1023=17407
- 解决问题二:
- 解决办法:
① 可以在需要访问页面时才将页面调入内存(虚拟存储技术),可以在页表项中增加一个标志位,用于表示该页面是否已经调入内存
② 若想要访问的页面不再内存中,则产生缺页中断(内中断),然后将目标页面调入内存
- 解决办法:
- 注意:
- ⭐若采用多级页表机制,则各级页表的大小不能超过一个页面
例如:某系统按字节编址,采用40位逻辑地址,页面大小位4KB,页表项大小为4B,假设采用纯页式存储,要采用几级页表,页内偏移量为多少位?
解:
页面大小=页框大小=4KB=212 B,故页内偏移量为12位,剩余逻辑地址28位,一个页框可以刚好放下210 个页表项,因此每一级页表对应页号应为10位,总共28位需要分三级
- ⭐两级页表访问内存次数分析=:(假设没有快表机构)
- 第一次访问:访问内存中的页目录表
- 第二次访问:访问内存中的二级页表
- 第三次访问:访问目标内存单元
- 结论:N 级页表,访问一个逻辑地址需要 N+1 次访问内存
- ⭐若采用多级页表机制,则各级页表的大小不能超过一个页面
- 单级页表存在的问题: