操作系统原理之内存管理1- 存储区层次结构及内存连续分配逻辑


近年,内存容量一直在不断扩大(2G 4G 16 G …64G),但仍不能满足现代软件发展的需要,因此,存储器是一种宝贵而又紧俏的资源。如何对它加以有效的管理,不仅直接影响到存储器的利用率,而且还对系统性能有重大影响。存储器管理的主要对象是内存(也包括cpu寄存器)
这里总结cpu 的存储器结构,及一个程序被编译加载以及加载后是如何分配内存的过程。
当前的操作系统中,普遍采用的是基于分页和分段机制的虚拟内存机制,其他分配方式作为历史方案或特定场景的方案仅供参考
由于内容较多,本文主要说明连续内存分配逻辑,基于分页和分段的虚拟内存机制在下一篇博客中介绍

存储器层次结构

在理想情况下存储器的速度足够快,能跟处理机的速度匹配,容量也非常大而且价格还应很便宜。但目前无法同时满足这样三个条件。因此在现代计算机系统中,内存部件通常是采用层次结构来实现。

一般存储层次至少有三级:最高层为 CPU 寄存器、主存、辅存。根据具体的功能分工还可以细分为寄存器、高速缓存、主存储器、磁盘缓存、固定磁盘、可移动存储介质等 6 层。
在这里插入图片描述
寄存器和主存储器又被称为可执行存储器。和辅存的访问机制和性能都不同,可执行寄存器访问效率更高比辅存快三个数量级或更多。

寄存器:
寄存器访问速度最快,完全能与 CPU 协调工作,但价格却十分昂贵,因此容量不可能做得很大。寄存器的长度一般以字(word)为单位。寄存器的数目,对于当前的微机系统和大中型机,可能有几十个甚至上百个;而嵌入式计算机系统一般仅有几个到几十个。
寄存器一般用于加速存储器的访问速度,如用寄存器存放操作数,用作地址寄存器加快地址转换速度等。

内存:
主存储器(简称内存或主存)用于保存进程运行时的程序和数据,容量一般 几十M到几十G,
CPU的控制部件只能从内存中取得指令和数据,数据能够从内存中读取并装入到寄存器中,或者从寄存器存入到主存储器。
CPU 与外围设备交换的信息一般也依托于内存。
由于内存的访问速度远低于 CPU 执行指令的速度,因此常利用寄存器和高速缓存,来进行缓和。

高速缓存:
高速缓存是现代计算机结构中的一个重要部件,其容量大于或远大于寄存器,而比内存约小两到三个数量级左右,从几十 KB 到几 MB,访问速度快于内存。
根据程序执行的局部性原理(即程序在执行时将呈现出局部性规律,在一较短的时间内,程序的执行仅局限于某个部分),将主存中一些经常访问的信息存放在高速缓存中,减少访问主存储器的次数,可大幅度提高程序执行速度。
通常,进程的程序和数据是存放在内存中,每当使用时,被临时复制到一个速度较快的高速缓存中。
当 CPU 访问一组特定信息时,首先检查它是否在高速缓存中,如果已存在,可直接从中取出使用,以避免访问主存,否则,再从主存中读出信息。
大多数计算机有指令高速缓存,用来暂存下一条欲执行的指令,如果没有指令高速缓存,CPU 将会空等若干个周期,直到下一条指令从主存中取出。
由于高速缓存的速度越高价格也越贵,故有的计算机系统中设置了两级或多级高速缓存。紧靠内存的一级高速缓存的速度最高(不应该是靠近cpu的速度最高吗?),而容量最小,二级高速缓存的容量稍大,速度也稍低

磁盘缓存:
由于目前磁盘的 I/O 速度远低于内存速度,因此将频繁使用的一部分磁盘数据和信息,暂时存放在磁盘缓存中,可减少访问磁盘的次数。
磁盘缓存本身并不是一种实际存在的存储介质,它依托于固定磁盘,提供对内存空间的扩充,即利用内存中的存储空间,来暂存从磁盘中读出(或写入)的信息。
内存也可以看做是辅存的高速缓存,因为,辅存中的数据必须复制到内存方能使用;反之,数据也必须先存在内存中,才能输出到辅存。

