目录
1. 内存管理
思考问题?
1. 为什么要进行内存管理?2. 页式管理中每个页表项大小的下限如何决定?
3. 多级页表解决了什么问题?又会带来什么问题?
什么是内存?内存有何作用?
像我们买手机的时候,我们通常都会选择版本,一般有 8+64、8+128、8+256 等等,随着我们科技水平的发展不断更新,这里 8GB 指的就是内存,+ 的 64GB、128GB、256GB 就是外存。举个简单的例子理解:内存是指我们想要运行一个进程,或者说是一个APP,必须把该APP放进内存才能运行,外存上存放的是我们已有的进程,但是没有被执行,也可以简单的说是我们手机桌面上存放的图标; 有这样一个明显的现象:打个比方,8GB 内存可以使我们运行 5 个软件,这 5 个软件是被调入内存中运行的;倘若我们想要运行第 6 个软件,操作系统就会通过某种置换算法将其中一个软件换出内存,由第 6 个软件换入内存;我们玩手机时后台运行多个软件时,有些软件再次被启动时可能会出现转圈等待或者重新启动的现象,这就是内存进程的换入/换出过程;
内存可存放数据。程序执行前需要先放到内存中才能被 CPU 处理 —— 我们的未运行的程序都是放在外存的,但是外存的读写速度比较慢,而 CPU 运行又比较快,这样,如果 CPU 去运行外存上的程序,那么 CPU 的速度会被外存所拖累;所以内存缓和了 CPU 与硬盘之间的矛盾;
那么在多道程序运行的环境下,系统中会有多个进程并发执行,通俗点说会有多个程序的数据同时放到内存中。那么,如何区分每个程序的数据是放在什么地方的呢?这样就需要对内存进行编址,定义连续的地址;
就比如说我们住酒店的时候,茫茫多的房间,我们就是通过房间号来找到我们对应的房间的;
几个常用的数量单位:一台手机/电脑有 4GB 内存:
这里的 G 表示内存单位,常用的单位还有 1K(千) =
;1 M(兆,百万) =
;1 G(十亿,千兆) =
;
这里的 B 表示存储单位,大写的 B 表示 Byte 字节,小写的 b 表示比特位 bit ;也就是 1B = 8 b;
也就是说 4GB 的内存可以存放 4 *
个地址空间,如果是按字节编址的话,也就是有 4 *
=
个地址空间;地址需要 32 个二进制位来表示(0 ~
- 1);
指令的工作原理:接下来我们思考,如果我们这个进程不是从地址 #0 开始存放的,会影响指令的正常执行吗?
那么非常重要的就是:如何把逻辑地址转换成物理地址?
绝对装入:在编译时,如果知道程序将放到内存中的哪个位置,编译程序将产生绝对地址的目标代码。装入程序按照装入模块中的地址,将程序和数据装入内存。
比如说上述的例子,如果编译时就知道模块要从地址 100 的地方开始存放……
那么编译时就将指令 0 和指令 1 改为在地址为 179 的存储单元上进行操作。
静态重定位(可重定位装入):编译、链接后的装入模块的地址都是从 0 开始的,指令中使用的地址、数据存放的地址都是相对于起始地址而言的逻辑地址。可根据内存的当前情况,将装入模块装到内存的适当位置。装入时对地址进行 “重定位”,将裸机地址变换为物理地址(地址变换是在装入时一次完成的)。
比如说上述的例子,编译、链接后的程序都是在地址为 79 的存储单元上进行操作,但是在装入时对地址进行重定位操作,装入的起始物理地址为 100 ,则所有地址相关的参数都 +100;指令 0 就会变为往地址为 179 的存储单元中写入 10 ;静态重定位的特点是在一个作业装入内存时,必须分配其要求的全部内存空间,如果没有足够的内存,就不能装入该作业。而且作业一旦进入内存中,在运行期间是不能再移动的;
动态重定位(动态运行时装入):编译、链接后的装入模块的地址都是从 0 开始的。装入程序把装入模块装入内存后,并不会立即把逻辑地址转换为物理地址,而是把地址推迟到程序真正要执行时才进行。因此装入内存后所有的地址依然是逻辑地址。这种方式需要一个重定位寄存器的支持。
重定位寄存器中记录着:存放装入模块存放的起始地址。
比如说上述的例子:重定位寄存器记录着装入模块的起始地址为 100 ,那么 CPU 在运行时会把指令中的地址加上重定位寄存器中的地址,CPU 将会去读取加和之后得到的地址;
从写程序到程序运行的过程:
链接的三种方式:
静态链接:在程序运行之前,先将各自目标函数及它们所需的库函数链接成一个完整的可执行文件,之后不再拆开;
就是上述例子中的过程;
装入时动态链接:将各目标模块装入内存时,边装入边链接的链接方式。
也就是程序运行之前,先不把目标模块进行链接,而是目标模块进入内存后,再对目标模块进行链接,一边装入内存一边链接;
运行时动态链接:在程序执行中需要该目标模块时,才对它进行链接。其优点是便于修改和更新,便于实现对目标模块的共享。
就是说目标模块不是一起进入内存的,有可能是 main 文件先进入内存,然后 main 文件运行过程中有需要 a 文件,然后再将 a 文件调入至内存,再调入 a 文件之后,再将 a 文件和 main 文件链接到一起;
1.1 内存管理的概念
1.1.1 内存管理的基本原理和要求
内存管理(Memory Management)是操作系统设计中最重要和最复杂的内容之一。虽然计算机硬件技术一直在飞速发展,内存容量也在不断增大,但仍然不可能将所有用户进程和系统所需要的全部程序和数据放入主存,因此操作系统必须对内存空间进行合理的划分和有效的动态分配。操作系统对内存的划分和动态分配,就是内存管理的概念。
有效的内存管理在多道程序设计中非常重要,它不仅可以方便用户使用存储器、提高内存利用率,还可以通过虚拟技术从逻辑上扩充存储器。
内存管理的主要功能有:
- 内存空间的分配与回收。由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率。
- 地址转换。在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。
- 内存空间的扩充。利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。
- 内存共享。指允许多个进程访问内存的同一部分。例如,多个合作进程可能需要访问同一块数据,因此必须支持对内存共享区域进行受控访问。
- 存储保护。保证各道作业在各自的存储空间内运行,互不干扰。
在进行具体的内存管理之前,需要了解进程运行的基本原理和要求。
1.1.1.1 程序的链接和装入
创建进程首先要将程序和数据装入内存。将用户源程序变为可在内存中执行的程序,通常需要以下几个步骤:
- 编译。由编译程序把用户源代码编译成若干目标模块。
- 链接。由链接程序将编译后形成的一组目标模块及它们所需的库函数链接在一起,形成一个完整的装入模块。
- 装入。由装入程序将装入模块装入内存运行。
程序的链接有以下三种方式:
1. 静态链接(在编译之前)
在程序运行之前,先将各目标模块及它们所需的库函数链接成一个完整的装配模块,以后不再拆开。将几个目标模块装配成一个装入模块时,需要解决两个问题:
① 修改相对地址,编译后的所有目标模块都是从 0 开始的相对地址,当链接成一个装入模块时要修改相对地址。
② 变换外部调用符号,将每个模块中所用的外部调用符号也都变换为相对地址。
2. 装入时动态链接(在编译之后)
将用户源程序编译后所得到的一组目标模块,在装入内存时,采用边装入边链接的方式。其优点是便于修改和更新,便于实现对目标模块的共享。
3. 运行时动态链接(在内存上运行时)对某些目标模块的链接,是在程序执行中需要该目标模块时才进行的。凡在执行过程中未被用到的目标模块,都不会被调入内存和被链接到装入模块上。其优点是能加快程序的装入过程,还可节省大量的内存空间。
内存的装入模块在装入内存时,同样有以下三种方式:
1. 绝对装入
绝对装入方式只适用于单道程序环境。在编译时,若知道程序将驻留在内存的某个位置,则编译程序将产生绝对地址的目标代码。绝对装入程序按照装入模块中的地址,将程序和数据装入内存。由于程序中的逻辑地址与实际内存地址完全相同,因此不需对程序和数据的地址进行修改。另外,程序中所用的绝对地址,可在编译或汇编时给出,也可由程序员直接赋予。而通常情况下在程序中采用的是符号地址,编译或汇编时再转换成绝对地址。
2. 可重定位装入
在多道程序环境下,多个目标模块的起始地址通常都是从 0 开始的,程序中的其他地址都是相对于起始地址的,此时应采用可重定位装入方式。根据内存的当前情况,将装入模块装入内存的适当位置。在装入时对目标程序中指令和数据地址的修改过程称为重定位,又因为地址变换通常是在进程装入时一次完成的,故称为静态重定位。当一个作业装入内存时,必须给它分配要求的全部内存空间,若没有足够的内存,则无法装入。此外,作业一旦进入内存,整个运行期间就不能在内存中移动,也不能再申请内存空间。
3. 动态运行时装入
也称为动态重定位。程序在内存中若发生移动,则需要采用动态的装入方式。装入程序把装入模块装入内存后,并不立即把装入模块中的相对地址转换成绝对地址,而是把这种地址转换推迟到程序真正要执行时才进行。因此,装入内存后的所有地址均为相对地址。这种方式需要一个重定位寄存器的支持。动态重定位的优点:
可以将程序分配到不连续的存储区;
在程序运行之前可以只装入部分代码即可投入运行,然后在程序运行期间,根据需要动态申请分配内存;
便于程序段的共享。
1.1.1.2 逻辑地址与物理地址
编译后,每个目标模块都从 0 号单元开始编址,则称为该目标模块的相对地址(或逻辑地址)。当链接程序将各个模块链接成一个完整的可执行目标程序时,链接程序顺序依次按各个模块的相对地址构成统一的从 0 号单元开始编址的逻辑地址空间(或虚拟地址空间),对于 32 位系统,逻辑地址空间的范围为 0 ~ - 1。进程在运行时,看到和使用的地址都是逻辑地址。用户程序和程序员只需要知道逻辑地址,而内存管理的具体机制则是完全透明的。不同进程可以有相同的逻辑地址,因为这些相同的逻辑地址可以映射到主存的不同位置。
物理地址空间是指内存中物理单元的集合,它是地址转换的最终地址,进程在运行时执行指令和访问数据,最后都要通过物理地址从主存中存取。当装入程序将可执行代码装入内存时,必须通过地址转换将逻辑地址转换成物理地址,这个过程称为地址重定位。
操作系统通过内存管理部件(MMU)将进程使用的逻辑地址转换为物理地址。进程使用虚拟内存空间中的地址,操作系统在相关硬件的协助下,将它 “转换” 成真正的物理地址。逻辑地址通过页表映射到物理地址,页表由操作系统维护并被处理器引用。
1.1.1.3 进程的内存映像
不同于存放在硬盘上的可执行程序文件(.exe),当一个程序调入内存运行时,就构成了进程的内存映射。一个进程的内存映像一般有几个要素:
- 代码段:即程序的二进制代码,代码段是只读的,可以被多个进程共享。
- 数据段:即程序运行时加工处理的对象,包括全局变量和静态变量。
- 进程控制块(PCB):存放在系统区。操作系统通过 PCB 来控制和管理进程。
- 堆:用来存放动态分配的变量。通过调用 malloc 函数动态地向高地址分配空间。
- 栈:用来实现函数调用。从用户空间的最大地址往低地址方向增长。
代码段和数据段在程序调入内存时就指定了大小,而堆和栈不一样。当调用像 malloc 和 free 这样的 C 标准库函数时,堆可以在运行时动态地扩展和收缩。用户栈和程序运行期间也可以动态地扩展和收缩,每次调用一个函数,栈就会增长;从一个函数返回时,栈就会收缩。
下图是一个进程在内存中的映像。其中,共享库用来存放进程用到的共享函数库代码,如 printf() 函数等。在只读代码段中,.init 是程序初始化时调用的 _init 函数;.text 是用户程序的机器代码;.rodata 是只读数据。在读/写数据段中,.data 是已初始化的全局变量和静态变量;.bss 是未初始化及所有初始化为 0 的全局变量的静态变量。
1.1.1.4 内存保护
确保每个进程都有一个单独的内存空间。内存分配前,需要保护操作系统不受用户进程的影响,同时保护用户进程不受其他用户进程的影响。内存保护可以采取两种方法:
1. 在 CPU 中设置一对上、下限寄存器,存放用户作业在主存中的下限和上限地址,每当 CPU 要访问一个地址时,分别和两个寄存器的值相比,判断有无越界。
2. 采用重定位寄存器(又称基地址寄存器)和界地址寄存器(又称限长寄存器)来实现这种保护。重定位寄存器含最小的物理地址值,界地址寄存器含逻辑地址的最大值。内存管理机构动态地将逻辑地址与界地址寄存器进行比较,若未发生地址越界,则加上重定位寄存器的值后映射成物理地址,在送交内存单元:
实现内存保护需要重定位寄存器和界地址寄存器,因此要注意两者之间的区别。重定位寄存器是用来 “加” 的,逻辑地址加上重定位寄存器中的值就能得到物理地址;界地址寄存器是用来 “比” 的,通过比较界地址寄存器中的值与逻辑地址的值来判断是否越界。
加载重定位寄存器和界地址寄存器时必须使用特权指令,只有操作系统内核才可以加载这两个寄存器。这种方案允许操作系统内核修改这两个寄存器的值,而不允许用户程序修改。
1.1.1.5 内存共享
并不是所有的进程内存空间都适合共享,只有那些只读的区域才可以共享。可重入代码又称纯代码,是一种允许多个进程同时访问但不允许被任何进程修改的代码。但在实际执行时,也可以为每个进程配以局部数据区,把在执行中可能改变的部分复制到该数据区,这样,程序在执行时只需对该私有数据区的内存进行修改,并不去改变共享的代码。
举个例子来看内存共享的实现方式:
考虑一个可以同时容纳 40 个用户的多用户系统,它们同时执行一个文本编辑程序,若该程序有 160KB 代码区和 40KB 数据区,则共需要 160*40+40*40=8000KB 的内存空间来支持 40 个用户。如果 160KB 代码是可分享的纯代码,则不论是在分页系统还是在分段系统中,整个系统只需保留一份副本即可,此时所需的内存空间仅为 40*40+160=1760KB。对于分页系统,假设页面大小为 4KB,则代码区占用 40 个页面、数据区占用 10 个页面。为了实现代码共享,应在每个进程的页表中都建立 40 个页表项,它们都指向共享代码区的物理页号。此外,每个进程还要为自己的数据区建立 10 个页表项,指向私有数据区的物理页号。
对于分段系统,由于是以段为分配单位的,不管该段有多大,都只需为该段设置一个段表项(指向共享代码段始址,以及段长 160KB)。
1.1.1.6 内存分配与回收
存储管理方式随着操作系统的发展而发展。在操作系统由单道向多道发展时,存储管理方式便由单一连续分配发展为固定分区分配。为了能更好的适应不同大小的程序要求,又从固定分区分配发展到动态分区分配。为了更好的提高内存利用率,进而从连续分配方式发展到离散分配方式——页式存储管理。引入分段存储管理的目的,主要是为了满足用户在编程和使用方面的要求,其中某些要求是其他几种存储管理方式难以满足的。
1.2 覆盖与交换
覆盖与交换技术是在多道程序环境下用来扩充内存的两种方法。
1.2.1 覆盖
早期的计算机系统中,主存容量很小,虽然主存中仅存放一道用户程序,但存储空间放不下用户进程的现象也经常发生,这一矛盾可以用覆盖技术来解决。
覆盖的基本思想如下:由于程序运行时并非任何时候都要访问程序及数据的各个部分(尤其是大程序),因此可把用户空间分成一个固定区和若干覆盖区。将经常活跃的部分放在固定区,其余部分按调用关系分段。首先将那些即将要访问的段放入覆盖区,其他段放在外存中,在需要调用前,系统再将其调入覆盖区,替换覆盖区中原有的段。
覆盖技术的特点是:打破了必须将一个进程的全部信息装入内存后才能运行的限制,但当同时运行程序的代码量大于主存时仍不能运行,此外,内存中能够更新的地方只有覆盖区的段,不在覆盖区中的段会常驻内存。覆盖技术对用户和程序员不透明。
1.2.2 交换
交换(对换)的基本思想是:把处于等待状态(或在 CPU 调度原则下被剥夺运行权利)的程序从内存移到辅存,把内存空间腾出来,这一过程又称换出;把准备好竞争 CPU 运行的程序从辅存移到内存,这一过程又称换入。
比如说:有一个 CPU 采用时间片轮转调度算法的多道程序环境。时间片到,内存管理器将刚刚执行过的进程换出,将另一个进程换入刚刚释放的内存空间。同时,CPU 调度器可以将时间片分配给其他已在内存中的进程。每个进程用完时间片都与另一进程交换。在理想情况下,内存管理器的交换过程速度足够快,总有进程在内存中可以执行。
有关交换,注意以下几个问题:
- 交换需要备份存储,通常是磁盘。它必须足够大,并提供对这些内存映像的直接访问。
- 为了有效使用 CPU,需要使每个进程的执行时间比交换时间长。
- 若换出进程,则必须确保该进程完全处于空闲状态。
- 交换空间通常作为磁盘的一整块,且独立于文件系统,因此使用起来可能很快。
- 交换通常在由许多进程运行且内存空间吃紧时开始启动,而在系统负荷降低时就暂停。
- 普通的交换使用不多,但交换策略的某些变体在许多系统中仍发挥作用。
交换技术主要在不同进程(或作业)之间进行,而覆盖则用于同一程序或进程中。
1.3 连续分配管理方式
连续分配管理方式是指为一个用户程序分配一个连续的内存空间。
比如说, 某用户需要 100MB 的内存空间,连续分配方式就在内存空间中为用户分配一块连续的 100MB 空间。连续分配方式主要包括单一连续分配、固定分区分配和动态分区分配。
1.3.1 单一连续分配
内存在此方式下分为系统区和用户区,系统区仅供操作系统使用,通常在低地址部分;在用户区内存中,仅有一道用户程序,即整个内存的用户空间由该程序独占。
这种方式的优点是简单、无外部碎片,无须进行内存保护,因为内存中永远只有一道程序。缺点是只能用于单用户、单任务的操作系统中,有内部碎片,存储器的利用率极低。
但是,如果内存中用户区的程序比较小,用户区中还有很大一部分处于空闲状态,此时页无法写入其他程序,内存利用率低;分配给某进程的内存区域中,如果有些部分没有用上,就是 “内部碎片”;
1.3.2 固定分区分配
固定分区分配是最简单的一种多道程序存储管理方式,它将用户内存空间划分为若干固定大小的区域,每个分区只装入一道作业。当有空闲分区时,便可再从外存的后备作业队列中选择适当大小的作业装入该分区,如此循环。在划分分区时有两种不同的方法:
- 分区大小相等。程序太小会造成浪费,程序太大又无法装入,缺乏灵活性。
- 分区大小不等。划分为多个较小的分区、适量的中等分区和少量大分区。
为便于内存分配,通常将分区按大小排队,并为之建立一张分区说明表,其中各表项包括每个分区的始址、大小及状态;如下图所示:
当有用户程序要装入时,便检索该表,以找到合适的分区给与分配并将其状态置为 “已分配” ;未找到合适分区时,则拒绝为该程序分配内存。
这种方式存在两个问题:一是程序可能太大而放不进任何一个分区,这时就需要使用覆盖技术来使用内存空间;二是当程序小于固定分区大小时,也要占用一个完整的内存分区,这样分区内存就存在空间浪费,这种现象称为内部碎片。固定分区是可用于多道程序设计的最简单的存储分配,无外部碎片,但不能实现多进程共享一个主存区,所以存储空间利用率低。
在固定分区分配中,为了便于分配,建立一张分区使用表,通常按分区大小排队,各表项包括每个分区的起始地址、大小及状态(是否已分配)。分配内存时,检索分区使用表,找到一个能满足要求且尚未分配的分区分配给装入程序,并将对应表项的状态置为 “已分配” ;若找不到这样的分区,则拒绝分配。回收内存时,只需将对应表项的状态置为 “未分配” 即可。
1.3.3 动态分区分配
又称可变分区分配,它是在进程装入内存时,根据进程的实际需要,动态地为之分配内存,并使分区的大小正好适合进程的需要。因此,系统中分区的大小和数目是可变的。
如下图所示:系统中有 64MB 内存空间,其中低 8MB 固定分配给操作系统,其余为用户可用内存。开始时装入前三个进程,它们分别分配到所需的空间后,内存仅剩 4MB,进程 4 无法装入。
在某个时刻,内存中没有一个就绪进程,CPU 出现空闲,操作系统就换出进程 2,换入进程 4。由于进程 4 比进程 2 小,这样在主存中就产生了一个 6MB 的内存块。之后 CPU 又出现空闲,需要换入进程 2 ,而主存无法容纳进程 2 ,操作系统就换出进程 1,换入进程 2。
动态分区在开始时是很好的,但随着时间的推移,内存中会产生越来越多小的内存块,内存的利用率也随之下降。这些小的内存块称为外部碎片,它存在于所有分区的外部,这与固定分区中的内部碎片正好相对。克服外部碎片可以通过紧凑技术来解决,即操作系统不时地对进程进行移动和整理。但这需要动态重定位寄存器的支持,且相对费时。紧凑的过程实际上类似于 Windows 系统中的磁盘碎片整理程序,只不过后者是对外存空间的紧凑。
在进程装入或换入内存时,若内存中有多个足够大的空闲块,则操作系统必须确定分配哪个内存块给进程使用,这就是动态分区的分配策略。考虑以下几种算法:
1. 首次适应(First Fit)算法。空闲分区以地址递增的次序链接。分配内存时,从链首开始顺序查找,找到大小能满足要求的第一个空闲分区分配给作业。
首次适应算法最简单,通常也是最好和最快的。不过,首次适应算法会使得内存的低地址部分出现很多小的空闲分区,而每次分配查找时都要经过这些分区,因此增加了开销。
2. 邻近适应(Next Fit)算法。又称循环首次适应算法,由首次适应算法演变而成。不同之处是,分配内存时从上次查找结束的位置开始继续查找。
邻近适应算法试图解决这个问题。但它常常导致在内存空间的尾部(因为在一遍扫描中,内存前面部分使用后再释放时,不会参与分配)分裂成小碎片。通常比首次适应算法要差。
3. 最佳适应(Best Fit)算法。空闲分区按容量递增的次序形成空闲分区链,找到第一个能满足要求且最小的空闲分区分配给作业,避免 “大材小用” 。
最佳适应算法虽然称为 “最佳”,但是性能通常很差,因为每次最佳的分配会留下很小的难以利用的内存块,会产生最多的外部碎片。
4. 最坏适应(Worst Fit)算法。空闲分区以容量递减的次序链接,找到第一个能满足要求的,即最大的分区,从中分割一部分存储空间给作业。
最坏适应算法与最佳适应算法相反,它选择最大的可用块,这看起来最不容易产生碎片,但是却把最大的连续内存划分开,会很快导致没有可用的大内存块,因此性能也非常差。
在动态分区分配中,与固定分区分配类似,设置一张空闲分区链(表),并按始址排序。分配内存时,检索空闲分区链,找到所需的分区,若其大小大于请求大小,便从该分区中按请求大小分割一块空间分配给装入进程(若剩余部分小到不足以划分,则无须分割),余下部分仍留在空闲分区链中。
回收内存时,系统根据回收内存的始址,从空闲分区链中找到相应的插入点,此时可能出现四种情况:① 回收区与插入点的前一空闲分区相邻,将这两个分区合并,并修改前一分区表项的大小为两者之和;② 回收区与插入点的后一空闲分区相邻,将这两个分区合并,并修改后一分区表项的始址和大小;③ 回收区同时与插入点的前、后两个分区相邻,此时将这三个分区合并,修改前一分区表项的大小为三者之和,取消后一分区表项;④ 回收区没有相邻的空闲分区,此时应为回收区新建一个表项,填写始址和大小,并插入空闲分区链。
以上三种内存分区管理方法有一个共同特点,即用户程序在主存中都是连续存放的。
在连续分配方式中,我们发现,即使内存有超过 1GB 的空闲空间,但若没有连续的 1GB 空间,则需要 1GB 空间的作业仍然是无法运行的;但若采用非连续分配方式,则作业所要求的 1GB 内存空间可以分散地分配在内存的各个区域,当然,这也需要额外的空间去存储它们(分散区域)的索引,使得非连续分配方式的存储密度低于连续分配方式。
内部碎片:分配给某进程的内存区域中,有些部分没有用上。
外部碎片:内存中某些空闲分区太小而难以利用。
1.4 非连续分配方式
非连续分配方式根据分区的大小是否固定,分为分页存储管理和分段存储管理。在分页存储管理中,又根据运行作业时是否要把作业的所有页面都装入内存才能运行,分为基本分页存储管理和请求分页存储管理。
1.4.1 基本分页存储管理
固定分区会产生内部碎片,动态分区会产生外部碎片,这两种技术对内存的利用率都比较低。我们希望内存的使用能尽量避免碎片的产生,这就引入了分页的思想:把主存空间划分为大小相等且固定的块,块相对较小,作为主存的基本单位。每个进程也以块为单位进行划分,进程在执行时,以块为单位逐个申请主存中的块空间。
分页的方法从形式上看,像分区相等的固定分区技术,分页管理不会产生外部碎片。但它又有本质的不同点∶块的大小相对分区要小很多,而进程也按照块进行划分,进程运行时按块申请主存可用空间并执行。这样,进程只会在为最后一个不完整的块申请一个主存块空间时,才产生主存碎片,所以尽管会产生内部碎片,但这种碎片相对于进程来说也是很小的,每个进程平均只产生半个块大小的内部碎片(也称页内碎片)。
1.4.1.1 分页存储的几个基本概念
1.4.1.1.1 页面和页面大小
进程中的块称为页或页面(Page),内存中的块称为页框或页帧(Page Frame)。外存也以同样的单位进行划分,直接称为块或盘块(Block)。进程在执行时需要申请主存空间,即要为每个页面分配主存中的可用页框,这就产生了页和页框的一一对应。
将进程的逻辑地址空间也分为与页框大小相等的一个个部分, 每个部分称为一个 “页” 或 “页面”。每个页面也有一个编号,即 “页号” ,页号也是从 0 开始的。
这里要明白:进程中的页面是和内存块一一对应的,所以页面大小和内存块大小设置的是一样的;
为方便地址转换,页面大小应是 2 的整数幂。同时页面大小应该适中,页面太小会使进程的页面数过多,这样页表就会过长,占用大量内存,而且也会增加硬件地址转换的开销,降低页面换入/换出的效率;页面过大又会使页内碎片增多,降低内存的利用率。
1.4.1.1.2 地址结构
分页存储管理的逻辑地址如下所示:
地址结构包含两部分:前一部分为页号 P,后一部分为页内偏移量 W。地址长度为 32 位,其中 0~11 位为页内地址,即每页大小为 4KB;12~31 位为页号,即最多允许 页。
如果有 k 位表示 “页内偏移量”,则说明该系统中一个页面的大小是
个内存单元;
如果有 M 位表示 “页号”,则说明在该系统中,一个进程最多允许有
个页面;
注意,地址结构决定了虚拟内存的寻址空间有多大。
问题 :如何实现地址变换?
首先内存中的内存块和进程中的页面大小是一一对应的;虽然进程中的各个页面在内存中是离散存放的,但是页面内部是连续存放的;
如果要访问逻辑地址 A,则
① 确定逻辑地址 A 对应的页号 P ;
② 找到 P 号页面在内存中的起始地址(需要查页表);
③ 确定逻辑地址 A 的 “页内偏移量” W;
逻辑地址 A 对应的物理地址 = P 号页面在内存中的起始地址 + 页内偏移量 W ;
问题:如何确定一个逻辑地址对应的页号和页内偏移量?
在某计算机系统中,页面大小是 50B。某进程逻辑地址空间大小为 200B,则逻辑地址 110 对应的页号、页内偏移量是多少?
首先进程的大小为 200B,页面大小为 50B,显然,进程可以被分为 4 个大小相等的页面;
如何计算:
页号 = 逻辑地址 / 页面长度(取除法的整数部分)
业内偏移量 = 逻辑地址 % 页面长度(取除法的余数部分)
页号 = 110 / 50 = 2
页内偏移量 = 110 % 50 = 10
1.4.1.1.3 页表
为了便于在内存中找到进程的每个页面所对应的物理块,系统为每个进程建立一张页表,它记录页面在内存中对应的物理块号,页表一般存放在内存中。
在配置页表后,内存执行时,通过查找该表,即可找到每页在内存中的物理块号。可见,页表的作用是实现从页号到物理块号的地址映射。
页表是由页表项组成的,页表项和地址都由两部分构成,而且第一部分都是页号,但页表项的第二部分是物理内存中的块号,而地址的第二部分是页内偏移;页表项的第二部分与地址的第二部分共同组成物理地址。
问题 1 :每个页表项占用多少个字节?
假设某系统物理内存大小为 4GB,页面大小为 4KB,则每个页表项至少应该为多少字节?
看上图 3.8 ,页表项是页表中存储的每一项,页表项由页号和块号组成;上面我们说过进程中的页面和内存中的内存块是一一对应的,所以页面大小等于内存块大小,也就等于 4KB,1KB =
B,4 *
=
B;
也就是说内存块大小为
,1 GB =
B,4GB =
B,所以 4GB 大小的内存会被分为
/
=
个内存块;
也就是说内存中内存块号应该从 0 ~
-1 开始排序;
那么也就意味着页表中的块号至少要用 20bit 来表示;
又因为计算机是以字节来定义的,1B = 8bit,所以3 * 8 = 24 bit ,所以至少要用 3 B 来表示页表中的块号;
页号是不需要占用内存空间的,因为页号在内存中是连续存放的,所以页号是可以隐藏的;这里这么理解,每个页表项占用 3 个字节,且都是连续存放的,假设页表项是从地址 x 开始存放的,那么要找到页号 i 对应的地址,只需要 x + i * 3 即可解出页号 i 对应的起始地址;
1.4.1.2 基本地址变换机构
地址变换机构的任务是将逻辑地址转换为内存中的物理地址。地址变换是借助于页表实现的。
在系统中通常设置一个页表寄存器(PTR),存放页表在内存的起始地址 F 和页表长度 M。平时,进程未执行时,页表的始址和页表长度存放在本进程的 PCB 中,当进程被调度执行时,才将页表始址和页表长度装入页表寄存器中。设页面大小为 L,逻辑地址 A 到物理地址 E 的变换过程如下(假设逻辑地址、页号、每页的长度都是十进制数):
① 计算页号 P(P=A/L)和页内偏移量 W(W=A%L)。
② 比较页号 P 和页表长度 M,若 P M,则产生越界中断,否则继续执行。
③ 页表中页号 P 对应的页表项地址 = 页表始址 F + 页号 P * 页表项长度,取出该页表项内容 b ,即为物理块号。注意区分页表长度和页表项长度。页表长度是指一共有多少页,页表项长度是指页地址占多大的存储空间。
④ 计算 E = b * L + W ,用得到的物理地址 E 去访问内存。
以上整个地址变换过程均是硬件自动完成的。例如,若页面大小 L 为 1KB,页号 2 对应的物理块为 b = 8,计算逻辑地址 A = 2500 的物理地址 E 的过程如下:P = 2500/1K = 2,W = 2500 % 1KB = 452,查找得到页号 2 对应的物理块的块号为 8 , E = 8*1024+452=8644;
页表项的大小不是随意规定的,而是有所约束的?
页表项的作用是找到该页在内存中的位置。以 32 位逻辑地址空间、字节编址单位、一页 4KB 为例,地址空间内一共有B/4KB=1M页,因此需要
M = 20 位才能保证表示范围能容纳所有页面,又因为以字节作为编址单位,即页表项大小
20/8(向上取整) = 3B。所以在这个条件下,为了保证页表项能够指向所有页面,页表项的大小应该大于等于 3B,当然,也可以选择更大的页表项让一个页面能够正好容下整数个页表项,进而方便存储(如取成 4B,这样一页正好可以装下 1K 个页表项),或增加一些其他信息。
存在的两个主要问题:
① 每次访存操作都需要进行逻辑地址到物理地址的转换,地址转换过程必须足够快,否则访存速度会降低;
② 每个进程引入页表,用于存储映射机制,页表不能太大,否则内存利用率会降低。
1.4.1.3 具有快表的地址变换机构
若页表全部放在内存中,则存取一个数据或一条指令至少要访问两次内存:第一次是访问页表,确定所存取的数据或指令的物理地址;第二次是根据该地址存取数据或指令。显然,这种方法比通常执行指令的速度慢了一半。
为此,在地址变换机构中增设一个具有并行查找能力的高速缓冲存储器——快表(是一种访问速度比内存快很多的高速缓存),又称相联存储器(TLB);TLB不是内存;是用来存放当前访问的若干页表项,以加速地址变换过程。与此对应,主存中的页表常称为慢表。
这里解释一下什么是高速缓存?
我们计算机在设计的时候通常都会设计存储设备,一般都是使用的硬盘,但是硬盘的读写速度不快,这里的不快是相对于 CPU 处理数据的能力而言的,这样这会造成 CPU 处理数据的速度和从硬盘读写数据的速度产生较大的差异;
为了缓和 CPU 处理数据的速度和硬盘读写数据速度的较大差异,我们通常都是先将数据从硬盘加载到内存(RAM)中,然后再由 CPU 从内存中读写数据,内存读写数据的速度是硬盘读写数据的好几十倍;
虽然内存读写数据的速度是硬盘读写数据的速度的好几十倍,但是相比于 CPU 处理数据的速度还是有很大的差异,因此又引入了高速缓存(Cache),高速缓存并不是内存;
明确一点就是:引入高速缓存的目的就是协调 CPU 处理数据和读写速度差异的问题;
在具有快表的分页机制中,地址的变换过程如下:
① CPU 给出逻辑地址后,由硬件进行地址转换,将页号送入高速缓存寄存器,并将此页号与快表中的所有页号进行比较。
② 若找到匹配的页号,说明所要访问的页表项在快表中,则直接从中取出该页对应的页框号,与页内偏移量拼接形成物理地址。这样,存取数据仅一次访存便可实现。
③ 若未找到匹配的页号,则需要访问主存中的页表,读出页表项后,应同时将其存入快表(快表虽然访问速度快,但是造价也很贵,所以快表中存储的信息并不多,因此需要不断的更新),以便后面可能的再次访问。若快表已满,则须按特定的算法淘汰一个旧页表项。
注意:有些处理机设计为快表和慢表同时查找,若在快表中查找成功则终止慢表的查找。
由于查询快表的速度比查询页表的速度快很多,因此只要快表命中,就可以节省很多时间。
因为局部性原理,一般来说快表的命中率可以达到 90% 以上。
例:某系统使用基本分页存储管理,并采用了具有快表的地址变换机构。访问一次快表耗时 1us,访问一次内存耗时 100us。若快表的命中率为 90%,那么访问一个逻辑地址的平均耗时是多少?
(1+100)*0.9+(1+100+100)*0.1=111us
上述式子的意思是:若快表中存有该地址,则快表访问需要 1us,得到地址后,需要访问内存中的物理地址,需要 100us,命中率 90%
如果快表中没有该地址,那么就需要在慢表中找到该地址,然后再访问内存中的物理地址,总共需要 1+100+100us,命中率 10%
这里介绍一下著名的局部性原理:
时间局部性:如果执行了程序中的某条指令,那么不久之后这条指令很有可能会再次被访问;如果某个数据被访问过,不久之后该数据很有可能再次被访问。(因为程序中存在大量的循环)
int i = 0; int a[100]; while(i<100) { a[i] = i; i++; } while循环肯定会不止一次的访问; 假设上述的程序存放在页面 1 ; 程序中定义的变量存放在页面 2 ; 那么上述程序一定会不断的在内存中循环页面 1 和页面 2;
空间局部性:一旦程序访问了某个存储空间,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的)
1.4.1.4 两级页表
单级页表存在的问题?
1. 页表必须连续存放,因此当页表很大时,需要占用很多个连续的页框。
2. 没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定的页面。
由于引入了分页管理,进程在执行时不需要将所有页调入内存页框,而只需将保存有映射关系的页表调入内存。但是,我们仍然需要考虑页表的大小。
以 32 位逻辑地址空间、页面大小 4KB、页表项大小 4B 为例,若要实现进程对全部逻辑地址空间的映射,则每个进程需要 即约 100 万个页表项。也就是说,每个进程仅页表这一项就需要 4MB 主存空间,而且还要求是连续的,显然这是不切实际的。即便不考虑对全部逻辑地址空间进行映射的情况,一个逻辑地址空间稍大的进程,其页表大小也可能是过大的。以一个 40MB 的进程为例,页表项共 40KB(40MB/4KB×4B),若将所有页表项内容保存在内存中,则需要 10 个内存页框来保存整个页表。整个进程大小约为 1 万个页面,而实际执行时只需要几十个页面进入内存页框就可运行,但若要求 10 个页面大小的页表必须全部进入内存,则相对实际执行时的几十个进程页面的大小来说,肯定降低了内存利用 率;从另一方面来说,这 10 页的页表项也并不需要同时保存在内存中,因为在大多数情况下,映射所需要的页表项都在页表的同一个页面中。
为了压缩页表,我们进一步延伸页表映射的思想,就可得到二级分页,即使用层次结构的页表:
将页表的10页空间也进行地址映射,建立上一级页表,用于存储页表的映射关系。这里对页表的 10 个页面进行映射只需要 10 个页表项,所以上一级页表只需要 1 页就已足够(可以存储 = 1024 个页表项)。在进程执行时,只需要将这一页的上一级页表调入内存即可,进程的页表和进程本身的页面可在后面的执行中再调入内存。根据上面提到的条件(32 位逻辑地址空间、页面大小4KB、页表项大小 4B ,以字节为编址单位),我们来构造一个适合的页表结构。页面大小为4KB , 页内偏移地址为
=12 位,页号部分为 20 位,若不采用分级页表,则仅页表就要占用 220x4B/4KB = 1024 页,这大大超过了许多进程自身需要的页面,对于内存来说是非常浪费资源的,而且查询页表工作也会变得十分不便、试想若把这些页表放在连续的空间内,查询对应页的物理页号时可以通过页表首页地址+页号x4B的形式得到,而这种方法查询起来虽然相对方便,但连续的1024 页对于内存的要求实在太高,并且上面也说到了其中大多数页面都是不会用到的,所以这种方法并不具有可行性。若不把这些页表放在连续的空间里,则需要一张索引表来告诉我们第几张页表该上哪里去找,这能解决页表的查询问题,且不用把所有的页表都调入内存,只在需要它时才调入(下节介绍的虚拟存储器思想),因此能解决占用内存空间过大的问题。读者也许发现这个方案就和当初引进页表机制的方式一模一样,实际上就是构造一个页表的页表,也就是二级页表。为查询方便,顶级页表最多只能有1个页面(一定要记住这个规定),因此顶级页表总共可以容纳 4KB/4B = 1K个页表项,它占用的地址位数为
K = 10位,而之前已经计算出页内偏移地址占用了 12 位,因此一个 32 位的逻辑地址空间就剩下了 10 位,正好使得二级页表的大小在一页之内,这样就得到了逻辑地址空间的格式。
二级页表实际上是在原有页表结构上再加上一层页表。
建立多级页表的目的在于建立索引,以便不用浪费主存空间去存储无用的页表项,也不用盲目地顺序式查找页表项。
1.5 基本分段存储管理
分页管理方式是从计算机的角度考虑的,目的是提高内存的利用率,提升计算机的性能。分页通过硬件机制实现,对用户完全透明。
分段管理方式的提出则考虑了用户和程序员,以满足方便编程、信息保护和共享、动态增长及动态链接等多方面的需要。
1.5.1 分段
段式管理方式按照用户进程中的自然段划分逻辑空间。例如,用户程序由主程序段、两个子程序段、栈段和数据段组成,于是可以把这个用户进程划分为 5 段,每段从 0 开始编址,并分配一段连续的地址空间(段内要求连续,段间不要求连续,因此整个作业的地址空间是二维的),其逻辑地址由段号 S 与段内偏移量 W 两部分组成。
段号为 16 位,段内偏移量为 16 位,因此一个作业最多有 = 65536 段,最大段长为 64KB。
在页式系统中,逻辑地址的页号和页内偏移量对用户是透明的,但在段式系统中,段号和段内偏移量必须由用户显示提供,在高级程序设计语言中,这个工作由编译程序完成。
1.5.2 段表
每个进程都有一张逻辑空间与内存空间映射的段表,其中每个段表项对应进程的一段,端表项记录该段在内存中的始址和长度。
配置段表后,执行中的进程可通过查找段表,找到每段所对应的内存区。可见,段表用于实现从逻辑段到物理内存区的映射。
1.5.3 地址变换机构
分段系统的地址变换机构如下图所示。为了实现进程从逻辑地址到物理地址的变换功能,在系统中设置了段表寄存器,用于存放段表始址 F 和段表长度 M。从逻辑地址 A 到物理地址 E 之间的地址变换过程如下:
① 从逻辑地址 A 中取出前几位为段号 S,后几位为段内偏移量 W。
② 比较段号 S 和段表长度 M,若 S M,则产生越界中断,否则继续执行。
③ 段表中段号 S 对应的段表项地址 = 段表始址 F + 段号 S * 段表项长度,取出该段表项的前几位得到段长 C。若段内偏移量 C,则产生越界中断,否则继续执行。
④ 取出段表项中该段的始址 b,计算 E = b + W,用得到的物理地址 E 去访问内存。
1.5.4 段的共享与保护
在分段系统中,段的共享是通过两个作业的段表中相应表项指向被共享的段的同一个物理副本来实现的。当一个作业正从共享段中读取数据时,必须防止另一个作业修改此共享段中的数据。不能修改的代码称为纯代码或可重入代码(它不属于临界资源),这样的代码和不能修改的数据可以共享,而可修改的代码和数据不能共享。
与分页管理类似,分段管理的保护方法主要有两种∶一种是存取控制保护,另一种是地址越界保护。地址越界保护将段表寄存器中的段表长度与逻辑地址中的段号比较,若段号大于段表长度,则产生越界中断;再将段表项中的段长和逻辑地址中的段内偏移进行比较,若段内偏移大于段长,也会产生越界中断。分页管理只需要判断页号是否越界,页内偏移是不可能越界的。
与分页管理不同,段式管理不能通过给出一个整数便确定对应的物理地址,因为每段的长度是不固定的,无法通过整数除法得出段号,无法通过求余得出段内偏移,所以段号和段内偏移一定要显示给出(段号,段内偏移),因此分段管理的地址空间是二维的。
1.6 段页式管理
分页存储管理能有效地提高内存利用率,而分段存储管理能反映程序的逻辑结构并有利于段的共享和保护。将这两种存储管理方法结合起来,便形成了段页式存储管理方式。
在段页式系统中,作业的地址空间首先被分成若干逻辑段,每段都有自己的段号,然后将每段分成大小固定的页。对内存空间的管理仍然和分页存储管理一样,将其分成若干和页面大小相同的存储块,对内存的分配以存储块为单位。
在段页式系统中,作业的逻辑地址分成三部分:段号、页号和页内偏移量。
为了实现地址变换,系统为每个进程建立一张段表,每个分段有一张页表。段表表项中至少包括段号、页表长度和页表始址,页表表项中至少包括页号和块号。此外,系统中还应有一个段表寄存器,指出作业的段表始址和段表长度(段表寄存器和页表寄存器的作用都有两个,一是在段表或页表中寻址,二是判断是否越界)。
注意:在一个进程中,段表只有一个,而页表可能有多个。
在进行地址变换时,首先通过段表查到页表始址,然后通过页表找到页帧号,最后形成物理地址。如下图所示,进行一次访问实际需要三次访问主存,这里同样可以使用快表来加快查找速度,其关键字由段号、页号组成,值是对应的页帧号和保护码。
1.7 小结
思考问题?
1. 为什么要进行内存管理?在单道系统阶段,一个系统在一个时间段内只执行一个程序,内存的分配极其简单,即仅分配给当前运行的进程。引入多道程序后,进程之间共享的不仅仅是处理机,还有主存储器。然而,共享主存会形成一些特殊的挑战。若不对内存进行管理,则容易导致内存数据的混乱,以至于影响进程的并发执行。因此,为了更好地支持多道程序并发执行,必须进行内存管理。
2. 页式管理中每个页表项大小的下限如何决定?
页表项的作用是找到该页在内存中的位置。以 32 位逻辑地址空间、字节编址单位、一页 4KB 为例,地址空间内共含有
B/4KB= 1M页,需要
= 20位才能保证表示范围能容纳所有页面,又因为以字节作为编址单位,即页表项的大小 ≥|20/8|=3B。当然,也可选择更大的页表项大小,让一个页面能够正好容下整数个页表项,以方便存储(例如取成4B,一页正好可以装下1K个页表项),或增加一些其他信息。
3. 多级页表解决了什么问题?又会带来什么问题?
多级页表解决了当逻辑地址空间过大时,页表的长度会大大增加的问题。而采用多级页表时,一次访盘需要多次访问内存甚至磁盘,会大大增加一次访存的时间。