内存管理的主要功能:
1.静态链接
在程序运行之前,先将各目标模块及它们所需的库函数连接成一个完整的可执行文件(装入模块),之后不再拆开。
将几个目标模块装配成一个装入模块时,需要解决的两个问题:①修改相对地址,编译后的所有目标模块都是从0开始的相对地址,当链接成一个装入模块时要修改相对地址。②变换外部调用符号,将每个模块中所用的外部调用符号也都变为相对地址。
2.装入时动态链接
将各目标模块装入内存时,边装入边链接的链接方式
3.运行时动态链接
在程序执行中需要该目标模块时,才对它进行链接。其优点是便于修改和更新,便于实现对目标模块的共享
绝对装入
装入方式只适用于单道程序环境。在编译时,如果知道程序将放到内存中的哪个位置,编译程序将产生绝对地址的目标代码。装入程序按照转入模块中的地址,将程序和数据装入内存。(编译或汇编时就将逻辑地址转化为绝对地址)
静态重定位装入
又称可重定位装入。编译、链接后的装入模块地址都是从0开始的,指令中使用的地址、数据存放的地址都是相对于初始地址而言的逻辑地址。可根据内存的当前情况,将装入模块装入到内存的适当位置。装入时对地址进行重定位
,将逻辑地址变换为物理地址(地址变换是在装入时一次完成的)
静态重定位的特点是在一个作业装入内存时,必须分配其要求的全部内存空间,如果没有足够的内存,就不能装入该作业。此外,作业在装入内存后,整个运行期间就不能在内存中移动,也不能申请内存空间
动态运行时装入/动态重定位
编译链接后的装入模块的地址都是从0开始的。装入程序把装入模块装入内存后,并不会立即把逻辑地址转化为物理地址,而是把地址转换推迟到程序真正要执行时才进行。因此装入内存后所有的地址依然是逻辑地址。这种方式需要一个重定位寄存器的支持。
动态重定位的优点:可以将程序分配到不连续的存储区;在程序运行之前可以只装入部分代码即可投入运行,然后在程序运行期间,根据需要动态申请分配内存;便于程序段的共享。
三种装入方式最本质的区别其实就是何时将逻辑地址转化为物理地址。
- 绝对装入。编译或汇编的时候就转化
- 静态重定位。程序装入内存的时候转化
- 动态重定位。程序运行的时候转化
编译后,每个目标模块都从0号单元开始编址,这称为目标模块的相对地址(或逻辑地址)。当链接程序将各个模块链接成一个完整的可执行目标程序时,链接程序顺序依次按各个模块的相对地址构成统一的从0号单元开始编址的逻辑地址空间(或虚拟地址空间),对于32位系统,逻辑地址空间的范围为0~ 2 32 − 1 2^{32} - 1 232−1。
物理地址空间是指内存中物理单元的集合,它是地址转换的最终地址,进程在运行时执行指令和访问数据,最后都要通过物理地址从主存中存取。当装入程序将可执行代码装入内存时,必须通过地址转换将逻辑地址转换为物理地址,这个过程称为地址重定位。
操作系统通过内存管理部件(MMU)将进程使用的逻辑地址转换为物理地址。逻辑地址通过页表映射到物理内存,页表由操作系统维护并被处理器引用。
不同于存放在硬盘上的可执行文件,当一个程序调入内存运行时,就构成了进程的内存映像。一个进程的内存映像一般有几个要素:
代码段和数据段在程序调入内存时就指定了大小,而堆和栈不一样。当调用malloc和free这样的C标准库函数时,堆可以在运行时动态地扩展和收缩。用户栈在程序运行期间也可以动态地进行扩展和收缩,每次调用一个函数,栈就会增长;从一个函数返回,栈就会收缩
确保每个进程都有一个单独的内存空间。内存分配前,需要保护操作系统不受用户进程的影响,同时保护用户进程不受其他用户进程的影响。内存保护可采取的两种方法:
方案一
在CPU中设置一对上、下限寄存器,用于存放用户作业在主存中的下限和上限地址,每当CPU访问一个帝制时,分别和两个寄存器的值对比,判断有无越界。
方案二
采用重定位寄存器(基地址寄存器)和界地址寄存器(限长寄存器)来实现这种保护。重定位寄存器含最小的物理地址值,界地址寄存器含逻辑地址的最大值
这两个方案的本质区别在于寄存器作用的不同:
- 方案一: 上下限寄存器都是用来和访问的地址比较的
- 方案二:基地址寄存器是用来给要访问的地址“加”的,限长寄存器是用来和逻辑地址进行比的(可以看到上图要进行内存访问时,先利用限长寄存器进行比较)
覆盖技术的思想:
将程序分为多个段(多个模块)。常用的段常驻内存,不常用的段在需要时调入内存。内存中分为一个“固定区”和若干个“覆盖区”。需要常驻内存的段放在“固定区”中,调入后就不再调出(除非运行结束);不常用的段放在“覆盖区”,需要用到时调入内存,用不到时调出内存。单一连续存储可采用覆盖技术
覆盖技术的特点是,打破了必须将一个进程的全部信息都装入主存后才能运行的限制,但当同时运行程序的代码量大于主存时任不能运行,此外,内存中能够更新的地方只有覆盖区的段,不在覆盖区中的段会常驻内存。必须由程序员声明覆盖结构,操作系统完成自动覆盖。缺点:对用户不透明。
交换技术的思想:把处于等待状态(或在CPU调度原则下被剥夺运行权利)的程序从内存移到辅存,把空间腾出来,这一过程又称为换出;把准备好竞争的CPU运行程序从辅存移到内存,这一过程称为换入。中级调度采用的就是交换技术。
在具有交换功能的操作系统中,通常把磁盘空间分为文件区和对换区两部分。文件区主要用于存放文件,主要追求存储空间的利用率,因此文件区空间的管理采用离散分配方式;对换区空间只占磁盘空间的小部分,被换出的进程数据就存放在对换区。由于对换的速度直接影响到系统的整体速度,因此对换区空间的管理主要追求换入和患处速度,因此通常对换区采用连续分配方式。对换区的I/O速度比文件区更快。
✨✨ 交换技术主要在不同进程(或作业)之间进行,而覆盖则用于同一个程序或进程中。对于主存无法存放用户程序的矛盾,现代计算机通过虚拟内存技术来解决W,覆盖技术则已称为历史;而交换技术在现代操作系统中仍具有较强的生命力。普通的交换使用的不多,但交换策略的某些变体在许多系统(如UNIX)中仍发挥作用
在单一连续分配方式中,内存被分为系统区和用户区。系统区通常位于内存的低地址部分,用于存放操作系统相关数据;用户区用于存放用户进程的相关数据。内存中只能有一道用户程序,用户程序独占整个用户区空间。
优点:实现简单;无外部碎片;可以采用覆盖技术扩充内存;不一定需要采用内存保护(早期的OC操作系统MS-DOS)
缺点:只能用于单用户、单任务的操作系统中;有内部碎片(如果分配给某进程的内存区域中,如果有些部分没有用上,就是“内部碎片”);存储器利用率极低
理解为什么这个叫单一连续分配?
因为除了系统区外所有空间都是一个用户的,这个区域的地址是连续的
固定分区分配将用户空间分割未若干个固定大小的分区,再每个分区中只装入一道作业,这样就形成了最早的、最简单的一种可运行多道程序的内存管理方式。在划分分区时有两种不同的方法:
如何避免重复分配一段内存空间、如何实现分配?
操作系统需要建立一个数据结构——分区说明表,来实现各个分区的分欸与回收。每个表项对应一个分区,通常按分区大小排列。每个表项包括对应分区的0、起始地址、状态(是否已分配)。
当用户程序要装入内存时,①由操作系统内核程序根据用户程序大小检索该表,从中找到一个能满足大小的、未分配的分区,将之分配给该程序,然后修改状态未“已分配”
优点:实现简单,无外部碎片
缺点:a.当用户程序太大时,可能所有的分区都不能满足需求,此时不得不采用覆盖技术来解决,但这又会降低性能;b.会产生内部碎片,内存利用率低。
外部****碎片指的是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。
动态分区分配又称可变分区分配,它是进程装入内存时,根据进程的时机需求,动态地为之分配内存(不预先划分内存分区),并使分区的大小正好适合进程的需要。因此,系统中分区的大小和数目是可变的。动态分配没有内部碎片,但是有外部碎片。可以通过==紧凑(拼凑,Compaction)==技术来解决外部碎片问题。
后面我们来讨论一下动态分区分配的几个问题。
通常采用空闲分区表或空闲分区链,如下图所示。
如何按照一定的动态分区算法,从空闲的分区表中选出一个分区分配给该作业。在下一小节详细讲动态分区算法。
插入操作:如下图将进程5插入到内存底部
回收内存,系统根据回收分区的始址,从空闲分区链中找到相应的从插入节点,此时可能出现四种情况:
① 回收分区与插入的前一空闲分区相邻,将这两个分区合并,并修改前一分区项的大小为两者之和;
②回收区与插入点的后一空闲分区相邻,将这两个分区合并,并修改后一分区的其实地址和大小
③回收区同时和插入点的前后、后两个空闲分区相邻,此时将这三个分区合并是,修改前一分区表项的大小为三者之和,取消后一分区表项
④回收区没有相邻的空闲分区,此时为回收区新建一个表项,填写起始地址和大小,并插入空闲分区链
解决问题若同时多个空闲分区都能满足需求时,应该选择哪个分区进行分配?
算法思想:每次都从低地址开始查找,找到第一个能满足大小的空闲分区
实现:空闲分区以地址递增的次序排列。每次分配内存时顺序查找空闲分区(或空闲分区表,找到大小能满足要求的第一个空闲分区)
首次适应算法最简单,通常也是最好和最快的。不过,首次适应算法会使得内存的低地址部分出现很多小的空闲分区,而每次分配查找时都要经过这些分区,因此增加了开销
算法思想:由于动态分区分配是一种连续分配方式,为各进程分配的空间必须是连续的一整片区域。因此为了保证当“大进程”到来时能有连续的大片空间,可以尽可能多地留下大片的空闲区,即优先使用更小的空闲分区
实现:空闲分区按容量递增次序链接。每次分配内存顺序查找空闲分区(或空闲分区表),找到大小能满足要求的第一个空闲分区。
缺点:每次都选最小的分区进行分配,会留下越来越多的、很小的、难以利用的内存块。因此这种方法会产生很多的外部碎片
又称最大适应算法(Largest Fit)
算法思想:为了解决最佳适应算法的问题——即留下太多难以利用的小碎片,可以在每次分配时优先使用最大的连续分区,这样分配后剩余的空闲区就不会太小,更方便使用。
实现:空闲分区按容量递减链接。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区
缺点:每次都选最大的分区进行分配,虽然可以让分配后留下的空闲区更大,更可用,但是这种方式会导致较大的连续空闲区被迅速用完。如果之后有“大进程”到达,就没有内存分区可用了。
算法思想:首次适应算法每次都从链头开始查找。这可能会导致低地址部分出现很多小的空闲分区,而每次分配查找时,都要经过这些分区,因此也增加了查找的开销。如果每次都从上次查找结束的位置开始检索,就能解决上述问题。
实现:空闲分区以地址递增顺序排列(可排列成一个循环链表)。每次分配内存时从上次查找结束的位置开始查找空闲分区链(或空闲分区表,找到大小能满足要求的第一个空闲分区)。
优点:使内存中的空闲分区分布得更均匀,从而减少了查找空闲分区时的开销
缺点:邻近适应算法常常导致在内存的尾部分裂成小碎片,通常比首次适应算法要差。
连续分配的方式会形成许多“碎片”,虽然可以通过紧凑的方式将许多碎片拼接成可用的空间大小,但是须付出很大的开销。所以开始考虑允许将一个进程直接分散到许多不相邻接的分区中。根据在离散分配时所分配地址空间的基本单位不同,将离散分配分为以下三种:
页面
将内存空间分为一个个大小相等的分区(比如:每个分区4KB),每个分区就是一个==“页框”(页框= 页帧 = 内存块 = 物理块= 物理页面).每个页框有一个编号,即“页框号”(页框号= 页帧号 = 内存块号 = 物理块号 = 物理页面号)==,页框号从0开始
。
将进程的逻辑地址空间分为与页框大小相等的各个部分,每个部分称为==“页”或“页面”。每个页面对应的编号即“页号”==,页号也从0开始
。
操作系统以页框为单位为各个进程分配内存空间。进程的每个页面分别放入一个页框中。也就是说,进程的页面
与内存的页框
有一一对应的关系。由于进程中最后一块往往装不满,因而会成为“页面碎片”
。
页框= 页帧 = 内存块 = 物理块 = 物理页面
区分于 页 = 页面
页面大小
这里主要讨论三种情况,页面过大、小、适合:
页面大小是2的整数幂的好处
- 逻辑地址的拆分更加迅速——如果每个页面大小为 2KB,用二进制数表示逻辑地址,则末尾 K 位 即为页内偏移量,其余部分就是页号。因此,如果让每个页面的大小为 2 的整数幂,计算机硬件就 可以很方便地得出一个逻辑地址对应的页号和页内偏移量,而无需进行除法运算,从而提升了运行速度。
- 物理地址的计算更加迅速——根据逻辑地址得到页号,根据页号查询页表从而找到页面存放的内 存块号,将二进制表示的内存块号和页内偏移量拼接起来,就可以得到最终的物理地址。
页表的始址放在页表基址寄存器(PTBR)中
为了能知道进程的每个页面再内存中存放的位置,操作系统要为每个进程建立一张页表.``页表通常存在PCB(进程控制块)中`。
页号不占用存储空间,理解为数据结构中的数组
分页存储管理的逻辑地址结构如下:
地址结构包含两个部分:前一部分为页号,后一部分为页内偏移量。在上图的例子中,地址长度为32位,其中0~11位为页内偏移量,或称为页内地址;12 ~ 32位为页号。
如果有K位表示“页内偏移量”,则说明该系统中一个也米娜的大小是2k 个内存单元
如果有M位表示“页号”,则说明在该系统中,一个进程最多只允许有2M个页面
Tips:有些奇葩题目中的页面大小不是2的整数次幂,这种情况:
页号= 逻辑地址 / 页面长度(整除)
页内偏移量 = 逻辑地址 % 页面长度(取出发的余数部分)
快表,又称联想寄存器(TLB, translation lookaside buffer),是一种访问速度比内存快很多的高速缓存(TLB不是内存,实质上是一种SRAM),用来存放最近当问的页表项的副本,可以加速地址变换的速度。与此对应,内存中的页表常称为慢表。(进程切换的时候快表的内容也会被清除)
有的系统会同时访问快表和页表,若在快表中找到则中断快表的查询
有快表时访问逻辑地址的步骤:
快表的速度比查询页表的速度快很多,因此只要快表命中,就可以节省很多时间。因为局部性上原理,一般来说快表的命中率可达90%以上
单页表存在的问题:
问题一:页表必须连续存放,因此当页表很大的时候,需要占用大量的连续页框。
问题二:没有必要让整个页表常驻内存,因为进程在一段时间内可能是需要访问某几个特定的页面
二级页表就是把页表项再分页并离散存储,然后再建立一张页表记录各个部分的存放位置,称为==页目录表,或外层页表,或称为顶层页表==
采用这种结构其实就是将一张大的连续页表拆分为了多个页表,而这多个页表还可以不同时在内存中。
二级页表的结构
如何实现地址变换
实现地址转换分为以下几步:
其实二级页表的查询和一级页表的查询只是多了一次根据一级页表查询到二级页表的过程,当然要是二级页表在内存外则需要发生缺页中断去辅存中调度二级页表
采用二级页表后,要是第二级页表不在内存中怎么办?
答:一方面采用虚拟存储技术可在页目录表中增加是否在内存中
这一列表示页面是否已经调入内存。
拓展成更高级页表
要是是分成两级页表后,页表还是很长,则可以采用更多级页表,一般来说页表的大小不能超过一个页面。这个可以这么理解,每个页表都是由上一级页指定的(也就是通过上一级页表的信息找到,第一级的则是通过PCB中的页目录始址来查找的)。这个指定一般是通过下对应的下一级页号的初始物理地址地址来来实现,所以一般只能指向一个页面,否则这个页表项的结构改变才可以做到
其实是套娃:数据是以页框为单位存储的,而这个页表项也需要用这个页框来进行存储,这个其实也就是多级页表的达到拆分页表的目的。所以除第一级页面后的页表大小应尽量等于一个页面的大小,这样子可以减少页内碎片。
另外可以辅助下图来理解:
为什么要引入分段存储管理,可以通过以下两个方面说明:
一方面是由于通常的程序都可分为若干个段,如主程序段、子程序段A……、数据段以及栈段等,每个段大多是一个独立逻辑单位;
另一方面,实现和满足信息共享、信息保护、动态链接以及信息的动态增长等需要,也都是以段为基本单位的
进程的地址空间:按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名(在低级语言中,程序员使用段名来编程),每段从0开始编址
内存分配规则:以段为单位进行分配(与分页管理的最大区别),每个段在内存中占据连续空间,但各段之间可以不相邻。段式存储会产生外部碎片
段表
程序分为多个段,各个段离散地装入内存,为了保证程序能正常运行,就必须能从物理内存中找到各个逻辑段的存放位置。为此,跟分页存储类似,需要为每个进程建立一张段映射表,简称“段表
”。
地址变换
段式管理的访存逻辑地址的过程,其实和分页管理的很累死,不一样的就是是否检验了逻辑地址中的段内地址是否合法还有分段管理中段的长度是不同的
页是信息的物理单位。分页的主要目的是实现离散分配,提高内存利用率。分页仅仅是系统管理上的需要,完全是系统行为,对用户不可见
段是信息的逻辑单元。分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑模块的信息。分段对用户是可见的,用户编程时需要显示地给出段名
页的大小固定且由系统决定,段的长度不固定,决定于用户编写的程序
分页的用户进程地址空间是一维的,程序只需给出一个记忆符即可表示一个地址。
分段的用户进程地址空间是二维的,程序员在标识一个地址时,既要给出段名,也要给出段内地址
分段比分页更容易实现信息的共享和保护。不能修改的代码成为纯代码和可重入代码(不属于临界资源),这样的代码是可以共享的。可以修改的代码是不能共享的
段一般比页大,所以段表项数目比页表项少,所需的联想存储器(快表/TLB)的大小也会相对较小,所以可以显著地减少存取数据的时间
访问一个逻辑地址需要几次访存?
优点 | 缺点 | |
---|---|---|
分页管理 | 内存空间利用率高,不会产生外部碎片,只会有少量业内碎片 | 不方便按照逻辑实现信息保护 |
分段管理 | 很方便用户按照逻辑模块实现信息共享和保护,不会产生内部碎片 | 如果段长过大,将其分配很大的连续空间很不方便。段式管理会产生外部碎片 |
✨段页式系统的基本原理是:分段和分页原理的结合,即现将用户程序分成若干个段,再把每个段分成若干个页,并为每个段赋予一个段名。会产生内部碎片。可以借助下图理解
段页式存储的逻辑地址结构
段页式存储的地址结构由:段号、段内页号、业内地址三部分组成,如下图。段号决定了成分为几个段;段内页号代表每个段内最多有多少页;业内偏移量代表页面大小、内存块大小是多少
段表、页表
每个段对应一个段表项,每个段表项由段号(虚拟存在,类似数组下标)、页表长度、页表存放块号(相应页表起始地址)组成。每个段表项长度相等,段号是隐含的。
每个页面对应一个页表项,每个页表项由页号、页面存放的内存块号组成。每个页表项长度相等,页号是隐含的
地址变换过程
段页式系统的一次访问逻辑地址的过程(这里不考虑TLB):
可以借助下图理解上述过程。可见若不使用TLB技术,段页式系统一次访问逻辑地址需要三次访存操作,两次次越界检查操作。
小结