程序链接和装载

在多道程序(就是多任务)环境下,要使程序运行,必须先创建进程。而创建进程的第一件事,便是将程序和数据装入内存。
源程序变为可执行的程序,通常都要经过以下几个步骤:
首先是要编译,由编译程序(Compiler)将用户源代码编译成若干个目标模块(Object Module);
其次是链接,由链接程序(Linker)将编译后形成的一组目标模块,以及它们所需要的库函数链接在一起,形成一个完整的装入模块(Load Module);
最后是装入,由装入程序(Loader)将装入模块装入内存。 如下:
在这里插入图片描述
程序的装入
模块装入内存的方式有:绝对装入方式、可重定位装入方式和动态运行时装入方式

绝对装入方式:

在编译时,如果知道程序将驻留在内存的什么位置,那么,编译程序将产生绝对地址的目标代码。例如,事先已知用户程序(进程)在内存中的地址从 R 处开始,则编译程序所产生的目标模块(即装入模块)的内存地址,便从 R 处开始一直向后增加。
绝对装入程序按照装入模块中的地址,将程序和数据装入内存。装入模块被装入内存后,由于程序中的逻辑地址与实际内存地址完全相同,故不须对程序和数据的地址进行修改。
程序中所使用的绝对地址,既可在编译或汇编时给出,也可由程序员直接赋予。
但在由程序员直接给出绝对地址时,不仅要求程序员熟悉内存的使用情况,而且一旦程序或数据被修改后,可能要改变程序中的所有地址。因此,通常是在程序中采用符号地址,然后在编译或汇编时,再将这些符号地址转换为绝对地址。
绝对装入方式只能将目标模块装入到内存中事先指定的位置。在多道程序环境下,编译程序不可能预知所编译的目标模块应放在内存的何处,因此,绝对装入方式只适用于单道程序环境。

可重定位装入方式:

在多道程序环境下,所得到的目标模块的起始地址通常是从 0 开始的,程序中的其它地址也都是相对于起始地址计算的。此时应采用可重定位装入方式,根据内存的当前情况,将装入模块装入到内存的适当位置。
采用可重定位装入程序将装入模块装入内存后,会使装入模块中的所有逻辑地址与实际装入内存的物理地址不同,
例如,在用户程序的 1000 号单元处有一条指令 LOAD 1,2500,该指令的功能是将 2500 单元中的整数 365 取至寄存器 1。
但若将该用户程序装入到内存的 10000~15000号单元而不进行地址变换,则在执行 11000 号单元中的指令时,它将仍从 2500 号单元中把数据取至寄存器 1 而导致数据错误。
正确的方法应该是将取数指令中的地址 2500 修改成 12500,即把指令中的相对地址 2500 与本程序在内存中的起始地址 10000 相加,才得
到正确的物理地址 12500。除了数据地址应修改外,指令地址也须做同样的修改,即将指令的相对地址 1000 与起始地址 10000 相加,得到绝对地址 11000。通常是把在装入时对目标程序中指令和数据的修改过程称为重定位。又因为地址变换通常是在装入时一次完成的,以后不再改变,故称为静态重定位
在这里插入图片描述

动态运行时装入方式:

可重定位装入方式可将装入模块装入到内存中任何允许的位置,故可用于多道程序环境;但这种方式并不允许程序运行时在内存中移动位置。因为,程序在内存中的移动,意味着它的物理位置发生了变化,这时必须对程序和数据的地址(是绝对地址)进行修改后方能运行。然而,实际情况是,在运行过程中它在内存中的位置可能经常要改变,此时就应采用动态运行时装入的方式
动态运行时的装入程序在把装入模块装入内存后,并不立即把装入模块中的相对地址转换为绝对地址,而是把这种地址转换推迟到程序真正要执行时才进行。因此,装入内存后的所有地址都仍是相对地址。为使地址转换不影响指令的执行速度,这种方式需要一个重定位寄存器的支持

程序的链接
源程序经过编译后,可得到一组目标模块,再利用链接程序将这组目标模块链接,形成装入模块。根据链接时间的不同,可把链接分成三种:静态链接、装入时动态链接、运行时动态链接

静态链接方式:

