本文为《现代操作系统》的读书笔记
内存管理的基本要求
- 内存管理应具有以下功能:
- 实现内存的分配和回收
- 地址变换:在多道程序环境下,把程序中的逻辑地址转换为相应的物理地址
- “扩充”内存容量:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存
- 进行存储保护:保证各道作业在各自的存储空间内运行,互不干扰
程序的装入和链接
- 在多道程序环境下,要使程序运行,必须创建进程,而创建进程第一件事就是将程序和数据装入内存。一个用户源程序要变为在内存中可执行的程序,通常要进行以下处理:
- (1) 编译:由编译程序将用户源程序编译成若干个目标模块 (目标代码)
- (2) 链接:由链接程序将目标模块和相应的库函数链接成装入模块 (装入模块通常采用相对地址的形式,其首地址为 0,其余指令中的地址都相对于首地址而编址)
- 静态链接: 在程序运行之前,先将各目标模块及它们所需的库函数,链接成一个完整的装入模块(执行文件),以后不再拆开
- 实现静态链接应解决的问题: (1) 相对地址的修改; (2) 变换外部调用符号
- 存在的问题:(1) 不便于对目标模块的修改和更新; (例如更改库函数之后还需要重新进行链接) (2) 无法实现对目标模块的共享 (例如同一个库函数可能需要被同时装入多个装入模块)
- 装入时动态链接:将一组目标模块在装入内存时,边装入边链接的方式。具有便于修改和更新、便于实现对目标模块的共享的优点
- 运行时动态链接:在程序运行中需要某些目标模块时,才对它们进行链接。具有高效且节省内存空间的优点
- 静态链接: 在程序运行之前,先将各目标模块及它们所需的库函数,链接成一个完整的装入模块(执行文件),以后不再拆开
- (3) 装入:由装入程序将装入模块装入内存
- 绝对装入方式 (只适用于单道程序环境):如果在编译时,事先知道用户程序在内存的驻留位置,则编译程序在编译时就产生绝对地址的目标代码。装入程序就直接把装入模块中的程序和数据装入到指定的位置 (程序的逻辑地址与物理地址相同,不需进行地址转换 (程序中仍是符号地址,只是编译或汇编后转换为绝对地址))
- 可重定位装入方式
- 重定位:多道程序环境下,多个目标模块的起始地址通常都是从 0 开始的,程序中的其他地址都是相对于起始地址的,将逻辑地址映射为物理地址的过程即为重定位(实质是一个地址变换过程/地址映射)
- 可重定位装入方式:事先不知用户程序在内存的驻留位置,装入程序在装入时根据内存的实际情况把相对地址(逻辑地址)转换为绝对地址,装入到适当的位置(在装入时进行地址转换); 根据地址变换进行的时间及采用技术手段不同,可分为静态重定位和动态重定位两类
- 静态重定位: 地址变换在装入时一次完成,以后不再改变 (要求作业在装入内存时,必须给它分配要求的全部内存空间;并且作业一旦进入内存,整个运行期间就不能在内存中移动,也不能再申请内存空间)
- 动态重定位: 事先不知用户程序在内存的驻留位置,为了保证程序在运行过程中,它在内存中的位置可经常改变,装入程序把装入模块装入内存后,并不立即把装入模块中相对地址转换为绝对地址,而是在程序运行时才进行地址转换。这种方式需一个重定位寄存器来支持 (可以将程序分配到不连续的存储区中;在程序运行前可以只装入它的部分代码;程序运行期间可以动态分配内存;便于程序段的共享,可以向用户提供一个比存储空间大得多的地址空间)
- 动态运行时装入方式
* 分区的存储保护
* 覆盖与交换
连续分区存储管理方式
- 连续/分区分配方式: 指为一个用户程序分配一块连续的内存空间
单一连续分配方式 (单独分区分配)
- 存储管理方法:将内存分为系统区(内存低端,分配给 OS 用)和用户区(内存高端,分配给用户用)。采用静态分配方式,即作业一旦进入内存,就要等待它运行结束后才能释放内存
- 主要特点:管理简单,但因内存中只装入一道作业运行,内存空间浪费大,各类资源的利用率也不高,只能用于单用户、单任务的 OS 中
分区分配方式
- 分区分配方式是满足多道程序设计需要的一种最简单的存储管理方法
- 存储管理方法:将内存分成若干个分区(大小相等 / 不相等),除 OS 占用一个分区外,其余的每一个分区容纳一个用户程序。按分区的变化情况,可将分区存储管理进一步分为:
- 固定分区存储管理、动态分区存储管理
固定分区分配方式
存储管理方法
- 内存空间的划分:将内存空间划分为若干个固定大小的分区,除 OS 占一分区外,其余的每一个分区装入一道程序。分区的大小可以相等,也可以不等,但事先必须确定,在运行时不能改变。即分区大小及边界在运行时不能改变
- 系统需建立一张分区说明表或使用表,以记录分区号、分区大小、分区的起始地址及状态 (已分配或未分配)
内存分配
- 当某个用户程序要装入内存时,由内存分配程序检索分区说明表,从表中找出一个满足要求的尚未分配的分区分配给该程序,同时修改说明表中相应分区的状态;若找不到大小足够的分区,则拒绝为该程序分配内存
- 当程序执行完毕,释放占用的分区,管理程序将修改说明表中相应分区的状态为未分配,实现内存资源的回收
主要特点
- 管理简单,但因作业的大小并不一定与某个分区大小相等,从而使一部分存储空间被浪费。所以主存的利用率不高
例
- 在某系统中,采用固定分区分配管理方式,内存分区(单位:字节)情况如图所示,现有大小为 1K、 9K、33K、121K 的多个作业要求进入内存,试画出它们进入内存后的空间分配情况,并说明主存浪费多大?
- 解:根据分区说明表,将 4 个分区依次分配给 4 个作业,同时修改分区说明表,其内存分配和分区说明表如下所示:
主存浪费空间 = ( 8 − 1 ) + ( 32 − 9 ) + ( 120 − 33 ) + ( 331 − 121 ) = 7 + 23 + 87 + 210 = 327 ( k ) =(8-1)+(32-9)+(120-33)+(331-121)=7+23+87+210=327(k) =(8−1)+(32−9)+(120−33)+(331−121)=7+23+87+210=327(k)
动态分区分配方式
存储管理方法
- 不事先将内存划分成一块块的分区,而是在作业进入内存时,根据作业的大小动态地建立分区,并使分区的大小正好适应作业的需要。因此系统中分区的大小是可变的,分区的数目也是可变的
主要特点
- 管理简单。进程的大小与某个分区大小相等,从而主存的利用率有所提高
分区分配中的数据结构
- 空闲分区表: 用来登记系统中的空闲分区 (分区号、分区起始地址、分区大小及状态)
- 空闲分区链: 用链头指针将系统中的空闲分区链接起来,构成空闲分区链。每个空闲分区的起始部分存放相应的控制信息(如大小,指向下一空闲分区的指针等)
分区分配算法
- 为了将一个作业装入内存,应按照一定的分配算法从空闲分区表(链)中选出一个满足作业需求的分区分配给作业,如果这个空闲分区的容量比作业申请的空间要大,则将该分区一分为二,一部分分配给作业,剩下的部分仍然留在空闲分区表(链)中,同时修改空闲分区表(链)中相应的信息。目前常用分配算法有:
- 首次适应算法(First Fit)、循环首次适应算法(Next Fit)、最佳适应算法(Best Fit)、最坏适应算法(Worst Fit)、快速适应算法(Quick Fit)
- 分区的切割:设请求的分区大小为
u.size
,空闲分区的大小为m.size
, 若m.size - u.size ≤ csize
(csize
是事先规定的不再切割的剩余分区的大小),说明多余部分太小,可不再切割,将整个分区分配给请求者;否则,从该分区中按请求的大小划分出一块内存空间分配出去,余下的部分仍留在空闲分区表/链中,然后,将分配区的首址返回给调用者
回收内存
- 当作业执行结束时,释放所占有的内存空间,OS 应回收已使用完毕的内存分区。系统根据回收分区的大小及首地址,在空闲分区表中检查是否有邻接的空闲分区,如有,则合成为一个大的空闲分区,然后修改有关的分区状态信息;回收分区与已有空闲分区的相邻情况有以下四种 (其中 (a) (b) 情况表项数不变;© 情况表项数减 1;(d) 情况表项数加 1):
首次适应算法(First Fit)
- 空分区(链)按地址递增的次序排列。在进行内存分配时,从空闲分区表/链首开始顺序查找,直到找到第一个满足其大小要求的空闲分区为止。然后按照作业大小,从该分区中划出一块内存空间分配给请求者,余下的空闲分区仍按地址递增的次序保留在空闲分区表(链)中
- 特点:优先利用内存低地址部分的空闲分区, 从而保留了高地址部分的大空闲区。但由于低地址部分不断被划分,致使低地址端留下许多难以利用的很小的空闲分区 (碎片或零头),而每次查找又都是从低地址部分开始,这增加了查找可用空闲分区的开销
例
- 系统中的空闲分区表如下,现有三个作业申请分配内存空间 100KB、30KB 及 7KB。给出按首次适应算法的内存分配情况及分配后空闲分区表
- 解:按首次适应算法,申请作业 100k,分配 3 号分区,剩下分区为 20k,起始地址 200K;申请作业 30k,分配 1 号分区,剩下分区为 2k,起始地址 50K;申请作业 7k,分配 2 号分区,剩下分区为 1k,起始地址 79K;其内存分配图及分配后空闲分区表如下
循环首次适应算法(Next Fit)
- 在为作业分配内存空间时,不再每次从空闲分区表/链首开始查找,而是从上次找到的空闲分区的下一个空闲分区开始查找,直到找到第一个能满足其大小要求的空闲分区为止。然后,再按照作业大小,从该分区中划出一块内存空间分配给请求者,余下的空闲分区仍按地址递增的次序保留在空闲分区表/链中
- 特点:使存储空间的利用更加均衡,不致使小的空闲区集中在存储区的一端,但这会导致缺乏大的空闲分区
例
- 系统中的空闲分区表如下,现有三个作业申请分配内存空间 100KB、30KB 及 7KB。给出按循环首次适应算法的内存分配情况及分配后空闲分区表
- 解:按循环首次适应算法,申请作业 100k,分配 3 号分区,剩下分区为 20k,起始地址 200K;申请作业 30k,分配 4 号分区,剩下分区为 301k,起始地址 350K;申请作业 7k,分配 1 号分区,剩下分区为 25k,起始地址 27K;其内存分配图及分配后空闲分区表如下
最佳适应算法(Best Fit)
- 空闲分区表/链按容量大小递增的次序排列。在进行内存分配时,从空闲分区表/链首开始顺序查找,直到找到第一个满足其大小要求的空闲分区为止。按这种方式为作业分配内存,就能把既满足作业要求又能将作业大小最接近的空闲分区分配给作业。如果该空闲分区大于作业的大小,则与首次适应算法相同,将剩余空闲分区仍按容量大小递增的次序保留在空闲分区表/链中
- 特点: 若存在与作业大小一致的空闲分区,则它必然被选中,若不存在与作业大小一致的空闲分区,则只划分比作业稍大的空闲分区,从而保留了大的空闲分区,但空闲区一般不可能正好和它申请的内存空间大小一样,因而将其分割成两部分时,往往使剩下的空闲区非常小,从而在存储器中留下许多难以利用的小空闲区(外碎片或外零头)
例
- 系统中的空闲分区表如下,现有三个作业申请分配内存空间 100KB、30KB 及 7KB。给出按最佳适应算法的内存分配情况及分配后空闲分区表
- 解:按最佳适应算法,申请作业 100k,分配 3 号分区,剩下分区为 20k,起始地址 200K;申请作业 30k,分配 2 号分区,剩下分区为 2k,起始地址 50K;申请作业 7k,分配 1 号分区,剩下分区为 1k,起始地址 79K;其内存分配图及分配后空闲分区表如下
最坏适应算法(Worst Fit)
- 空闲分区表/链按容量大小递减的次序排列。在进行内存分配时,从空闲分区表/链首开始顺序查找,找到的第一个能满足作业要求的空闲分区,一定是个最大的空闲区。这样可保证每次分割后的剩下的空闲分区不至于太小(还可被分配使用,以减少“外碎片”),仍把它按从大到小的次序保留在空闲分区表/链中
- 特点: 总是挑选满足作业要求的最大的分区分配给作业。这样使分给作业后剩下的空闲分区也较大,可装下其它作业。但由于最大的空闲分区总是因首先分配而划分,当有大作业到来时,其存储空间的申请往往会得不到满足
例
- 系统中的空闲分区表如下,现有三个作业申请分配内存空间 100KB、30KB 及 7KB。给出按最坏适应算法的内存分配情况及分配后空闲分区表
- 解:按最坏适应算法,申请作业 100k,分配 1 号分区,剩下分区为 231k,起始地址 420K;申请作业 30k,分配 1 号分区,剩下分区为 201k,起始地址 450K;申请作业 7k,分配 1 号分区,剩下分区为 194k,起始地址 457K;其内存分配图及分配后空闲分区表如下
* 快速适应算法(Quick Fit)
- 将空闲分区根据容量大小进行分类 (根据进程常用的空间大小进行划分),对于每一类具有相同容量的所有空闲分区,单独设立一个空闲分区(链)表。系统中存在多个空闲分区(链)表,同时在内存中设立一张管理索引表,每个表项对应了一种空闲分区类型,并指向该类型的空闲分区表的表头
- 特点:查找效率高,找到该类后,取下第一块分配即可;不会产生碎片;分区归还给系统时算法复杂,系统开销大;内存空间存在一定的浪费
动态重定位分区分配方式
碎片问题
- 在分区存储管理方式中,必须把作业装入到一片连续的内存空间。如果系统中有若干个小的分区 (碎片),其总容量大于要装入的作业,但由于它们不相邻接,也将导致作业不能装入内存
- 内部碎片:指分配给作业的存储空间中未被利用的部分。如固定分区中存在的碎片
- 外部碎片:指系统中无法利用的小的空闲分区。如动态分区中存在的碎片
- 例如,下图中有四个小空闲分区,不相邻,但总容量为 90KB,如果现有一作业要求分配 40KB 的内存空间,由于系统中所有空闲分区的容量均小于 40KB,故此作业无法装入内存
碎片问题的解决方法
- 拼接 / 紧凑 / 紧缩技术: 将内存中所有作业移到内存一端(作业在内存中的位置发生了变化,这就必须对其地址加以修改或变换即称为重定位),使本来分散的多个小空闲分区连成一个大的空闲区
- 拼接时机:分区回收时;当找不到足够大的空闲分区且总空闲分区容量可以满足作业要求时
- 动态重定位分区分配技术: 在动态分区分配算法中增加拼接功能,在找不到足够大的空闲分区来满足作业要求,而系统中总空闲分区容量可以满足作业要求时,进行拼接
非连续分配管理方式
- 连续分配存储管理方式产生的问题
- (1) 会产生碎片
- (2) 把进程放在一个存储区中,要求一段较大并且连续的空间
- 非连续分配管理方式允许将作业/进程离散放到多个不相邻接的分区中,就可以避免拼接。基于这一思想产生了以下的离散分配方式:
- 分页式存储管理 (离散分配的基本单位是页)、 分段式存储管理 (离散分配的基本单位是段)、段页式存储管理 (离散分配的基本单位是页)
基本分页存储管理方式
基本思想
空间划分
- (1) 将一个用户进程的地址空间(逻辑)划分成若干个大小相等的区域,称为页
- (2) 内存空间也分成若干个与页大小相等的区域,称为(存储、物理)块 / 页框 (Frame)
内存分配
- 在为进程分配内存时, 以块为单位, 将进程中若干页装入到多个不相邻的块中, 最后一页常装不满一块而出现页内碎片
- 若页面较小: 减少页内碎片和内存碎片的总空间, 有利于提高内存利用率; 每个进程页面数增多, 从而使页表长度增加, 占用内存较大; 页面换进换出速度将降低
- 若页面较大: 每个进程页面数减少,页表长度减少,占用内存就较小; 页面换进换出速度将提高; 会增加页内碎片,不利于提高内存利用率
- 页面大小通常为 2 的幂,一般在 512B - 8KB 之间
页表
- 为了便于在内存找到进程的每个页面所对应的块,系统为每个进程建立一张页面映象,简称页表。页表一般存放在内存中,记录了页面在内存中对应的块号;页表的基址及长度由页表寄存器给出
- 内存访问速度降低:访问一个数据/指令需访问内存 2 次(页表一次,内存一次)
- 内存访问速度降低:访问一个数据/指令需访问内存 2 次(页表一次,内存一次)
地址结构
- 逻辑地址:e.g. 地址长为 32 位,其中 0~11 位为页内地址,每页的大小为
2
12
=
4
K
B
2^{12}=4KB
212=4KB; 12~31 位为页号,地址空间最多允许有
2
20
=
1
M
2^{20} =1M
220=1M 页。若给定一个逻辑地址空间中的地址为A,页面大小为L,则页号P
- 物理地址:e.g. 地址长为 22 位,其中 0~11 位为块内地址,即每块的大小与页相等;12~21 位为块号,内存地址空间最多允许有 2 10 = 1 K 2^{10} =1K 210=1K 块
地址变换机构
- 地址变换机构: 将用户地址空间中的逻辑地址变换为内存空间中的物理地址 (页号
→
\rightarrow
→ 块号)
- 基本的地址变换机构借助页表来完成 (注意:如果页号不合法,则产生越界中断)
- 例:在一分页存储管理系统中,逻辑地址长度为 16 位,页面大小为 4096 字节,现有一逻辑地址为 2F6AH,且第 0、1、2 页依次放在物理块 10、12、 14 号中,问相应的物理地址为多少?
- 因逻辑地址长度为 16 位,页面大小 4096 字节,所以,前面的 4 位表示页号。2F6AH 的二进制表示:0010 1111 0110 1010,可知页号为 2,根据已知条件:该页放在 14 号物理块中。物理地址的十六进制表示为:EF6AH
- 例:在一分页存储管理系统中,逻辑地址长度为 16 位,页面大小为 4096 字节,现有一逻辑地址为 2F6AH,且第 0、1、2 页依次放在物理块 10、12、 14 号中,问相应的物理地址为多少?
- 具有快表的地址变换机构 (解决速度问题):有了分页后,次内存访问必须进行两次,页表访问会降低一半的性能
→
\rightarrow
→ 利用局部性原理,提出快表 (转换检测缓冲区 (Translation Lookaside Buffer, TLB))
- 快表是一种特殊高速缓冲存储器。内容为页表中的一部分或全部. CPU 产生的逻辑地址的页首先在快表中寻找,若找到(命中),就找出其对应的物理块;若未找到(未命中),再到页表中找其对应的物理块,并将之复制到快表。若快表中内容满,则按某种算法淘汰某些页
- 有效访问内存的时间: T = P T L B × ( T T L B + T M ) + ( 1 − P T L B ) ∗ ( T T L B + 2 T M ) T=P_{TLB}\times(T_{TLB}+T_M)+(1-P_{TLB})*(T_{TLB} + 2T_M) T=PTLB×(TTLB+TM)+(1−PTLB)∗(TTLB+2TM). 其中, P T L B P_{TLB} PTLB 为快表的命中率, T T L B T_{TLB} TTLB 为快表的访问时间, T M T_M TM 为内存的访问时间
- 基本的地址变换机构借助页表来完成 (注意:如果页号不合法,则产生越界中断)
- 多级页表 (解决容量问题):现代计算机系统支持非常大的逻辑地址空间 (
2
32
2^{32}
232~
2
64
2^{64}
264),则页表本身就变得非常大; 解决方法:
- (1) 只将当前需要的部分页表项调入内存,其余的需要时再调入
- (2) 多级页表
二级页表
- 将页表再进行分页,并离散地将各个页表页面存放在不同的物理块中,同时也再建立一张页表(外层页表),用来记录页表页面对应的物理块号
- e.g. 在下图中,32 位 (4 GB) 的虚拟地址被划分为 10 位的 P T 1 PT_1 PT1 域、10 位的 P T 2 PT_2 PT2 域和 12 位的 Offset (偏移量) 域。因为偏移量是 12 位,所以页面长度是 4KB,共有 2 20 2^{20} 220 个页面。例如一个需要 12MB 内存的进程 P P P,其最底端是 4MB 的程序段,后面是 4MB 的数据段,顶端是 4MB 的堆栈段,在数据段上方和堆栈段下方之间是大量没有使用的空闲区。在图 b b b 左边是顶级页表,它具有 1024 个表项,对应于 10 位的 P T 1 PT_1 PT1 域。当一个虚拟地址送到 MMU (Memory Management Unit) 时,先提取 P T 1 PT_1 PT1 域并把该值作为访问顶级页表的索引。顶级页表的表项 0 指向程序正文的页表,表项 1 指向数据的页表,表项 1023 指向堆栈的页表,其他的表项(用阴影表示的)未用。现在把 P T 2 PT_2 PT2 域作为访问选定的二级页表的索引,以便找到该虚拟页面的对应页框号
- 值得注意的是,虽然在图中虚拟地址空间超过 100 万个页面,实际上只需要四个页表:顶级页表,以及
0
0
0~
4
M
4M
4M (正文段)、
4
M
4M
4M~
8
M
8M
8M (数据段) 和顶端
4
M
4M
4M (堆栈段) 的二级页表。顶级页表中 1021 个表项的 “在/不在” 位都被设为 0,当访问它们时强制产生一个缺页中断。如果发生了这种情况,操作系统将注意到进程正在试图访问一个不希望被访问的地址,并采取适当的行动,比如向进程发出一个信号或杀死进程等。
多级页表
- 对于 32 位机器,采用两级页表结构是非常合适的;但对于 64 位的机器,若仍然采用两级页表结构,页表项占 4B,页面大小为
2
22
2^{22}
222,通过计算需要占用
4
B
×
2
42
4B\times2^{42}
4B×242 (即
16
T
B
16TB
16TB)的连续内存空间,因此必须采用多级页表结构:
- 将外层页表再进行分页,将各外层页表页面离散地存放在不相邻接的物理块中,再利用第 2 级的外层页表来记录它们之间的对应关系
- 逻辑地址:
页的共享与保护
共享代码 (数据) 的实现方法
- 由各进程共享的一段代码 (数据),要求各进程相应的页存入内存相同物理块中
- 带来的问题: 若共享数据与不共享数据划在同一块中,则:有些不共享的数据也被共享,不易保密; 计算共享数据的页内位移较困难
- 实现数据共享的最好方法: 段式存储管理
页的保护
- (1) 地址越界保护
- (2) 在页表中设置保护位 (定义操作权限:只读,读写,执行等)
基本分段存储管理方式
分段存储管理方式的引入
- 引入分段存储管理方式,主要是为了满足用户的一系列要求:
- 方便编程:按逻辑关系分为若干个段,每个段从 0 编址,并有名字和长度,访问的逻辑地址由段名和段内偏移量决定
- 信息共享:共享是以信息为逻辑单位,页是存储信息的物理单位,而段是信息的逻辑单位
- 信息保护:保护也是对信息的逻辑单位进行保护的
- 动态链接:动态链接以段为单位
- 动态增长:实际应用中,某些段(数据段)会不断增长,前面的存储管理方法均难以实现
分段系统的基本原理
空间划分 (分段)
- 将用户作业的逻辑地址空间划分成若干个大小不等的段(由用户根据逻辑信息的相对完整来划分)。各段有段名(常用段号代替),首地址为 0
内存分配
- 在为作业分配内存时,以段为单位,分配一段连续的物理地址空间;段间不必连续
段表
- 类似页表,段表记录了段与内存位置的对应关系;段表的基址及长度由段表寄存器给出
共享与保护
- 分段易于实现段的共享,即允许若干个进程共享一个或多个分段;段的共享,是通过不同作业段表中的项指向同一个段基址来实现。几道作业共享的例行程序就可放在一个段中,只要让各道作业的共享部分有相同的基址/限长值
- 对共享段的信息必须进行保护
共享段表
共享段的分配
- 当第一个使用共享段的进程提出请求时,由系统为该共享段分配一物理区,并调入该共享段,同时修改相应的段表(该段的内存地址)和共享段表。当其它进程需要调用此段时,不需调入,只需修改相应的段表和共享段表
共享段的回收
- 当共享共享段的某进程不再使用该共享段时,修改相应的段表和共享段表。当最后一个共享此段的进程也不再需要此段时,则系统回收此共享段的物理区,同时修改共享段表(删除该表项)
分段管理的保护主要有三种:
- (1) 地址越界保护: 先利用段表寄存器中的段表长度与逻辑地址中的段号比较,若段号超界则产生越界中断;再利用段表项中的段长与逻辑地址中的段内位移进行比较,若段内位移大于段长,也会产生越界中断
- 注:在允许段动态增长的系统中,允许段内位移大于段长
- (2) 访问控制保护
- (3) 环保护机构: 在环系统中,程序的访问和调用应遵循一定的规则:
- 一个程序可以访问同环或较低特权环中的数据
- 一个程序可以调用同环或较高特权环中的服务
段页式存储管理方式
- 段页式存储管理:将用户程序分成若干个段(段式),并为每一个段赋一个段名,再把每个段分成若干个页(页式)。其地址结构由段号、段内页号、及页内位移三部分所组成
虚拟存储器
- 常规存储器管理方式的特征:
- (1) 一次性:作业在运行前需一次性地全部装入内存,可能导致以下问题:(1) 有的作业很大, 所需内存空间大于内存总容量, 使作业无法运行; (2) 有大量作业要求运行,但内存容量不足以容纳下所有作业,只能让一部分先运行,其它在外存等待
- (2) 驻留性:作业装入内存后,便一直驻留内存,直至作业运行结束
- 基于局部性原理,程序在运行之前,没有必要全部装入内存,仅须将当前要运行的页(段) 装入内存即可。运行时,如访问的页(段)在内存中,则继续执行,如访问的页(段)未在内存中(缺页或缺段),则利用 OS 的请求调页(段)功能,将该页(段)调入内存。如内存已满,则利用 OS 的页(段)置换功能,按某种置换算法将内存中的某页(段)调至外存,从而调入需访问的页
- 虚拟存储器是指仅把作业的一部分装入内存便可运行作业的存储管理系统,它具有请求调页功能和页面置换功能,能从逻辑上对内存容量进行扩充,其逻辑容量由外存容量和内存容量之和决定,其最大容量由计算机的地址结构决定,其运行速度接近于内存,成本接近于外存
- 实现虚拟存储器必须解决好以下有关问题:主存辅存统一管理问题、逻辑地址到物理地址的转换问题、部分装入和部分对换问题
- 虚拟存储管理主要采用以下技术实现:请求分页存储管理、请求分段存储管理、请求段页式存储管理
- 虚拟存储器的特征:(1) 多次性 (虚拟存储器最重要的特征): 一个作业被分成多次调入内存运行; (2) 对换性: 允许在作业运行过程中进行换进、换出。换进、换出可提高内存利用率; (3) 虚拟性: 能够从逻辑上扩充内存容量,使用户所看到的内存容量远大于实际内存容量 (注:虚拟性以多次性和对换性为基础,而多次性和对换性又是以离散分配为基础)
请求分页存储管理方式
- 在分页系统的基础上,增加了请求调页功能、页面置换功能所形成的页式虚拟存储器系统。它允许只装入若干页的用户程序和数据,便可启动运行,以后在硬件支持下通过调页功能和页面置换功能,陆续将要运行的页面调入内存,同时把暂不运行的页面换到外存上,置换时以页面为单位
请求分页中的硬件支持
- 页表机制
- (1) 状态位:指示该页是否已调入内存。若是 0,表示该表项对应的虚拟页面不在内存中,访问该页面会引起一个缺页中断
- (2) 访问位:不论是读还是写,系统都会在该页面被访问时设置访问位。它的值被用来帮助操作系统在发生缺页中断时选择要被淘汰的页面
- (3) 修改位:表示该页在调入内存后是否被修改过。若修改过,则换出时需重写至外存
- (4) 外存地址:指出该页在外存上的地址
- 缺页中断机构: 在请求分页系统中,当访问的页不在内存,便产生一缺页中断,请求 OS 将所缺页调入内存空闲块,若无空闲块,则需置换某一页,同时修改相应页表表目
- 缺页中断与一般中断的区别: (1) 在指令执行期间产生和处理中断信号; (2) 一条指令在执行期间,可能产生多次缺页中断
- 缺页中断与一般中断的区别: (1) 在指令执行期间产生和处理中断信号; (2) 一条指令在执行期间,可能产生多次缺页中断
这里默认访问快表中的页不会遇到缺页
请求分页中的内存分配策略和分配算法
- 为进程分配内存时,将涉及以下三个问题:
- (1) 最小物理块数的确定: 最小物理块数指能保证进程正常运行所需的最小的物理块数,与计算机的硬件结构有关,取决于指令的格式、功能和寻址方式
- (2) 物理块的分配策略
- 固定分配局部置换:为每个进程分配固定数目 n n n 的物理块,在整个运行中都不改变。如出现缺页,则从中置换一页
- 可变分配全局置换:分配固定数目的物理块,但 OS 自留一空闲块队列,若发现缺页,则从空闲块队列中分配一空闲块与该进程,并调入缺页于其中。当空闲块队列用完时, OS 才从内存中选择一页置换
- 可变分配局部置换:分配一定数目的物理块,若发现缺页,则从该进程的页面中置换一页,根据该进程缺页率高低,则可增加或减少物理块
- (3) 物理块分配算法
- 平均分配算法:平均分配给各个进程
- 按比例分配算法:根据进程的大小按比例分配给各个进程
- 考虑优先权的分配算法:将系统提供的物理块一部分根据进程大小先按比例分配给各个进程,另一部分再根据各进程的优先权适当增加物理块数
请求分页中的页面调入策略
- 调入策略决定什么时候将一个页面由外存调入内存,从何处将页面调入内存
- 何时调入页面
- (1) 预调页策略:将那些预计在不久便被访问的页预先调入内存。这种调入策略提高了调页的效率,减少了 I/O 次数。但由于这是一种基于局部性原理的预测,若预调入的页面在以后很少被访问,则造成浪费,故这种方式常用于程序的首次调入
- (2) 请求调页策略:当进程运行中访问的页不在内存时,则发出缺页中断,提出请求调页,由 OS 将所需页调入内存。这种策略实现简单,应用于目前的虚拟存储器中,但易产生较多的缺页中断,且每次调一页,系统开销较大,容易产生抖动现象
- 从何处调入页面: 在请求分页系统中,通常将外存分成了文件区和对换区,文件区按离散分配方式存放文件,对换区按连续分配方式存放对换页
- 对换区:系统有足够的对换区空间,运行前可将与进程相关的文件从文件区复制至对换区,以后缺页时,全部从对换区调页
- 文件区:系统没有足够的对换区空间,凡是不会被修改的文件,每次都直接从文件区调页,换出时不必写回外存。而对可能会修改的文件第一次调页直接从文件区,换出时换至对换区,以后从对换区调页
页面置换算法
- 页面置换算法是用来选择换出页面的算法。页面置换算法的优劣直接影响到系统的效率,若选择不合适,可能会出现以下现象:
- 刚被淘汰出内存的页面,过后不久又要访问它,需要再次将其调入,而该页调入内存后不久又再次被淘汰出内存,然后又要访问它,如此反复,使得系统把大部分时间用在了页面的调进换出上,而几乎不能完成任何有效的工作,这种现象称为抖动
最佳置换算法
- 在缺页中断发生时,有些页面在内存中,其中有一个页面(包含紧接着的下一条指令的那个页面)将很快被访问,其他页面则可能要到 10、100 或 1000 条指令后才会被访问,每个页面都可以用在该页面首次被访问前所要执行的指令数作为标记。最优页面置换算法规定应该置换标记最大的页面
- 这个算法惟一的问题就是它是无法实现的。当缺页中断发生时,操作系统无法知道各个页面下一次将在什么时候被访问
- 但用这种方式,我们可以通过最优页面置换算法对其他可实现算法的性能进行比较。如果操作系统达到的页面置换性能只比最优算法差 1%,那么即使花费大量的精力来寻找更好的算法最多也只能换来 1% 的性能提高
- 假定系统为某进程分配了 3 个物理块,进程运行时的页面走向为 1,2,3,4,1,2,5,1,2,3,4,5,开始时 3 个物理块均为空,计算采用最佳置换页面淘汰算法时的缺页率?
- 缺页率 =
7
/
12
7/12
7/12
- 缺页率 =
7
/
12
7/12
7/12
先进先出置换算法 FIFO
- 由操作系统维护一个所有当前在内存中的页面的链表, 最新进入的页面放在表尾,最久进入的页面放在表头。当发生缺页中断时,淘汰表头的页面并把新调入的页面加到表尾。但最先进入的页面不一定是不常用的,因此很少使用纯粹的 FIFO 算法 (对具有线性顺序访问的程序比较合适,而对其他情况效率不高)
- 假定系统为某进程分配了 3 个物理块,进程运行时的页面走向为 1,2,3,4,1,2,5,1,2,3,4,5,开始时 3 个物理块均为空,计算采用先进先出页面淘汰算法时的缺页率?
- 缺页率 =
9
/
12
9/12
9/12
- 缺页率 =
9
/
12
9/12
9/12
最近最少使用置换算法 LRU
Least Recently Used
- 观察:在前面几条指令中频繁使用的页面很可能在后面的几条指令中被使用。反过来说,已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。这个思想提示了一个可实现的算法: 在缺页中断发生时,置换未使用时间最长的页面
- 该算法的性能接近于最佳算法,但实现起来代价很高。为了完全实现 LRU,需要在内存中维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾。困难的是在每次访问内存时都必须要更新整个链表。在链表中找到一个页面,删除它,然后把它移动到表头是一个非常费时的操作,即使使用硬件实现也一样费时,所以在实际系统中往往使用该算法的近似算法
例
- 假定系统为某进程分配了 3 个物理块,进程运行时的页面走向为 1,2,3,4,1,2,5,1,2,3,4,5,开始时 3 个物理块均为空,计算采用最近最少使用页面淘汰算法时的缺页率?
- 缺页率 =
10
/
12
10/12
10/12
- 缺页率 =
10
/
12
10/12
10/12
LRU 算法的近似算法实现
- (1) 矩阵法: 维护一个 n × n n\times n n×n 矩阵 ( n n n 为物理页数),一个页面被访问后,将对应的行置 1,对应列置 0。每次选择 1 个数最多的行的页号进行替换
- (2) 利用一特殊的栈保存当前使用的页号,每当进程访问某页面时,把被访问页面移到栈顶,于是栈底的页面就是最久未使用的页面
- (3) 为每个页面设立一个寄存器记录页面的访问情况。每当进程访问某页面时,将该页面对应寄存器的最高位置 1,系统定期将寄存器右移一位并将最高位补 0,于是寄存器数值最小的页面是最久未使用的页面
- (4) 软件模拟 LRU 算法,称为最不常用算法 (Not Frequently Used, NFU)
- 将每个页面与一个软件记数器相联,记数器的初值为 0。每次时钟中断时,由操作系统扫描内存中的所有的页面,将每个页面的
R
R
R 位(访问位,它的值是 0 或 1)加到它的记数器上。这个计数器大体上跟踪了各个页面被访问的频繁程度。发生缺页中断时,则置换计数器值最小的页面
- 存在问题:它从来不忘记任何事情。比如,在一个多次(扫描)编译器中,在第一次扫描中被频繁使用的页面在程序进入第二次扫描时,其计数器的值可能仍然很高。实际上,如果第一次扫描的执行时间恰好是各次扫描中最长的,含有以后各次扫描代码的页面的计数器可能总是比含有第一次扫描代码的页面小,结果是操作系统将置换有用的页面而不是不再使用的页面
- 修改: 老化(aging)算法: (1) 在
R
R
R 位被加进之前先将计数器右移一位;(2) 将
R
R
R 位加到计数器最左端的位而不是最右端的位
- 将每个页面与一个软件记数器相联,记数器的初值为 0。每次时钟中断时,由操作系统扫描内存中的所有的页面,将每个页面的
R
R
R 位(访问位,它的值是 0 或 1)加到它的记数器上。这个计数器大体上跟踪了各个页面被访问的频繁程度。发生缺页中断时,则置换计数器值最小的页面
时钟置换算法
- 时钟算法需要给每个物理块增加一个附加位,称为使用位
u
u
u。当某一页装入内存,该物理块的使用位设为 1;当该页随后被访问时,它的使用位也设为 1。对于页面置换算法,把用于替换的物理块集合看作是一个循环缓冲区,并且有一个指针与之关联。当需要进行页面置换时,如果指针所在的页面
u
=
0
u=0
u=0,则将它置换,然后把指针指向下一个物理块。否则,把该块的使用位置为 0,然后跳过该块继续扫描,直到找到一个
u
=
0
u=0
u=0 的物理块
- 改进 Clock 算法: 再增加一个修改位
w
w
w。具有 2 个附加位的物理块中的页具有 4
种情况:- 最近未访问过,也未被修改过( u = 0 , w = 0 u=0,w=0 u=0,w=0)
- 最近未访问过,但被修改过( u = 0 , w = 1 u=0,w=1 u=0,w=1)
- 最近访问过,但没有被修改过( u = 1 , w = 0 u=1,w=0 u=1,w=0)
- 最近访问过,也修改过( u = 1 , w = 1 u=1,w=1 u=1,w=1)
- 改进型时钟算法如下:(1) 从指针当前位置开始扫描,在这次扫描过程中对使用位的值不做任何修改,找到一个
u
=
0
,
w
=
0
u=0,w=0
u=0,w=0 的物理块,进行置换; (2) 如果第一步失败,则查找
u
=
0
,
w
=
1
u=0,w=1
u=0,w=1 的块,遇到的第一个这样的物理块,则把该块中的页置换出去,同时把扫描过程中的遇到的
u
=
1
u=1
u=1 的块设为
u
=
0
u=0
u=0; (3) 如果前两步都失败,在重新执行第一步、第二步,这样一定会找到一个合适的页替换出去
- 替换原则:若有未使用过的页面,则首先将它换出,若全部页面都使用过,则优先把未修改的页面换出,这样可以避免写回
- 替换原则:若有未使用过的页面,则首先将它换出,若全部页面都使用过,则优先把未修改的页面换出,这样可以避免写回
* 其它置换算法
最少使用 (LFU) 置换算法
- 选择到当前时间为止访问次数最少的页面淘汰。该算法要求为每页设置一个访问计数器,用来记录该页面被访问的频率。每当页面被访问,该页面的访问计数器加 1。发生缺页中断时,淘汰计数值最小的页面,并将所有计数器清零
页面缓冲算法
- 该算法是对 FIFO 算法的发展,通过建立置换页面的缓冲,就有机会找回刚被置换的页面,从而减少系统 I/O 的开销
- 页面缓冲算法用 FIFO 算法选择被置换页,选择出的页面不是立即换出,而是放入两个链表之一,如果页面未被修改,就将其归入到空闲页面链表的末尾,否则将其归入已修改页面链表末尾。这些空闲页面和已修改页面会在内存中停留一段时间 。如果这些页面被再次访问,只需将其从相应链表中移出,就可以返回进程,从而减少一次 I/O 开销
- 需调新页,则将新页读入到空闲页面链表的第一个页面中,然后将其从该链表中移出。当已修改的页面达到一定数目后,再将它们一起写入磁盘,然后将它们归入空闲页面链表。这样能大大减少 I/O 操作的次数
请求分段存储管理方式
- 在分段系统的基础上,增加了请求调段功能及分段置换功能,所形成的段式虚拟存储器系统。它允许只装入若干段的用户程序和数据,便可启动运行,以后在硬件支持下通过请求调段功能和分段置换功能,陆续将要运行的段调入内存,同时把暂不运行的段换到外存上,置换时以段为单位
请求分段中的硬件支持
- 段表机制
- 存取方式:存取属性(执行、只读、允许读/写)
- 访问字段 A A A:记录该段被访问的频繁程度
- 修改位 M M M:表示该段在进入内存后,是否被修改过
- 存在位 P P P:表示该段是否在内存中
- 增补位:表示在运行过程中,该段是否做过动态增长
- 外存地址:表示该段在外存中的起始地址
内存的深入理解与应用
内存分配方式
内存构成
- 程序代码区: 存放函数体的二进制代码
- 全局区 (静态区) (static): 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放
- 栈区 (stack): 由编译器自动分配释放,存放函数的参数值,局部变量的值等
- 堆区 (heap): 一般由程序员分配释放,分配方式类似链表,若程序员不释放,程序结束时可能由 OS 回收
内存分配方式
- 从静态存储区分配: 内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量; 初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域
- 在栈上创建: 在执行函数时,函数的参数值,内局部变量的存储单元都可以在栈上创建。函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限
- 从堆上分配,亦称动态内存分配: 程序在运行的时动态申请内存,程序员自己负责释放内存
- 操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的
delete
/free
语句才能正确的释放本内存空间
- 操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的
申请大小的限制
- 栈:在 Windows 下, 栈是向低地址扩展的数据结构,是一块连续的内存的区域。栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小
- 堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。堆获得的空间比较灵活,也比较大
申请效率的比较
- 栈由系统自动分配,速度较快。但程序员是无法控制的
- 堆是由
new
分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
内存错误
- 特点: 编译器不能自动发现这类错误,通常是在程序运行时才能捕捉到 (时隐时现,无明显症状)
- Tip: 编译器不会对栈内存和堆内存进行初始化,因此内存的缺省初值究竟是什么并没有统一的标准
- e.g. VS 在 Debug 和 Release 状态下在初始化变量时所做的操作是不同的。Debug 是将未被初始化的栈内存的每个字节位都赋值成
0xcc
;未被初始化的堆内存会被写入0xcd
(这也是程序输出中,“烫烫烫” 和 “屯屯屯” 的由来了),以有利于调试。而 Release 的赋值是直接从内存中分配的,内容近似于随机。所以如果在没有初始化变量的情况下去使用它的值,就会导致问题发生
- e.g. VS 在 Debug 和 Release 状态下在初始化变量时所做的操作是不同的。Debug 是将未被初始化的栈内存的每个字节位都赋值成