既然是介绍存储器管理,肯定要介绍存储器
顾名思义,存储器,肯定是用来存储数据的,如果把计算机中的CPU比喻成人的宝贝脑袋瓜子,存储器就是你的海马体(不是海绵体)专门用来存储数据。
计算机执行时,几乎每一条指令都涉及对存储器的访问,就像你平时无论做什么,都会用到大脑中的记忆功能。
计算机存储器根据控制器指定的位置存入和取出信息。有了存储器,计算机才有记忆功能,才能保证正常工作。
理想的存储器要满足下面三个条件:
现在的技术无法同时满足这三个条件,所以现代计算机系统中无一例外地使用了多层结构地存储器系统
上图中,寄存器,高速缓存,主存储器,磁盘缓存均属于操作系统存储管理的管辖范畴,掉电后其中存储的信息不再存在。
而低层的固定磁盘和可移动存储介质则属于设备管理的管理范畴,其中存储的信息将被长期保存。
在计算机系统的存储层次中,寄存器和主存储器又被称为可执行存储器,存放其中的信息与存放于辅存中的信息而言,计算机采用的访问机制是不同的,所需耗费的时间也不同。
存储器分为 内存储器(内存) 与 外存储器(外存)。内存储器又常称为主存储器(简称主存),属于主机的组成部分;外存储器又常称为辅助存储器(简称辅存),属于外部设备。
存储器管理的主要对象是内存,由于对外存的管理与对内存的管理类似,只是它们的用途不同,外存主要用于存放文件,对外存的管理放在文件管理中介绍
主存储器简称内存或主存,用于保存进程运行时的程序和数据,也称可执行存储器。
寄存器拥有非常高的读写速度,所以在寄存器之间的数据传送非常快。
高速缓存是现代计算机结构中一个重要部件,从上图可以看出,高速缓存是介于寄存器和主存储器之间的存储器,可以备份主存中较常用的数据,减少处理机对主存储器的访问次数,可以大幅度地提高执行速度。
通常进程的程序和数据存放在主存储器中,每当要访问时,才被临时复制到一个速度较快的高速缓存中。这样当CPU访问一组特定信息时,须首先检查它是否在高速缓存中,
大部分计算机都有指令高速缓存,用来缓存下一条将执行的指令。
高速缓存的速度越高价格越贵,所以有的计算机系统中设计了两级或多级高速缓存。
从上图中可以看到,磁盘缓存介于主存储器和固定磁盘之间。
我们知道,磁盘的I/O 速度远低于对主存的访问速度,为了缓和两个之间速度上的不匹配,我们设置了磁盘缓存.
磁盘缓存主要用于暂时存放频繁使用的一部分磁盘数据和信息,以减少访问磁盘的次数。
注意,磁盘缓存和高速缓存不同,磁盘缓存不是一种实际存在的存储器,而是利用主存中的部分存储空间暂时存放从磁盘中读出(或写出)的信息
主存可以看成是辅存的高速缓存,因为辅存中的数据必须复制到主存方能使用,反之,数据也必须保存到主存中,才能输出到辅存。
一个文件通常被存储在辅存中,当期需要运行或被访问时,就必须调入主存,也可以暂时存放在主存的磁盘高速缓存中。
用户程序在系统中运行,必须先将其装入内存,然后再将其变为一个可以执行的程序,通常需要如下步骤:
如果只有单个目标模块,则无需进行链接可直接装入,这个目标模块也就是装入模块。所以我们先来介绍这种最简单的情况。
在将一个装入模块装入内存时,有三种装入方式
(Absolute)
计算机系统很小,且仅能运行单道程序时,完全有可能知道程序将驻留在什么位置,此时可采用绝对装入方式。
用户程序经过编译后,将产生绝对地址(即物理地址)的目标代码。
优点:装入过程简单
缺点:依赖于硬件结构,不适于多道程序系统
(Relocation Loading Mode)
绝对装入方式只能将目标模块装入到内存中事先指定的位置,这只适用于弹道程序环境。
多道程序环境下,编译程序不可能预知编译后所得到的目标模块应在何处。
因此对于用户程序编译所形成的若干个目标模块,它们的起始地址都是从0 开始的,程序中的其他地址也都是相对于起始地址计算的。
此时可采用可重定位装入方式,它可以根据内存的具体情况将装入模块装入到内存的合适位置。
在采用可重定位装入程序将装入模块装入内存后,装入模块的所有逻辑地址与实际装入内存后的物理地址不同。
案例
有这样一个用户程序编译而成的单个目标模块,起始地址从0开始。
在程序的1000号单元中,有一条指令LOAD 1, 2500
,功能是将2500处的整数365取至寄存器1。
这样看没有问题。
若将其装入内存的10000~15000号单元
若没有进行地址变换,从图中可以看到,原来在2500处位置的数据现在位置变为了12500,但是在执行程序LOAD 1, 2500
时,仍然会从地址2500处进行取值。导致数据错误。
所以要将目标程序的数据地址和指令地址进行修改。
把在装入时对目标程序中指令和数据地址的修改过程称为重定位
因为地址变换通常是在进程装入时一次完成的,以后不再改变,故称为静态重定位
(Dynamic Run-time Loading)
也称为 动态重定位
可重定位装入方式可将装入模块装入到内存中任何允许的位置。
但是该方式并不允许程序运行时在内存中移动位置
因为程序在内存中的移动意味着其物理地址发生改变,此时必须对程序和数据的地址(绝对地址)修改。
实际中一个程序可能在内存中的位置经常发生改变。
此时应该使用动态运行时装入方式
动态运行时的装入程序把装入模块装入内存后,并不立即将装入模块的逻辑地址转换为物理地址,而是将地址转换推迟到程序真正执行时才进行。
因此装入内存后的所有地址仍是逻辑地址。
但是每次程序运行时才进行地址转换,会不会影响程序的指令速度呢?
这就需要用到重定位寄存器的支持。
前面在介绍程序的装入时,是以一种特殊情况,即只有一个目标模块,此时这个目标模块就是装入模块。
链接程序的功能是将这组目标模块以及它们所需要的库函数装配成一个完整的装入模块
根据进行链接的时间不同,可以将链接 分成如下三种。
(Static Linking)
程序运行前,先将各模块及它们所需的库函数链接成一个完整的可执行程序,以后不再拆开。
(Load-time Dynamic Linking)
将用户的源程序编译后所得到的一组目标模块,在装入内存时,采用边装入,边链接的方式
优点:
(Run-time Dynamic Linking)
对某些目标模块的链接,是在程序执行中所需要该目标模块时,才对它进行的链接。
程序在装入内存时,必须要现有一定大小的内存空间才能成功装入内存。
连续分配方式为一个用户分配一个连续的内存空间,即程序中代码或数据的逻辑地址相邻,体现在内存空间分配时物理地址的相邻。
连续分配方式可分为四类:
内存在此种方式下分为系统区和用户区,系统区仅提供给操作系统使用,用户区为用户提供。
这种方式无需进行内存保护,内存中永远只有一道程序,不会因访问越界而干扰其它程序
固定分区分配是最简单的一种多道程序存储管理方式,
将用户内存空间划分为若干个固定大小的区域,每个分区只装入一道作业。
划分分区方法有两种:
分区大小相等
用于利用一台计算机去控制多个相同对象的场合,但是缺乏灵活性
分区大小不等
划分为含有多个较小的分区,适量的中等分区,少量的大分区
为便于内存分配,常将分区按其大小进行排队,并为之建立一张分区使用表,其中各表项包括每个分区的起始地址,大小及状态(是否已分配)
当有一个用户程序要装入时,有内存分配程序依据用户程序的大小检索该表,从中找出一个能满足要求的,尚未分配的分区,将之分配给程序,然后将该表项中的状态置为"已分配"。
若未找到大小足够的分区,则拒绝为该用户分配内存
动态分区分配又称为可变分区分配。
根据进程的实际需要,动态地为进程分配内存空间。
在实现动态分区分配时,会涉及到三方面的问题:
实现动态分区分配需要配置相应的数据结构描述空闲分区和已分配分区的情况,为分配提供依据。
常用的数据结构有:
⓶ 空闲分区链
为实现对空闲分区的分配和链接,在每个分区的起始部分设置一些用于控制分区分配的信息,以及用于链接各分区所用的前向指针,在分区尾部设置一后向指针。
通过前,后向链接指针,可将所有的空闲分区链接成一个双向链。
为检索方便,在分区尾部重复设置状态位和分区大小表目。
当分区被分配出去以后,把状态位由"0"改为"1",此时前后向指针已无意义。
将新作业装入内存时,需要按照一定的算法,从空闲分区表或者空闲分区链空选出一个分区分配给改作业。
这一节比较多,放在后面介绍
动态分区存储管理方式中,主要的操作时分配内存和回收内存
回收区与插入点的前一个空闲分区F1 相邻接,
此时将回收区与插入点的前一分区合并,不必为回收区分配新表项,只需修改前一分区F1的大小。
回收分区与插入点的后一空闲分区相邻接
此时可将两分区合并,形成新的空间分区,但用回收区的首址作为新空闲区的首址,大小为两者之和。
回收区既不与F1邻接,也不与F2邻接。
这时为回收区单独建立一个新表项,填写回收区的首址和大小,并根据其首址插入到空闲链中的适当位置。
地址变换过程是在程序执行期间,随着对每条指令或数据的访问自动进行的。称为动态重定位
为了实现动态分区分配,通常将系统中的空闲分区链接成一个链。
所谓顺序搜索,就是依次搜索空闲分区链上的空闲分区,去寻找一个其大小能满足要求的分区。
基于顺序搜索的动态分区分配算法有如下四种
首次适应(First Fit, FF)算法
空闲分区以地址递增的次序链接,
分配内存时顺序查找,找到大小能满足要求的第一个空闲分区
循环首次适应(Next Fit, NF)算法
又称邻近适应算法,由首次适应算法演变而成。
不同之处在于分配内存时从上次查找结束的位置开始继续查找
最佳适应(Best Fit, BF)算法
空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区。
最坏适应(Worst Fit)算法
又称最大适应算法
空闲分区以容量递减的次序链接,找到第一个能满足要求的空闲分区,即挑出最大的分区
在这几种算法中首次适应算法是最简单,通常也是最快和最好的。
不过首次适应算法会使得内存中的低地址部分出现很多小的空闲分区,每次分配查找时,都要经过这些分区,增加了查找的开销。
当系统很大时,内存分区可能会很多,相应的空闲分区链就很长,这时采用顺序搜索分区方法可能会很慢,为提高搜索空闲分区的速度,可采用基于索引搜索的动态分区分配算法,目前常用的有:
有兴趣的小伙伴可以上网查找资料,这里不再仔细介绍。
物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。在前端总线上传输的内存地址都是物理内存地址,编号从0开始一直到可用物理内存的最高端。这些数字被北桥(Nortbridge chip)映射到实际的内存条上。物理地址是明确的、最终用在总线上的编号,不必转换,不必分页,也没有特权级检查(no translation, no paging, no privilege checks)。
逻辑地址:CPU所生成的地址。逻辑地址是内部和编程使用的、并不唯一。例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址(偏移地址),不和绝对物理地址相干。
我的简单理解:一次考试,全校1000个学生,我考了第999名,班级倒数第二。
这里的全校排名999名就是物理地址。
而班级排名倒数第二就是逻辑地址,是相对于这个班级而言的。如果你的成绩放在另外一个班里,可能就是倒数第一。但是无论你在哪个班,全校排名即物理地址都不会改变。
外部碎片:分区外的空间浪费
内部碎片:分区内的空间浪费