在程序运行之前,先将各目标模块及它们所需的库函数,链接成一个完整的装配模块,以后不再拆开。我们把这种事先进行链接的方式称为静态链接方式。
例如有三个目标模块 A、B、C,它们的长度分别为 L、M 和 N。在模块 A 中有一条语句 CALL B,用于调用模块 B。在模块 B 中有一条语句 CALL C,用于调用模块 C。B 和C 都属于外部调用符号。这几个目标模块装配成一个装入模块时,
要对相对地址进行修改。原模块 B 和 C 在装入模块的起始地址不再是 0,而分别是 L 和 L+M,所以此时须修改模块 B 和 C 中的相对地址,即把原 B 中的所有相对地址都加上 L,把原 C 中的所有相对地址都加上 L+M
这种先进行链接所形成的一个完整的装入模块,又称为可执行文件。通常都不再拆开它,要运行时可直接将它装入内存
在这里插入图片描述

装入时动态链接:

装入时动态链接。这是指将用户源程序编译后所得到的一组目标模块,在装入内存时,采用边装入边链接的链接方式。
在装入一个目标模块时,若发生一个外部模块调用事件,将引起装入程序去找出相应的外部目标模块,并将它装入内存,装入时也要修改相对内存地址。优点:便于程序修改、更新和共享。

运行时动态链接:

这是指对某些目标模块的链接,是在程序执行中需要该(目标)模块时,才对它进行的链接。
应用程序在运行时,每次要运行的模块可能是不相同的。但由于事先无法知道本次要运行哪些模块,故只能是将所有可能要运行到的模块都全部装入内存,并在装入时全部链接在一起。显然这是低效的,因为往往会有些目标模块根本就不运行。
行时动态链接方式,是对上述在装入时链接方式的一种改进。这种链接方式是将对某些模块的链接推迟到程序执行时才进行链接,亦即,在执行过程中,当发现一个被调用模块尚未装入内存时,立即由 OS 去找到该模块并将之装入内存,把它链接到调用者模块上。

程序被加载到内存后,要为程序分配内存空间,空间分配方式也有很多种具体如下

内存分配方式-连续分配

连续分配方式,是指为一个用户程序分配一个连续的内存空间。这种分配方式曾被广泛应用于 20 世纪 60~70 年代的 OS 中,它至今仍在内存分配方式中占有一席之地;又可把连续分配方式进一步分为单一连续分配、固定分区分配、动态分区分配以及动态重定位分区分配四种方式。

单一连续分配

最简单的一种存储管理方式,但只能用于单用户、单任务的操作系统中。这种存储管理方式,把内存分为系统区和用户区两部分,系统区仅提供给 OS 使用,通常是放在内存的低址部分;剩余的全部内存空间提供给用户使用。

固定分区分配

固定分区式分配是最简单的一种可运行多道程序的存储管理方式。
将内存用户空间划分为若干个固定大小的区域,在每个分区中只装入一道作业,
一共有几个分区,便允许有几道作业并发运行。
当有一空闲分区时,便可以从外存的后备作业队列中选择一个适当大小的作业装入该分区,当该作业结束时,又可再从后备作业队列
中找出另一作业调入该分区。
分区划分方法:
分区大小相等:所有的内存分区大小相等。缺点是缺乏灵活性,会浪费空间;当程序太大时,一个分区装不下时,程序会无法运行。一般用于执行特定相同任务的设备。
分区大小不等:把内存区划分成含有多个较小的分区、适量的中等分区及少量的大分区。避免上述缺点。
内存分配
建立一张分区使用表,保存每个分区的起始地址、大小及状态(是否已分配),当有用户程序要装入时,由内存分配程序检索该表,从中找出一个能满足要求的、尚未分配的分区,将之分配给该程序,然后将该表项中的状态置为“已分配”;若未找到大小足够的分区,则拒绝为该用户程序分配内存
在这里插入图片描述

固定分区方式,由于每个分区的大小固定,必然会造成存储空间的浪费,因而现在已很少将它用于通用的计算机中;但在某些用于控制多个相同对象的控制系统中,由于每个对象的控制程序大小相同,是事先已编好的,所以可以使用。

动态分区分配

