存储器管理
随着计算机技术的发展,系统软件和应用软件在种类、功能上都急剧地膨胀。存储器容量扩大仍不能满足现代软件发展的需要,因此存储器仍然是一种宝贵而又稀缺的资源。存储器管理的主要对象是内存,对存储器的管理直接影响到存储器的利用率和系统性能也。对外存的管理与对内存的管理相类似,只是外存主要是用来存放文件。
存储器
在计算机执行时几乎每一条指令都涉及对存储器的访问,因此要求存储器的速度必须非常快,能与处理机的速度相匹配。此外还要求存储器具有非常大的容量,且价格还应很便宜。但是实际上鱼和熊掌不可兼得,所以在现代计算机系统中都采用了多层结构的存储器系统。
存储器的层次结构
对于通用计算机而言,存储层次至少应具有三级:CPU 寄存器、主存、辅存。实际情况下还可以根据具体的功能细分为寄存器、高速缓存、主存储器、磁盘缓存、固定磁盘、可移动存储介质等 6 层。在存储层次中的层次越高,则存储介质越靠近 CPU、访问速度越快,相对的价格也越高且存储容量也越小。寄存器、高速缓存、主存储器和磁盘缓存均属于操作系统存储管理的管辖范畴,掉电后它们中存储的信息不再存在。而低层的固定磁盘和可移动存储介质则属于设备管理的管辖范畴,存储的信息将被长期保存。
可执行存储器
寄存器和主存储器又被称为可执行存储器,进程可以在很少的时钟周期内使用一条 load 或 store 指令对可执行存储器进行访问。对辅存的访问则需要通过 I/O 设备实现,在访问中将涉及到中断、设备驱动程序以及物理设备的运行,辅存的所需耗费的时间远远高于访问可执行存储器的时间。操作系统的存储管理负责对可执行存储器的分配、回收,以及提供在存储层次间数据移动的管理机制。
寄存器具有与处理机相同的速度,对寄存器的访问速度最快,但价格昂贵且容量小。主存储器简称内存或主存,是计算机系统中的主要部件,用于保存进程运行时的程序和数据。通常处理机都是从主存储器中取得指令和数据的,并将其所取得的指令放入指令寄存器中,所读取的数据装入到数据寄存器中,或者将寄存器中的数据存入到主存储器。
缓存
对于缓存的讨论一般涉及高速缓存和磁盘缓存,缓存一般是用于缓和 2 种存介质速度不匹配的问题。
高速缓存
在计算机系统中,为了缓和内存与处理机速度之间的矛盾,许多地方都设置了高速缓存。高速缓存是是介于寄存器和存储器之间的存储器,主要用于备份主存中较常用的数据。这样可以减少处理机对主存储器的访问次数,提高程序执行速度。高速缓存容量远大于寄存器,而比内存约小两到三个数量级左右,访问速度快于主存储器。高速缓存的设计原理是局部性原理,也就是程序在执行时在一较短的时间内,程序的执行仅局限于某个部分。通常进程的程序和数据存放在主存储器中,每当要访问时才被临时复制到速度较快的高速缓存中。当 CPU 访问一组特定信息时,首先检查它是否在高速缓存中,如果已存在就直接从中取出使用,否则就从主存中读出信息。
磁盘缓存
磁盘的 I/O 速度远低于对主存的访问速度,为了缓和两者之间在速度上的不匹配而设置了磁盘缓存。磁盘缓存主要用于暂时存放频繁使用的一部分磁盘数据和信息,以减少访问磁盘的次数。但磁盘本身并不是一种实际存在的存储器,而是利用主存中的部分存储空间暂时存放从磁盘中读出(或写入)的信息。
程序的装入和链接
用户程序的执行步骤
用户程序要在系统中运行,须先将它装入内存然后再将其转变为一个可以执行的程序。用户程序的执行通常都要经过以下几个步骤:
- 编译:由编译程序对用户源程序进行编译,形成若干个目标模块;
- 链接:由链接程序将若干个目标模块以及所需要的库函数链接在一起,形成一个完整的装入模块;
- 装入:由装入程序将装入模块装入内存。
程序在编写的时候使用的是逻辑地址,而最终在内存执行时程序和数据都使用具体的物理地址来存储,要执行程序就涉及到逻辑地址到逻辑地址的转换。
程序的装入
绝对装入方式
当计算机系统很小且仅能运行单道程序时,完全有可能知道程序在内存物理位置。采用绝对装入方式(Absolute Loading Mode)是用户程序经编译后,产生物理地址的目标代码。程序中所使用的物理地址可由程序员直接赋予,但要求程序员熟悉内存的使用情况,这样会提高程序维护的复杂性。因此通常是在程序中采用符号地址,然后在编译或汇编时转换为绝对地址。
可重定位装入方式
在多道程序环境下,编译程序不可能预知经编译后所得到的目标模块应放在内存的何处。因此程序中使用的地址和数据存放的地址都是相对于起始地址而言的逻辑地址,起始地址通常都是从 0 开始的。此时应采用可重定位装入方式(Relocation Loading Mode),它根据内存的具体情况将装入模块装入到内存的适当位置。
在采用可重定位装入程序将装入模块装入内存后,会使装入模块中的所有逻辑地址与实际装入内存后的物理地址不同。
例如在用户程序的 500 号单元处有一条指令 LOAD1,1500,该指令的功能是将 1500 单元中的整数 400 取至寄存器 1。但若直接将该用户程序装入到内存的 1000 ~ 3000 号单元,则在执行 1500 号单元中的指令时,仍然会从 1500 号单元中把数据取至寄存器 1,而导致数据错误。这是因为用户程序使用的是逻辑地址,内存内部需要用物理地址来寻址,正确的方法应该将取数指令中的地址 1500 修改成 1500 + 1000 = 2500。
一个作业装入内存时必须分配作业需要的全部内存空间,如果没有足够的内存就不能装入作业。作业一旦装入内存后,在运行期间就不能移动,也不能再申请内存空间。
动态运行时的装入方式
可重定位装入方式不允许程序运行时在内存中移动位置,因为程序在内存中的移动时必须对程序和数据的绝对地址进行修改。但是在程序运行过程中,它在内存中的位置可能经常要改变。此时可以采用动态运行时装入(Dynamic Run-time Loading)的方式,装入程序在把装入模块装入内存后,把这种地址转换推迟到程序真正要执行时才进行。这种方式的优点是便于程序的修改和更新,实现对目标模块的共享。因为装入内存后的所有地址都仍是逻辑地址,这种方式需要一个重定位寄存器的支持。
程序的链接
源程序经过编译后可得到一组目标模块,链接程序的功能是将这组目标模块以及所需要的库函数装配成一个完整的装入模块。在对目标模块进行链接时,根据进行链接的时间不同可分成如下三种。
静态链接方式
静态链接(Static Linking)方式在程序运行之前,先将各目标模块及它们所需的库函数链接成一个完整的装配模块。进行静态链接时需要对相对地址进行修改,并且变换外部调用符号。
例如经过编译后所得到的三个目标模块 A、B、C,长度分别为 L、M 和 N,需要将这几个目标模块装配成一个装入模块。在模块 A 中 有一条语句 CALL B 用于调用模块 B,在模块 B 中有一条语句 CALL C,用于调用模块 C。在由编译程序所产生的所有目标模块中使用的都是相对地址,其起始地址都为 0。因为每个模块中的地址都是相对于起始地址计算的,所以在链接后原模块 B 和 C 在装入模块的起始地址要相应的修改为 L 和 L + M。同时也要将每个模块中所用的外部调用符号也都变换为相对地址,把 B 的起始地址变换为 L,把 C 的起始地址变换为 L + M。
装入时动态链接
装入时动态链接(Load-time Dynamic Linking)是指一组目标模块在装入内存时,采用边装入边链接的链接方式。在装入一个目标模块时,若发生一个外部模块调用事件,将引起装入程序去找出相应的外部目标模块并将它装入内存,修改目标模块中的相对地址。
装入时动态链接方式便于修改和更新,由于各目标模块是分开存放的,所以局部的修改并不会影响整个模块,比较灵活。而静态链接装配在一起的装入模块,如果要修改或更新其中的某个目标模块,则要求重新打开装入模块。同时装入时动态链接便于实现对目标模块的共享,因为 OS 能轻易将一个目标模块链接到几个应用模块上。
运行时动态链接
应用程序在运行时,往往每次要运行的模块可能是不相同的。前 2 种方式由于事先无法知道本次要运行哪些模块,故只能是将所有可能要运行到的模块全部都装入内存并全部链接在一起。因此经常有部分目标模块根本就不运行,所以这么做的效率很低下。
运行时动态链接(Run-time Dynamic Linking)链接方式是将对某些模块的链接推迟到程序执行时才进行,也就是当发现一个被调用模块尚未装入内存时,立即由 OS 去找到该模块并装入内存,将其链接到调用者模块上。凡在执行过程中未被用到的目标模块,都不会被调入内存和被链接到装入模块上。这样不仅能加快程序的装入过程,而且可节省大量的内存空间。
对换
覆盖技术
早期的计算机内存很小,因此经常会出现内存大小不够的情况,后来引入了覆盖技术用来解决“程序大小超过物理内存总和”的问题。覆盖技术的思想是将程序分为多个段(多个模块),常用的段常驻内存,不常用的段在需要时调入内存。内存中分为一个“固定区”和若干个“覆盖区”,需要常驻内存的段放在“固定区”中,调入后就不再调出(除非运行结束)。不常用的段放在“覆盖区”,需要用到时调入内存,用不到时调出内存。
例如程序 0 是系统中需要频繁运行的程序,而程序 1 和 2 仅在程序 0 需要完成某些功能的时候调用,且 2 个程序不会同时被调用。这种情况下可以将程序 0 放在固定区中,由于程序 1 和 2 不那么常用且不会同时调用,因此可以准备一个足够大的覆盖区来供调用时存放程序。
覆盖技术必须由程序员声明覆盖结构,操作系统完成自动覆盖。这种方式对用户不透明,增加了用户编程负担,目前已经被淘汰。
对换技术
在内存中的某些进程由于某事件尚未发生而被阻塞运行,但它却占用了大量的内存空间。另一方面却又有许多作业因内存空间不足只能驻留在外存上,不能进入内存运行。对换技术是指把内存中暂时不能运行的进程或者暂时不用的程序和数据换出到外存上,腾出足够的内存空间,再把已具备运行条件的进程或进程所需要的程序和数据换入内存。根据每次对换时所对换的数量,可将对换分为如下两类:
对换类型 | 说明 |
---|---|
整体对换 | 以整个进程为单位 |
页面(分段)对换 | 以进程的一个“页面”或“分段”为单位 |
对换空间的管理
在具有对换功能的 OS 中,通常把磁盘空间分为文件区和对换区两部分。文件区占用磁盘空间的大部分,用于存放各类文件。由于通常的文件的访问的频率较低,故对文件区管理先是提高文件存储空间的利用率,然后才是提高对文件的访问速度,采取的是离散分配方式。对换空间只占用磁盘空间的小部分,用于存放从内存换出的进程。由于进程的对换操作的频率较高,故对对换空间管理的先是提高进程换入和换出的速度,然后才是提高文件存储空间的利用率,采取的是连续分配方式。
为了实现对对换区中的空闲盘块的管理,需要用相应的数据结构记录外存对换区中的空闲盘块的使用情况。通常使用空闲分区表或空闲分区链,在空闲分区表的每个表目中应包含对换区的首址及其大小。由于对换分区的分配采用的是连续分配方式,因而对换空间的分配与回收与动态分区方式时的内存分配与回收方法相同。
进程的换出和换入
当内核发现内存不足时需要调用(或换醒)对换进程,它的主要任务是实现进程的换出和换入。
进程换出
进程换出是将内存中的某些进程调出至对换区,腾出内存空间。首先需要选择被换出的进程,检查所有驻留在内存中的进程,首先选择处于阻塞状态或睡眠状态的进程。当有多个这样的进程时选择优先级最低的进程换出,为了防止低优先级进程在被调入内存后很快又被换出,还需考虑进程在内存的驻留时间。如果系统中已无阻塞进程但内存空间仍不足,便选择优先级最低的就绪进程换出。在对进程换出时,只能换出非共享的程序和数据段,对于那些共享的程序和数据段,只要还有进程需要它就不能被换出。在进行换出时应先申请对换空间,若申请成功就启动磁盘,将该进程的程序和数据传送到磁盘的对换区上。
进程换入
对换进程将定时执行进程换入操作,首先要先找出“就绪”状态但已换出的进程。当有许多这样的进程时,将选择其中已换出到磁盘上时间最久(必须大于规定时间)的进程作为换入进程,为它申请内存。如果申请成功可直接将进程从外存调入内存,如果失败则需先将内存中的某些进程换出,腾出足够的内存空间后再将进程调入。在对换进程成功地换入一个进程后,若还有可换入的进程则再继续执行换入换出过程,直到内存中再无“就绪且换出”状态的进程为止。