【操作系统基础】内存管理(二)

本文接上文,主要讲解操作系统内存分配与回收的几种方式。

三、内存的分配与回收

1. 单一连续分配

在单一连续分配方式中,内存被分为系统区和用户区。系统区通常位于内存的低地址部分,用于存放操作系统相关数据;用户区用于存放用户进程相关数据。内存中只能有一道用户程序,用户程序独占整个用户区空间。

【操作系统基础】内存管理(二)_第1张图片

优点:实现简单;无外部碎片;可以采用覆盖技术扩充内存;不一定需要采取内存保护(eg:早期的 PC 操作系统 MS-DOS)。

缺点:只能用于单用户、单任务的操作系统中;有内部碎片;存储器利用率极低。

这里简单解释一下内存碎片,即分配给某进程的内存区域中,如果有些部分没有用上,就是“内部碎片”。

2. 固定分区分配

为了能在内存中装入多道程序,且这些程序之间又不会相互干扰,于是将整个用户空间划分为若干个固定大小的分区,在每个分区中只装入一道作业,这样就形成了最早的、最简单的一种可运行多道程序的内存管理方式。固定分区分配又可分为分区大小相等以及分区大小不想等两种方式

  • 分区大小相等:缺乏灵活性,但是很适合用于用一台计算机控制多个相同对象的场合(比如:炼钢厂有 n 个相同的炼钢炉,就可以把内存分为 n 个大小相等的区域存放 n 个炼钢炉控制程序)

  • 分区大小不等:增加灵活性,可以满足不同大小的进程需求。根据常在系统中运行的作业大小情况进行划分(比如:划分多个小分区、适量中等分区、少量大分区)

【操作系统基础】内存管理(二)_第2张图片

操作系统需要建立一个数据结构——分区说明表,来实现各个分区的分配与回收。每个表项对应一个分区,通常按分区大小排列。每个表项包括对应分区的大小、起始位置、状态(是否已分配)。当用户程序要装入内存时,由操作系统内核程序根据用户程序大小检索该表,从中找到一个能满足大小的、未分配的分区,将之分配给该程序,然后修改状态为“已分配”。

优点:实现简单,无外部碎片

缺点:

  1. 当用户程序太大时,可能所有的分区都不能满足需求,此时不得不采用覆盖技术来解决,但这又会降低性能;
  2. 会产生内部碎片,内存利用率低。

3. 动态分区分配

动态分区分配又称可变分区分配。这种分配方式不会预见划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的。系统用空闲分区表或空闲分区链的数据结构记录内存的适用情况:

【操作系统基础】内存管理(二)_第3张图片

把一个新作业装入内存时,须按照一定的动态分区分配算法,从空闲分区表(或空闲分区链)中选出一个分区分配给该作业。

动态分区分配算法:在动态分配方式中,当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配?

算法 算法思想 分区排列顺序 优点 缺点
首次适应 从头到尾找合适的分区 空闲分区以地址递增次序排列 综合看性能最好。算法开销小,回收分区后一般不需要对空闲分区队列重新排序
最佳适应 优先使用更小的分区,以保留更多大的分区 空闲分区以容量递增次序排列 会有更多的大分区被保留下来,更能满足大进程需求 会产生很多太小的、难以利用的碎片;算法开销大,回收分区后可能需要对空闲分区队列重新排序
最坏适应 优先使用更大的分区,以防止产生太小的不可用碎片 空闲分区以容量递减次序排列 可以减少难以利用的小碎片 大分区容易被用完,不利于大进程;算法开销大(原因同上)
邻近适应 由首次适应演变而来,每次从上次查找结束位置开始查找 空闲分区以地址递增次序排序(可排列成循环链表) 不用每次都从低地址的小分区开始检索。算法开销小(原因同首次适应算法) 会使高地址的大分区也被用完。

动态分区没有内部碎片,但是有外部碎片。

内部碎片,分配给某进程的内存区域中,如果有些部分没有用上。外部碎片,是指内存中的某些空闲分区由于太小难以利用。

如果内存中空闲空间的总和本来可以满足某进程的要求,但由于进程需要的是一整块连续的内存空间,因此这些“碎片”不能满足进程的需求。可以通过紧凑(拼凑,Compaction)技术来解决外部碎片。

4. 基于分页存储管理

4.1 什么是分页存储