动态分区分配是根据进程的实际需要,动态地为之分配内存空间。包括数据结构、分区分配算法和分区的分配与回收操作
数据结构
空闲分区表:在系统中设置一张空闲分区表,用于记录每个空闲分区的情况。每个空闲分区占一个表目,表目中包括分区序号、分区始址及分区的大小等数据项
空闲分区链:为了实现对空闲分区的分配和链接,在每个分区的起始和结束部分,设置一些用于控制分区分配的信息,以及用于链接各分区所用的指针;通过前、后向链接指针,将所有的空闲分区链接成一个双向链。当分区被分配出去以后,把状态位由“0”改为“1”
在这里插入图片描述

分区分配算法
为把一个新作业装入内存,须按照一定的分配算法,从空闲分区表或空闲分区链中选出一分区分配给该作业。目前常用算法包括:
*首次适应算法:*从空闲分区链首部开始顺序查找,直至找到一个大小能满足要求的空闲分区为止;然后再按照作业的大小,从该分区中划出一块内存空间分配给请求者,余下的空闲分区仍留在空闲链中。若从链首直至链尾都不能找到一个能满足要求的分区,则此次内存分配失败,返回。该算法要求空闲分区链以地址递增的次序链接,因此可以利用底址空间。保留了高址区域的大块分区,缺点是低址部分会被不断划分 ,增加查找开销。
*循环首次适应算法:*为进程分配内存空间时,不再是每次都从链首开始查找,而是从上次找到的空闲分区的下一个空闲分区开始查找,直至找到一个能满足要求的空闲分区,从中划出一块与请求大小相等的内存空间分配给作业. 必须设置起始查寻指针,用于指示下一次起始查寻的空闲分区,并采用循环查找方式。相比上述算法,空闲分区分布更均匀,但是大块分区无法收到保留
最佳适应算法: 算法要求将所有的空闲分区按其容量以
从小到大的顺序形成一空闲分区链。这样,第一次找到的能满足要求的空闲区,必然是最佳的。孤立地看,最佳适应算法似乎是最佳的,然而在宏观上却不一定。因为每次分配后所切割下来的剩余部分总是最小的,这样,在存储器中会留下许多难以利用的小空闲区
最坏适应算法: 扫描整个空闲分区表或链表,挑选一个最大的空闲区分割给作业使用。
快速适应算法: 又称为分类搜索法,是将空闲分区根据其容量大小进行分类,对于每一类具有相同容量的所有空闲分区,单独设立一个空闲分区链表,这样,系统中存在多个空闲分区链表,同时在内存中设立一张管理索引表,该表的每一个表项对应了一种空闲分区类型,
并记录了该类型空闲分区链表表头的指针。

分区分配操作
分配内存:系统应利用某种分配算法,从空闲分区链(表)中找到所需大小的分区。设请求的分区大小为 u.size,表中每个空闲分区的大小可表示为 m.size。若 m.size-u.size≤size(size 是事先规定的不再切割的剩余分区的大小),说明多余部分太小,可不再切割,将整个分区分配给请求者;否则(即多余部分超过 size),从该分区中按请求的大小划分出一块内存空间分配出去,余下的部分仍留在空闲分区链(表)中。然后,将分配区的首址返回给调用者。
在这里插入图片描述

回收内存:当进程运行完毕释放内存时,系统根据回收区的首址,从空闲区链(表)中找到相应的插入点,此时可能出现以下四种情况之一:
回收区与插入点的前一个空闲分区 F 1 相邻接,此时应将回收区与插入点的前一分区合并,不必为回收分区分配新表项
回收分区与插入点的后一空闲分区 F 2 相邻接。此时也可将两分区合并,形成新的空闲分区,但用回收区的首址作为新空闲区的首址,大小为两者之和
回收区同时与插入点的前、后两个分区邻接,此时将三个分区合并,使用 F 1 的表项和 F 1 的首址,取消 F 2 的表项,大小为三者之和
回收区既不与 F 1 邻接,又不与 F 2 邻接。这时应为回收区单独建立一新表项,填写回收区的首址和大小,并根据其首址插入到空闲链中的适当位置

伙伴系统