将内存空间分为一个个大小相等的分区(比如每个分区4 kb),每个分区就是一个"页框"(页框=页帧=内存块=物理块=物理页面)。每个页框有一个编号,即"页框号",页框号从 0 开始;将进程的逻辑地址空间也分为页框大小相等的一个个部分,每个部分称为一个"页"或者"页面",每个页面也有一个编号,即"页号",页号也是从 0 开始。

操作系统以页框为单位为各个进程分配内存空间。进程的每个页面分别放入到一个页框中。也就是说,进程的页面与内存的页框有一一对应的关系。各个页面不必连续存放,可以放到不相邻的各个页框中。

4.2 重要的数据结构——页表

为了能知道进程的每个页面在内存中存放的位置,操作系统要为每个进程建立一张页表。页表通常存在于PCB(进程控制快)中。

【操作系统基础】内存管理(二)_第4张图片

这里还有几点需要补充的:

  • 一个进程对应一张页表;
  • 进程的每个页面对应一个页表项;
  • 每个页表项由“页号”和“块号”组成;
  • 页表记录进程页面和实际存放的内存块之间的隐射关系。

4.3 基本地址变换结构

基本地址变换结构是一组用于“实现逻辑地址到物理地址转换”的硬件机构。基本地址变换机构可以借助进程的页表将逻辑地址转换为物理地址。通常会在系统中设置一个页表寄存器(PTR),存放页表在内存中的起始地址 F 和页表长度 M。进程未执行时,页表的起始地址和页表长度放在进程控制块(PCB)中,当进程被调度时,操作系统内核会把它们放到页表寄存器中。注意:页面大小是二的整数幂。

设页面大小为L,逻辑地址A到物理地址E的变换过程如下:

【操作系统基础】内存管理(二)_第5张图片

  1. 计算页号 P 和页内偏移量 W(如果用十进制数手算,则 P=A/L,W=A%L;但是在计算机实际运行时,逻辑地址结构是固定不变的,因此计算机硬件可以更快地得到二进制表示的页号、页内偏移量);
  2. 比较页号 P 和页表长度 M,若 P >= M,则产生越界中断,否则继续执行。(注意:页号是从0开始的,而页表长度至少是1,因此 P=M 时也会越界);
  3. 页表中页号 P 对应的页表项地址 = 页表起始地址F + 页号P * 页表项长度,取出该页表项内容 b,即为内存块号。(注意区分页表项长度、页表长度、页面大小的区别。页表长度指的是这个页表中总共有几个页表项,即总共有几个页;页表项长度指的是每个页表项占多大的存储空间;页表大小指的是一个页面占多大的存储空间);
  4. 计算 E = b * L + W,用得到的物理地址 E 去访存。(如果内存块号、页面偏移量是用二进制表示的,那么把二者拼接起来就是就是最终的物理地址了)。

4.4 具有快表的地址变化机构

在介绍快表之前我们需要了解局部性原理,局部性原理分为时间局部性和空间局部性:

  • 时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某个数据被访问过,不久之后该数据很可能再次被访问。(因为程序中存在大量循环);
  • 空间局部性:一旦程序访问某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的)。

上一小节的基本地址变换结构中,每次要访问一个逻辑地址,都需要查询内存中的页表,由于局部性原理,可能连续很多次查到的都是同一个页表项。既然如此,就利用这个特性减少访问页表的次数,由此引入快表。

快表,又称联想寄存器(TLB,translation lookaside buffer),是一种访问速度比内存快很多的高速缓存(TLB 不是内存!),用来存放最近访问的页表项的副本,可以加速地址变换的速度,与此对应,内存中的页表常称为慢表。

【操作系统基础】内存管理(二)_第6张图片

引入快表后,地址的变换过程:

  1. CPU 给出逻辑地址,由某个硬件算得页号、页内偏移量,将页号与快表中的所有页号进行比较。
  2. 如果没有匹配的页号,说明要访问的页表项在快表中有副本,则直接从中取出该页对应的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此,若没有快表命中,则访问某个逻辑地址仅需一次访存即可。
  3. 如果没有找到匹配的页号,则需要访问内存中的页表,找到对应页表项,得到页面存放的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此,若快表未命中,则访问某个逻辑地址需要两次访存(注意:在找到页表项后,应同时将其存入块表,以便后面可能的再次访问。但若快表已满,则必须按照一定的算法对旧的页表项进行替换)

由于查询快表的速度比查询页表的速度块很多,因此只要快表命中,就可以节省很多时间。因为局部性原理,一般来说快表的命中率可以达到 90% 以上。