固定分区和动态分区方式都有不足之处。固定分区方式限制了活动进程的数目,当进程大小与空闲分区大小不匹配时,内存空间利用率很低。动态分区方式算法复杂,回收空闲分区时需要进行分区合并等,系统开销较大。伙伴系统方式是对以上两种内存方式的一种折衷方案
伙伴系统规定,无论已分配分区或空闲分区,其大小均为 2 的 k 次幂,k 为整数,l≤k≤m,其中:2 1 次方表示分配的最小分区的大小,2 m次方 表示分配的最大分区的大小,通常 2 m次方是整个可分配内存的大小。
假设系统的可利用空间容量为 2 m次方 ,则系统开始运行时,整个内存区是一个大小为 2 m次方的空闲分区。在系统运行过程中,由于不断的划分,可能会形成若干个不连续的空闲分区,将这些空闲分区根据分区的大小进行分类,对于每一类具有相同大小的所有空闲分区,单独设立一个空闲分区双向链表。这样,不同大小的空闲分区形成了 k(0≤k≤m)个空闲分区链表

当需要为进程分配一个长度为 n 的存储空间时,首先计算一个 i 值,使 2 i- 1次方 <n≤2 i 次方,然后在空闲分区大小为 2 i 的空闲分区链表中查找。若找到,即把该空闲分区分配给进程。否则,表明长度为 2 i 的空闲分区已经耗尽,则在分区大小为 2 i+ 1次方的空闲分区链表中寻找。若存在 2 i+ 1次方的一个空闲分区,则把该空闲分区分为相等的两个分区,这两个分区称为一对伙伴,其中的一个分区用于分配,而把另一个加入分区大小为 2 i次方 的空闲分区链表中。若大小为 2 i+ 1 次方的空闲分区也不存在,则需要查找大小为 2 i + 2次方 的空闲分区,若找到则对其进行两次分割:第一次,将其分割为大小为 2 i+ 1次方的两个分区,一个用于分配,一个加入到大小为 2 i+ 1次方的空闲分区链表中;第二次,将第一次用于分配的空闲区分割为 2 i 次方的两个分区,一个用于分配,一个加入到大小为 2 i 的空闲分区链表中。若仍然找不到,则继续查找大小为 2 i+ 3次方的空闲分区,以此类推。由此可见,在最坏的情况下,可能需要对 2 k次方 的空闲分区进行 k 次分割才能得到所需分区。
与一次分配可能要进行多次分割一样,一次回收也可能要进行多次合并,如回收大小为 2 i次方 的空闲分区时,若事先已存在 2 i 次方的空闲分区时,则应将其与伙伴分区合并为大小为2 i+ 1次方 的空闲分区,若事先已存在 2 i + 1次方 的空闲分区时,又应继续与其伙伴分区合并为大小为2 i+ 2 次方的空闲分区,依此类推。
与前面所述的多种方法相比较,由于该算法在回收空闲分区时,需要对空闲分区进行合并,所以其时间性能比前面所述的分类搜索算法差,但比顺序搜索算法好,而其空间性能则远优于前面所述的分类搜索法,比顺序搜索法略差

动态重定位

在连续分配方式中,必须把一个系统或用户程序装入一连续的内存空间。如果在系统中只有若干个小的分区,即使它们容量的总和大于要装入的程序,但由于这些分区不相邻接,也无法把该程序装入内存。例如:在内存中现有四个互不邻接的小分区,它们的容量分别为 10 KB、30 KB、14 KB 和 26 KB,其总容量是 80 KB。但如果现在有一作业到达,要求获得 40 KB 的内存空间,由于必须为它分配一连续空间,故此作业无法装入。这种不能被利用的小分区称为“零头”或“碎片”
拼接/紧凑法:上述场景中,将内存中的所有作业进行移动,使它们全都相邻接,这样,即可把原来分散的多个小分区拼接成一个大分区,这时就可把作业装入该区。这种通过移动内存中作业的位置,以把原来多个分散的小分区拼接成一个大分区的方,由于经过紧凑后的某些用户程序在内存中的位置发生了变化,此时若不对程序和数据的地址加以修改(变换),则程序必将无法执行。所以,拼接后,都必须对移动了的程序或数据进行重定位.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

catch that elf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值