4.5 两级页表

单级页表存在的问题:

  • 页表的项必须连续存放,因此当页表很大时,需要占用很多个连续的页框;
  • 没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定页面。

对于上述问题,我们可将长长的页表进行分组,使每个内存块刚好可以放入一个分组。另外要为离散分配的页表再建立一张页表,称为称为页目录表(也称外层页表或顶层页表)。

【操作系统基础】内存管理(二)_第7张图片

5. 基于分段存储管理

5.1 什么是分段

将进程的地址空间按照程序自身逻辑关系划分为若干段,每个段都有一个段名(在低级语言中,程序员使用段名来编程),每段从0开始编址。内存的分配原则是以段为单位进行分配,每个段在内存中占据连续空间,但各段之间可以不相邻。

【操作系统基础】内存管理(二)_第8张图片
分段系统中的逻辑地址由段号(段名)和段内地址(段内偏移量)所组成。如:

在这里插入图片描述

段号的位数决定了每个进程最多可以分为多少段,段内地址位数决定了每个段的最大长度是多少;在上述例子中,若系统是按字节寻址的,段号占16位,因此在该系统中,每个进程最多有 2^16 = 64 k 个段,段内地址占16位,因此每个段的最大长度是 2^16 = 64 KB。

5.2 段表

程序分为多个段,各段离散地装入内存,为了保证程序能正常运行,就必须从物理内存中找到各个逻辑段的存放位置。为此,需为每个进程建立一张映射表,也即“段表”。

【操作系统基础】内存管理(二)_第9张图片

每个段对应一个段表项,其中记录了该段在内存中的起始地址(又称基址)和段长度。各个段表项的长度是相同的。

5.3 分段与分页对比

页是信息的物理单位。分页的主要目的是为了实现离散分配。提高内存利用率。分页仅仅是系统管理上的需要,完全是系统行为,对用户不可见;

段是信息的逻辑单位。分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑块的信息。分段对用户可见,用户编程需要显式地给出段名。

页的大小固定且由系统决定。段的长度却不固定,决定于用户编写的程序。

分页的用户进程地址空间是一维的,程序员只需给出一个记忆符即可表示一个地址;

分段的用户进程地址空间是二维的,程序员在标识一个地址时,既要给出段名,也要给出段内地址。

分段比分页更容易实现信息的共享和保护;不能被修改的代码称为纯代码或可重入代码(不属于临界资源),这样的代码是可以共享的。可修改的代码是不能共享的(比如,有一个代码段中有很多变量,各进程并发地同时访问可能造成数据不一致)。

访问一个逻辑地址需要几次访存?

分页(单级页表):第一次访存——查内存中的页表,第二次访存——访问目标内存单元。总共两次访存;

分段:第一次访存——查内存中的段表,第二次访存——访问目标内存单元。总共两次访存与分页系统类似,分段系统中也可以引入快表机构,将近期访问过的段表项放入快表中,这样可以减少一次访问,加快地址变换速度。

6. 段页式存储管理

段页式存储管理将进程按逻辑模块分段,再将各段分页(如每个页面 4 KB),再将内存空间分为大小相同的内存块(页框/页帧/物理块)。

【操作系统基础】内存管理(二)_第10张图片

6.1 段页式管理的逻辑地址结构

分段系统的逻辑地址结构由段号和段内地址(段内偏移量)组成,而段页式系统的逻辑地址结构由段号、页号、业内地址(页内偏移量)组成,如:

【操作系统基础】内存管理(二)_第11张图片

段号的位数决定了每个进程最多可以分几个段,页号位数决定了每个段最大有多少页,页内偏移量决定了页面大小、内存块大小是多少。

在上述图中,若系统是按字节寻址的,则段号占16位,因此在该系统中,每个进程最多有 2^16 = 64 k 个段,页号占 4 位,因此每个段最多有 2^4 = 16 页,页内偏移量占 12 位,因此每个页面大小为 2^12 = 4 KB。

6.2 段表、页表

每个段对应一个段表项,每个段表项由段号、页表长度、页表存放块号(页表起始地址)组成。每个段表项长度相等,段号是隐含的。每个页面对应一个页表项,每个页表项由页号、页面存放的内存块号组成。每个页表项长度相等,页号是隐含的。

【操作系统基础】内存管理(二)_第12张图片

你可能感兴趣的:(操作系统基础)