操作系统 - 第 4 章 存储器管理

引言:

    存储器一直都是计算机系统的重要组成部分;近年来,随着计算机技术的发展,系统软件和应用软件在种类 、功能上都急剧地膨胀,虽然存储器容量一直在不断扩大,但仍不能满足现代软件发展的需要;因此,存储器依旧是一种宝贵而又稀缺的资源

    如何对存储器加以有效的管理,不仅直接影响到存储器的利用率,而且对系统性能也有重大影响;存储器管理的主要对象是内存;由于对外存的管理与对内存的管理类似,只是它俩的用途不同,即外存主要用来存放文件,所以将把外存的管理放在文件管理一章进行介绍

4.1  存储器的层次结构

    在计算机执行时,几乎每一条指令都涉及对存储器的访问,因此要求对存储器的访问速度能匹配处理机的运行速度,否则就会明显地影响处理机的运行

    此外,还要求存储器具有非常大的容量,而且存储器的价格还应该比较便宜

    对于这样十分严格的三个条件,目前是无法同时满足的;于是,在现代计算机系统中,无一例外地采用了多层次结构的存储器系统

4.1.1  多层结构的存储器系统
1. 存储器的多层结构

通用计算机

存储层次

对于通用计算机而言,存储层次至少应该具有三个级别 :

最高层为 CPU 寄存器中间为主存(内存)最底层为辅存(硬盘)

高档计算机

存储层次

在较高档的计算机中,还可以根据具体的功能细分为 :

寄存器 、高速缓存 、主存储器 、磁盘缓存 、固定磁盘 、可移动存储介质 等 6 层

图示
存储层次的特征

在存储层次中,层次越高(即越靠近 CPU)

存储介质的访问速度越快,价格也越高,所配置的存储容量也越小

掉电后

信息是否丢失

寄存器 、高速缓存 、主存储器 、磁盘缓存,都属于操作系统存储管理的范围;

掉电后,存储的信息会丢失

低层的固定磁盘 、可移动存储介质则属于设备管理的范围;

掉电后,存储的信息依旧存在

2. 可执行存储器

访问机制不同

在计算机系统的存储层次中,寄存器主存储器,也被称为 "可执行存储器"
可执行存储器中的信息 、辅存中的信息,计算机采用不同的访问机制,所以耗费的时间也不同
进程可以在很少的时钟周期内,使用 load 或 store 指令来访问 "可执行存储器" 中存储的信息
OS 通过 I/O 设备,来访问辅存中的信息
对辅存的访问中将涉及到中断 、设备驱动程序以及物理设备的运行,所需耗费的时间远远高于访问可执行存储器的时间,一般相差至少 3 个数量级
访问管理对于不同层次的存储介质,由 OS 进行统一管理

OS 的存储管理,负责对可执行存储器的分配 、回收,以及提供在存储层次间数据移动的管理机制

例如,主存与磁盘缓存 、高速缓存与主存之间的数据移动

设备和文件管理则根据用户的需求,OS 提供对辅存的管理机制

4.1.2  主存储器与寄存器
1. 主存储器
概念

主存储器简称 "内存" 或 "主存" ,也可称 "可执行存储器" ,是计算机系统中的主要部件

作用主存储器,用于保存进程运行时的程序和数据
读取流程通常,处理机(CPU)都是从主存储器中取得指令和数据的,并将取得的指令放入指令寄存器,将读取的数据装入数据寄存器;或反之,将指令寄存器中的指令以及数据寄存器中的数据存储到主存储器中
发展

早期的内存是由磁芯做成的,其容量一般为数十 KB 到数百 KB ;随着 VLSI 的发展,现在的内存已由 VLSI 构成,其容量,即使是微机系统,也在数十 MB 到数 GB ,而且还在不断增加

嵌入式计算机系统一般仅有几十 KB 到几 MB

作用CPU 与外围设备交换的信息,一般也依托于主存储器的地址空间

为什么会变为

多层次结构

由于主存储器访问速度远低于 CPU 执行指令的速度,为缓和这一矛盾,在计算机系统中引入了寄存器和高速缓存
2. 寄存器
特点

寄存器具有和处理机相同的速度,故对寄存器的访问速度最快,寄存器完全能与 CPU 协调工作但寄存器的价格十分昂贵,因此寄存器容量不可能做得很大

在早期计算机中,寄存器的数目仅为几个

功能

寄存器,用于存放处理机运行时的数据,来加速存储器(主存)的访问速度

场景 :用寄存器存放操作数 、用作地址寄存器来加快地址转换速度

发展

随着 VLSI 的发展,寄存器的成本也在迅速降低,在当前的微机系统和大中型机中,寄存器的数目都已增加到数十个到数百个,寄存器的字长一般是 32 位或 64 位;

而在小型的嵌入式计算机中,寄存器的数目仍只有几个到十几个,而且寄存器的字长通常只有 8 位

4.1.3  高速缓存和磁盘缓存
1. 高速缓存
引入高速缓存是现代计算机结构中的一个重要部件;高速缓存是介于 CPU 寄存器和主存储器之间的存储器
功能高速缓存,主要用于备份主存中常用的数据从而减少处理机对主存储器的访问次数,这样可大幅度地提高程序执行速度
特点

高速缓存容量远大于寄存器,而比内存小约两到三个数量级,从几十 KB 到几 MB ;

高速缓存的访问速度比主存储器更快

CPU与内存

矛盾

在计算机系统中,为了缓和内存与处理机速度之间的矛盾,许多地方都设置了高速缓存
局部性原理

将一些常用数据放在高速缓存中,这样做是否有效,这将涉及到程序执行的 "局部性原理" :程序在执行时将呈现出局部性规律,即在一个较短的时间内,程序的执行仅局限于某个部分

处理机存/取

流程

通常,进程的程序和数据存放在主存储器中,每当要访问时,才被临时复制到一个速度较快的高速缓存中

这样,当 CPU 访问一组特定信息时,会首先检查这组信息是否在高速缓存中如果信息在高速缓存中,则直接从高速缓存中取出使用,从而避免访问主存否则,就必须从主存中读取这组信息

指令高速缓存就像大多数计算机都有 "指令高速缓存" ,用来暂存下一条即将执行的指令;如果没有指令高速缓存,则 CPU 将会空等若干个周期,直至下一条指令从主存中取出
多级高速缓存由于高速缓存的速度越高则价格越贵,所以有的计算机系统中设置了两级甚至多级高速缓存;紧靠内存(这里应该是寄存器)的一级高速缓存的速度最高,容量最小;二级高速缓存的容量稍大,速度也稍低
2. 磁盘缓存
由于目前磁盘的 I/O 速度远低于对主存的访问速度,为了缓和两者之间在速度上的不匹配,而设置了磁盘缓存,主要用于暂时存放频繁使用的一部分磁盘数据和信息,以减少访问磁盘的次数

磁盘缓存与高速缓存不同,磁盘缓存本身并不是一种实际存在的存储器,而是利用主存中的部分存储空间暂时存放从磁盘中读出(或写入)的信息

主存也可以看做是辅存的高速缓存
辅存中的数据必须先复制到主存储器中,才能被使用;反之,处理机输出的数据也必须先存入主存储器中,才能输出到辅存
一个文件的数据可能先后出现在不同层次的存储器中;
例如,一个文件的数据通常被存储在辅存(如硬盘),当其需要运行或被访问时,就必须调入主存,也可以暂时存放在主存的磁盘高速缓存中
大容量的辅存常常使用磁盘,磁盘数据经常备份到磁带或可移动磁盘组上,以防止硬盘故障时丢失数据;有些系统自动地把旧文件数据从辅存转储到海量存储器中,如磁带上,这样还能降低存储价格

4.2  程序的装入和链接

程序的装入和链接
用户程序要在系统中运行,必须先将用户程序装入内存,然后再将装入内存的用户程序变为一个可以执行的程序;
从用户源代码到可执行程序,通常要经过以下几个步骤:
1编译 ,编译程序(Compiler)对用户源程序进行编译,形成若干个目标模块(Object Module)
2链接 ,链接程序(Linker)将编译后形成的一组目标模块以及这些目标模块所需的函数库(lib),链接在一起,形成一个完整的装入模块(Load Module)
3装入 ,装入程序(Loader)将装入模块,装入内存
下图展示了这样的三步过程
图示
4.2.1  程序的装入
引言为了阐述上的方便,我们先介绍一个无需进行链接的单个目标模块的装入过程,该目标模块也就是装入模块
将一个装入模块装入内存时,可以有以下三种装入方式:
1. 绝对装入方式(Absolute Loading Mode)
背景当计算机系统较小,且仅能运行单道程序时,完全可以知道程序将驻留在内存的什么位置,此时可以采用绝对装入方式
含义用户程序经过编译后,将产生绝对地址(物理地址)的目标模块
流程事先已知用户程序(进程)驻留在从 R 处开始的位置,则编译程序所产生的目标模块(装入模块),便可从 R 处开始向上扩展(目标模块的首地址就是 R+1也就是说,绝对装入程序便可按照装入模块中的地址,直接将程序和数据装入内存装入模块被装入内存后,由于程序中的相对地址(逻辑地址)与实际内存地址完全相同,故不需要对程序和数据的地址进行修改
特点程序中所使用的绝对地址既可在编译或汇编时给出,也可由程序员直接赋予
缺点由程序员直接给出绝对地址时,不仅要求程序员熟悉内存的使用情况,而且一旦程序或数据被修改后,可能要改变程序中的所有地址
改善措施在程序中采用符号地址,然后在编译或汇编时,再将这些符号地址转换为绝对地址
2. 可重定位装入方式(Relocation Loading Mode)
绝对装入方式不足绝对装入方式只能将目标模块装入到内存中事先指定的位置,这只适用于单道程序环境
新问题在多道程序环境下,编译程序无法预知编译后所得到的目标模块应放在内存的何处
改进方式用户程序编译后所形成的若干个目标模块,起始地址通常都是从 0 开始的程序(目标模块)中用到的所有地址也都是相对于所在目标模块的起始地址计算得到的;此时,不能再用绝对装入方式,而应采用可重定位装入方式,根据内存的具体情况将装入模块装入到内存的合适为止
特点可重定位装入程序将装入模块装入内存后,会使得装入模块中的所有逻辑地址与实际装入内存后的物理地址不同
流程在用户程序的 1000 号单元处有一条指令 "LOAD 1,2500" ,该指令的功能是将 2500 单元中的整数 X 取至寄存器 1 但若将该用户程序装入到内存的 10000 ~ 15000 号单元而不进行地址变换,则在执行 11000 号单元中的指令时,它将仍从 2500 号单元中把数据取至寄存器 1 ,而导致数据错误 。正确的方法应该是,将取数指令中的地址 2500 修改成 12500 ,即把指令中的逻辑地址 2500 与本程序在内存中的起始地址 10000 相加,才得到正确的物理地址 12500 ;除了数据地址应修改外,指令地址也须做同样的修改,即将指令的逻辑地址 1000 与起始地址 10000 相加,得到绝对地址 11000
通常,把在装入时对目标程序中指令地址和数据地址的修改过程,称为 "重定位"
因为地址变换通常是在进程装入时一次性完成的,以后不再改变,故称为 "静态重定位"
图示
3. 动态运行时的装入方式(Dynamic Run-time Loading)
可重定位装入方式优点可将装入模块装入到内存中任何允许的位置,故可用于多道程序环境
可重定位装入方式缺点不允许程序运行时在内存中移动位置
可重定位装入方式缺点说明程序在内存中移动,意味着程序的物理位置发生了变化,这时必须对程序和数据的地址(绝对地址)进行修改后才能运行;实际情况是,在运行过程中,程序在内存中的位置经常会改变;例如,在具有对换功能的系统中,一个进程可能会被多次换出,又多次被换入,每次被换入后在内存中的位置通常是不同的
改进措施采用动态运行时装入方式

动态运行时

装入方式说明

动态运行时的装入程序在把装入模块装入内存后,并不立即把装入模块中的逻辑地址转换为物理地址而是把地址转换操作推迟到程序真正执行时才进行因此,装入内存后的所有地址还是逻辑地址为了使地址转换不影响指令的执行速度,动态运行时装入方式需要一个重定位寄存器来支持地址转换

4.2.2  程序的链接
编译的产物源程序经过编译后,可得到一组目标模块
链接的作用链接程序的功能,是将这组目标模块以及它们所需要的库函数装配成一个完整的装入模块
链接的分类在对目标模块进行链接时,根据进行链接的时间不同,可把链接分成如下三种
1. 静态链接(Static Linking)方式
含义在程序运行之前,先将各目标模块以及这些目标模块各自所需的库函数链接成一个完整的装配模块,以后不再拆开;把这种事先进行链接的方式称为 "静态链接方式"
示例

在经过编译之后得到三个目标模块 A 、B 、C ,它们的长度分别为 len_a ,len_b ,len_c 

在模块 A  中有一条语句 CALL B 用于调用模块 B ;在模块 B 中有一条语句 CALL C 用于调用模块 C ;对于模块 A 来说,B 是外部调用符号;对于模块 B 来说,C 是外部调用符号;将这几个目标模块装配成一个装入模块时,必须解决以下两个问题:
修改相对地址由编译程序所产生的所有目标模块中,使用的都是相对地址,每个目标模块的起始地址都是 0,每个目标模块中的地址都是相对于当前目标模块的起始地址计算得到的;在链接成一整个装入模块后,原目标模块 B 和 C 在装入模块中的起始地址不再为 0 ,而分别是 Len_a 和 Len_a + Len_b ,所以必须修改目标模块 B 和 C 中所有的地址,即把原目标模块 B 中的所有地址都加上 Len_a ,把原目标模块 C 中的所有地址都加上 Len_a + Len_b

变换

外部调用符号

将每个模块中所用的外部调用符号也都变换为相对地址,如把 B 的起始地址变换为 Len_a ,把 C 的起始地址变换为 Len_a + Len_b    

小结:这种先进行链接所形成的一个完整的装入模块,又称为 "可执行文件" ;通常都不再把它拆开,要运行时直接将该可执行文件装入内存;这种事先进行链接,以后不再拆开的链接方式,称为 "静态链接方式"

2. 装入时动态链接(Load-time Dynamic Linking)
概念将用户源程序编译后所得到的一组目标模块,在装入内存时,采用边装入边链接的链接方式
在装入一个目标模块 A 时若发生一个外部模块 B 调用事件,装入程序会去找相应的外部目标模块 B ,并将该外部目标模块 B 装入内存还要按照静态链接方式中修改目标模块中的相对地址
装入时动态链接的优点

便于

修改和更新

静态链接的缺点 :由静态链接方式装配在一起的装入模块,如果要修改或更新其中某个目标模块,则需要重新打开整个装入模块;这不仅效率低,有时候还无法实现
采用装入时动态链接方式,由于各目标模块是分开存放的,所以要修改或更新各目标模块则非常容易
共享目标模块静态链接的缺点 :每个应用模块都必须含有其目标模块的拷贝,无法实现对目标模块的共享
采用装入时动态链接方式,OS 很容易将一个目标模块链接到多个应用模块上,实现多个应用程序对该目标模块的共享
3. 运行时动态链接(Run-time Dynamic Linking)

装入时动态链接

低效

在许多情况下,应用程序在运行时,每次要运行的模块可能都不相同;由于事先无法知道本次要运行哪些目标模块,所以只能将所有可能要运行到的目标模块全都装入内存,并在装入时全部链接在一起;这种方式效率很低,有些目标模块根本不会被调用
进阶运行时动态链接方式,是对装入时动态链接方式的改进

运行时动态链接

概念

对某些目标模块的链接推迟到程序执行时才进行在执行过程中,当发现一个被调用的目标模块尚未装入内存时,OS 立即找到该目标模块,并将该目标模块装入内存,将其链接到调用者模块上(修改相对地址,变换调用符号)在执行过程中未被用到的目标模块,都不会被装入内存以及被链接到装入模块上这样的链接方式不仅能加快程序的装入过程,而且能节省大量内存空间

4.3  连续分配存储管理方式

    为了能将用户程序装入内存,必须为用户程序分配一定大小的内存空间

    连续分配方式,是最早出现的一种存储器分配方式;连续分配方式为一个用户程序分配一个连续的内存空间,"连续" 即程序中代码或数据的逻辑地址相邻,也就是分配的内存空间的物理地址也是相邻的

    连续分配方式可分为 4 种:单一连续分配固定分区分配动态分区分配动态可重定位分区分配

4.3.1  单一连续分配(单进程)

   (1). 在单道程序环境下当时的存储器管理方式是把内存分为 "系统区" 和 "用户区" 两部分

   (2). 系统区内存尽提供给 OS 使用,系统区通常位于内存的低地址部分在用户区内存中,仅装有一道用户程序,即整个内存的用户空间由该用户程序独占

   (3). 这样的存储器分配方式,称为 "单一连续分配方式"

说明:

    很多早期的单用户 、单任务操作系统都配置了存储器保护机构,用于防止用户程序对操作系统的破坏;但后来多款单用户操作系统都未采取存储器保护措施,这样既可以节省硬件,另外在单用户环境下,机器被独占,不可能被其他用户程序干扰即使出现破坏行为,也是用户程序自己破坏操作系统,只会影响该用户程序自己的运行而操作系统可通过系统重新启动而再次装入内存

4.3.2  固定分区分配(多进程)

背景

    为了能在内存中载入多道程序,且使这些程序之间不会相互干扰,于是将整个用户空间划分为若干个固定大小的区域,在每个分区中只装入一道作业,从而形成了最早的 、最简单的一种可运行多道程序的分区式存储管理方式

    当有一个分区空闲时,便可以再从 "外存的后备作业队列" 中选择一个适当大小的作业,装入该空闲分区;当该作业结束时,可再从后备作业队列中找出另一合适作业调入该分区

1. 划分分区的方法

    有下面两种方法将内存的用户空间划分为若干个固定大小的分区

   (1). 分区大小相等(指,用户空间划分的分区,大小都一样)

    缺点

        缺乏灵活性当程序太小时会造成内存空间浪费

        当程序太大,一个分区又无法装入该程序,致使该程序无法运行

    优点:

        当利用一台计算机同时控制多个相同对象时,因为这些对象所需的内存空间大小相同,这种划分方式更方便 、实用

   (2). 分区大小不等

        为了增加存储器分配灵活性,故将用户空间划分为若干个大小不等的分区

        最好能调查常在该系统中运行的作业大小,根据用户需要来划分分区;

        通常,把内存的用户空间分成多个较小的分区 、适量的中等分区 、少量的大分区;这样便可以根据程序大小,为程序分配适当的分区

2. 内存分配

   (1). 为了便于内存分配,通常将分区按大小进行排队(分区大小不等)

   (2). 为这些排队的分区,建立一张分区使用(状态)表,其中各表项的内容包括:每个分区的起始地址分区大小分区状态(是否已被分配)

   (3). 当有用户程序要装入时 "内存分配程序" 根据用户程序的大小,检索该 "分区使用表"从中找出一个能满足要求的 、尚未分配的分区 ,将该分区分配给该用户程序 ,然后将该分区对应的表项(分区状态)置为 "已分配" 若没有找到大小足够的分区,则拒绝为该用户程序分配内存(分区)

    说明:固定分区分配,是最早出现的 、可用于多道程序系统的存储管理方式由于每个分区大小固定,必然造成存储空间浪费,因而现在已经很少用于通用的 OS ;但在某些用于控制多个相同对象的控制系统中,由于每个对象的控制程序大小相同,且程序事先已编写好,所需数据数量也是固定的,所以仍采用固定分区存储管理方式

4.3.3  动态分区分配

    动态分区分配,也称为 "可变分区分配" ,根据进程的实际需要,动态地为进程分配内存空间

    实现动态分区分配时,涉及到分区分配中所用的数据结构分区分配算法分区的分配操作和回收操作,这三个问题

1. 动态分区分配中的数据结构

    为了实现动态分区分配,系统中必须配置相应的数据结构,用来描述空闲分区 、已分配分区的情况,为分配提供依据

    常用的数据结构有以下两种:

   (1). 空闲分区表

      在系统中设置一张空闲分区表,用于记录每个空闲分区的情况

      每个空闲分区的信息映射为表中的一个表项,表项中的字段包括:分区号 、分区大小 、分区起始地址等信息

   (2). 空闲分区链

      为了实现对空闲分区的分配和链接,在每个分区的起始部分设置一些用于控制分区分配的信息在分区头部设置用于链接各分区的前向指针,在分区尾部设置后向指针通过前 、后向链接指针,可将所有的空闲分区链接成一个双向链表

      为了检索方便,在分区尾部 "重复" 设置状态位和分区大小字段;当分区被分配出去后,把状态位从 "0" 改为 "1" ,此时,前 、后向指针将失去意义

2. 动态分区分配算法

    为了把一个新作业装入内存(用户空间的某个分区),必须按照一定的分配算法,从空闲分区表或空闲分区链中选出一个分区,分配给该作业;

    内存分配算法对系统性能影响很大,人们花了大力气研究内存分配算法,故而产生了许多动态分区分配算法;后面介绍的 4 种动态分区分配算法,都属于 "顺序式搜索算法" ;再后面介绍 3 种 "索引式搜索算法"

3. 分区分配操作

    在动态分区存储管理方式中,主要的操作是分配内存 、回收内存

   1). 分配内存

      (1). 找到合适的分区 

            系统将利用某种分配算法,从空闲分区表(链)中找到所需(匹配)大小的分区

      (2). 判断是否要对选中的分区进行切割

      设请求的分区大小为 u.size ,表中每个空闲分区的大小可表示为 m.size ;若 m.size - u.size <= size(size 是事先规定的不再分割的剩余分区的大小),说明多余部分太小,可不再切割,故可将整个分区分配给请求者 ;否则(即 m.size - u.size > size 多余部分超过 size),便从该分区中按请求的大小,划出一块内存空间分配给用户余下的分区内存仍留在空闲分区表(链)中

      (3). 处理完的分区返给调用者

      将分配出来的内存块的首地址,返回给调用者

   2). 回收内存

      当进程运行完毕释放内存时系统根据回收区(先前分配出去的内存块)的首地址,从空闲分区表(链)中找到相应的插入点,假设插入点的前一个空闲分区为 F1 ,插入点的后一个空闲分区为 F2 ,此时可能出现以下四种情况之一:

     (1). 回收分区与插入点的前一个空闲分区 F1 相邻接;此时应将回收分区与前分区合并不必为回收分区分配新表项只修改前分区的大小

     (2). 回收分区与插入点的后一个空闲分区 F2 相邻接;此时应将回收分区与后分区合并回收分区的首地址作为合并后的新空闲分区的首地址新分区大小为前 、回收分区之和

     (3). 回收分区同时与插入点的前 、后两个分区邻接;此时将前 、回收 、后三个分区合并使用前分区的表项和首地址,作为合并后新分区的表项和首地址新分区大小为前 、回收 、后三个分区的和

     (4). 回收分区既不与 F1 邻接,也不与 F2 邻接;此时为回收分区单独建立一个新表项填写回收区的首地址和分区大小并根据其首地址插入到空闲链中的适当位置

4.3.4  基于顺序搜索的动态分区分配算法

    为了实现动态分区分配,通常是将系统中的 "空闲分区" 链成一个 "双链表"

    所谓 "顺序搜索" ,是指依次搜索(遍历)空闲分区链上的空闲分区(结点),去寻找一个大小能满足进程需求的分区

    基于顺序搜索的动态分区分配算法有如下四种:首次适应算法 、循环首次适应算法 、最佳适应算法 、最坏适应算法  (其实我觉得把算法称为 "策略" 更好)

1. 首次适应(First Fit,FF)算法

    我们以空闲分区链为例,来说明采用 FF 算法时的分配情况

   (1). FF 算法要求空闲分区链以地址递增的次序链接

   (2). 在分配内存块时,(总是)从链首开始顺序查找,直至找到一个大小能满足要求的空闲分区为止

   (3). 然后再按照作业的大小,从该空闲分区中划出一块内存空间,分配给请求者,该空闲分区中剩下的内存空间仍留在空闲链中

   (4). 若遍历整个链表都没有找到一个能满足要求的分区,表明系统中没有足够大的内存分配给该进程,内存分配失败,返回

   优点:

      FF 算法优先利用内存中低地址部分的空闲分区,从而保留了高地址部分的大空闲区,这为之后到达的大作业,分配大的内存空间创造了条件

   缺点:

      低地址部分不断被划分,链中会留下许多难以利用的 、很小的空闲分区,这些还在链中的 、小的空闲分区,称为 "碎片" 每次查找又都是从低地址开始的,增加了可用空闲分区的查找开销

2. 循环首次适应(Next Fit,NF)算法

   改进:为了避免低地址部分留下许多很小的空闲分区(碎片),以及减少可用空闲分区的查找开销

   循环首次适应算法的步骤:

   循环首次适应算法在为进程分配内存空间时,不再是每次都是链首开始查找,而是从上次找到的空闲分区的下一个空闲分区开始查找直至找到一个能满足进程要求的空闲分区,从该空闲分区中划出一块与请求大小相等的内存空间,分配给作业

   算法适配:

      应设置一个 "起始查寻指针" ,用于指示下一次起始查寻的空闲分区,并采用循环查找方式,即如果最后一个(链尾)空闲分区的大小仍不能满足要求,则应返回到第一个(链首)空闲分区,比较该空闲分区的大小是否满足要求找到后,应调整起始查寻指针(的指向);没找到满足要求的空闲分区,则内存分配失败,返回

   优点:

      能使内存中的空闲分区分布得更加均匀,从而减少了空闲分区的查找开销

   缺点:

      空闲分区链中,从而缺少大的空闲分区

3. 最佳适应(Best Fit,BF)算法

   所谓 "最佳" 是指,每次为作业分配内存时,总是把能满足要求 、又是最小的空闲分区分配给作业

   为了加速寻找,该算法要求将所有的空闲分区,按其容量(大小)以从小到大的顺序形成一个空闲分区链;这样,第一次找到的能满足要求的空闲分区必然是最佳的

   缺点

      每次分配后所切割下来的剩余部分总是最小的,这样,在空闲分区链中会留下许多难以利用的 "碎片"

4. 最坏适应(Worst Fit,WF)算法

   最坏适应分配算法选择空闲分区的策略,与最佳适应算法正好相反

   最坏适应算法的步骤

      扫描整个空闲分区链,总是挑选一个最大的空闲分区,从中分割一部分内存块给作业使用

   缺点

      空闲分区链中,缺乏大的可用的空闲分区,故称最坏适应算法

   优点

      可以使链表中剩下的空闲分区不至于太小,产生 "碎片" 的可能性最小,对中 、小作业有利

      最坏适应算法的查找效率很高(算法要求,将所有的空闲分区,按其容量从大到小的顺序形成空闲分区链,查找时,只要看第一个分区能否满足作业要求即可)

4.3.5  基于索引搜索的动态分区分配算法

    基于顺序搜索的动态分区分配算法,比较适用于不太大的系统;当系统很大时,系统中的内存分区可能会有很多,相应的空闲分区链就会很长,这时采用顺序搜索分区的方法可能会很慢

    为了提高搜索空闲分区的速度,在大 、中型系统中往往采用基于索引搜索的动态分区分配算法;目前常用的有快速适应算法伙伴系统哈希算法

1. 快速适应(quick fit)算法

概括 :

    该算法又称为 "分类搜索法"

    将空闲分区根据容量大小进行分类,具有相同容量的所有空闲分区,都单独设立一个空闲分区链表;此时系统中存在多个空闲分区链,同一个分区链表上的分区,大小都相等

    同时在内存中设立一张管理索引表;其中的每一个索引表项,对应一个空闲分区链表,表项中记录了该空闲分区链表表头的指针

    空闲分区的分类是根据进程常用的空间大小进行划分的,如 2 KB 、4 KB 、8 KB 等;对于其他大小的分区,如 7 KB 这样的空闲区,既可以放在 8 KB 的链表中,也可以放在一个特殊的空闲区链表中

分配:

    该算法在搜索可分配的空闲分区时分为两步:

    第一步:根据进程的长度,从索引表中去寻找到能容纳当前进程的最小空闲链表

    第二步:从链表中取下第一块进行分配即可

优点 :

    算法在进行空闲分区分配时,不会对任何分区进行分割,所以能保留大的分区,满足大进程的需要;也不会产生内存碎片;查找效率高

缺点:

    为了有效合并分区,分区还给主存时的算法比较复杂,系统开销大

    算法在分配空闲分区时,以进程为单位,一个分区只属于一个进程,因此在为进程分配的一个分区中或多或少存在一定的浪费;这是典型的以空间换时间的提高分配效率的做法

2. 伙伴系统(buddy system)

      该算法规定,无论已分配分区还是空闲分区,单个分区的大小均为 2 的 k 次幂k 为整数,1\leqslant k\leqslant m);通常 2^m 是整个分区的可分配内存的大小(也就是最大分区的大小)

      假设系统的可利用空间容量为 2^m 个字,则系统开始运行时,整个内存区是一个大小为 2^m 的空闲分区

      在系统运行过程中,由于不断地划分,将会形成若干个不连续的空闲分区,将这些空闲分区按分区的大小进行分类对于具有相同大小的所有空闲分区,单独设立一个 "空闲分区双向链表" ,则不同大小的空闲分区形成了 k 个空闲分区链表

分配流程 :

      当需要为进程分配一个长度为 n 的存储空间时,首先计算一个 i 值,使 2^{i-1}< n\leqslant 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 的空闲分区时 :

        (1). 若事先已存在 2^i 的空闲分区,则应将这个大小为 2^i 的空闲分区,与伙伴分区合并为大小为 2^{i+1} 的空闲分区

        (2). 若事先已存在 2^{i+1} 的空闲分区,又应继续与其伙伴分区合并为大小为 2^{i+2} 的空闲分区,以此类推

        在伙伴系统中,对于一个大小为 2^k ,地址为 x 的内存块,其伙伴块的地址则用 buddy_k(x)表示,通式为 :

                    buddyk(x) =  x + 2k  (若 x MOD 2k+1 = 0)

                                          x - 2k   (若 x MOD 2k+1 = 2k)

            在伙伴系统中,其分配和回收的时间性能取决于查找空闲分区的位置和分割 、合并空闲分区所花费的时间

          

                  a. 时间性能 : 在回收空闲分区时,需要对空闲分区进行合并,

                      伙伴系统的时间性能比快速适应算法差,但由于采用了索引搜索算法,又比顺序搜索算法好

                  b. 空间性能 :由于对空闲分区进行合并,减少了小的空闲分区,提高了空闲分区的可使用率,

                      伙伴系统的空间性能比快速适应算法好,比顺序搜索算法差

            在当前的操作系统中,普遍采用的是接下来要讲的基于离散分配方式的分页和分段机制的虚拟内存机制,

            虚拟内存机制相较于伙伴算法更为合理且高效,但在多处理机系统中,伙伴系统仍算得上一种有效的内存分配和释放的方法,仍广泛使用

3. 哈希算法

        在上述的分类搜索算法和伙伴系统算法中,都是将空闲分区根据分区大小进行分类,对于具有相同大小的空闲分区,单独设立一个空闲分区链。

        在为进程分配空间时,需要在一张管理索引表中,查找到所需空间大小所对应的表项,从中得到对应的空闲分区链的表头指针,

        从而遍历该空闲分区链,得到一个空闲分区。

        如果对空闲分区的分类较细,则相应索引表的表项也就较多,因此会显著地增加搜索索引表的时间开销

        哈希算法就是利用哈希快速查找的优点,以及空闲分区在可利用空闲区表中的分布规律,建立哈希函数,

        构造一张以空闲分区大小为关键字的哈希表,该表的每一个表项记录了一个对应的空闲分区链的表头指针

        当进行空闲分区分配时,根据所需空闲分区大小,通过哈希函数计算,即得到在哈希表中的位置,

        从中得到相应的空闲分区链,实现最佳分配策略

4.3.6  动态可重定位分区分配

1. 紧凑

      连续分配方式的一个重要特点是 :一个系统或用户程序必须被装入一片连续的内存空间中

      当一台计算机运行了一段时间后,其内存空间将会被分割成许多小的分区,故整个内存空间缺乏大的空闲分区;即使这些分散的许多小分区的容量总和大于要装入的程序,但由于这些分区不相邻,所以无法将该大程序装入内存

示例 :

      下图中展示了内存中出现的四个互不相邻的小分区,其容量分别为 10 KB 、30 KB 、14 KB 、26 KB ,容量总和是 80 KB ;但如果出现一个作业,要求获得 40 KB 的内存空间,由于必须为它分配一个连续空间,故此作业无法装入;这种不能被利用的小分区就是前面提及的 "碎片" ,也可称作 "零头"

      若想把大作业装入,可采用的一种方法是 :将内存中的所有作业进行移动,使它们全都相邻;这样,即可把原来分散的多个空闲小分区拼接成一个大分区,可将一个作业装入该区;这种通过移动内存中作业的位置,把原来多个分散的小分区拼接成一个大分区的方法,称为 "拼接" 或 "紧凑"

      虽然 "紧凑" 能获得大的空闲分区,但也带来了新的问题,即经过紧凑后的用户程序在内存中的位置发生了变化,此时若不对程序和数据的地址加以修改(变换),则程序必将无法执行;为此,在每次 "紧凑" 后,都必须对移动了的程序或数据进行重定位;为了提高内存的利用率,系统在运行过程中经常需要进行 "紧凑" ,每 "紧凑" 一次,就要对移动了的程序或数据的地址进行修改,这不仅是一件相当麻烦的事情,而且还大大影响到系统的效率;下面介绍的动态重定位方法,可以很好解决上述问题

2. 动态重定位

      在前面介绍的动态运行时装入的方式中作业装入内存后的所有地址仍然都是相对(逻辑)地址;当程序指令真正执行时,才会把相对(逻辑)地址转换为绝对(物理)地址

      为了使地址的转换不会影响到指令的执行速度,必须有硬件地址变换机构的支持 :在系统中增设一个重定位寄存器,用这个寄存器存放程序(或数据)在内存中的(物理)起始地址;程序在执行时,真正访问的内存地址是相对(逻辑)地址与重定位寄存器中的地址相加后形成的

      地址变换过程是在程序执行期间,对每条指令或数据的访问是自动进行,故称为 "动态重定位";当系统对内存进行了 "紧凑" ,而使若干程序从内存的某处移至另一处时,不需要对程序做任何修改,只要用该程序在内存中的新的起始地址去置换原来的起始地址即可

      下图展示了动态重定位的实现原理

3. 动态重定位分区分配算法

      动态重定位分区分配算法与动态分区分配算法基本相同,差别仅在于 :在动态重定位分区分配算法中,增加了紧凑的功能;

      通常,当该算法无法找到一个足够大的空闲分区来满足用户需求时 :

      如果所有的小的空闲分区的容量总和大于用户进程的要求,这时便会对内存进行 "紧凑" ,将经过 "紧凑" 后得到的大空闲分区分配给用户进程;

      如果所有的小的空闲分区的容量总和仍小于用户进程所需,则返回分配失败信息

4.4 对换(Swapping)

      对换技术也称为交换技术,最早用于麻省理工学院的单用户分时系统 CTSS 中;由于当时的计算机的内存都非常小,为了使该系统能分时运行多个用户程序而引入了对换技术;

      系统把所有的用户作业存放在磁盘上,每次只能调入一个作业进入内存;当该作业的一个时间片用完时,将该作业调至外存的后备队列上等待,再从后备队列上将另一个作业调入内存;这就是最早出现的分时系统中所用的对换技术

      由上所述可知,要实现内 、外存之间的对换,系统中必须有一台 I/O 速度较高的外存,而且其容量也必须足够大,能容纳正在分时运行的所有用户作业,目前最常使用的是大容量磁盘存储器;

      下面主要介绍目前在多道程序环境中广泛使用的对换技术

4.4.1 多道程序环境下的对换技术
1. 对换的引入   

      在多道程序环境下,一方面,内存中的某些进程由于某事件尚未发生而被阻塞运行,而这些进程占用了大量的内存空间,甚至有时可能出现内存中所有进程都被阻塞(没有进程正在运行),迫使 CPU 停下来等待;另一方面,有许多作业因内存空间不足,一直驻留在外存上,无法进入内存运行;显然这对系统资源是一种严重的浪费,导致系统吞吐量下降;

      为了解决上述问题,在系统中又增设了对换(交换)设施;

      所谓 "对换" ,是指把内存中暂时不能运行的进程或者暂时不使用的程序及数据,换出到外存上,以便腾出足够的内存空间;再把已具备运行条件的进程或进程所需的程序及数据调入内存

      对换是改善内存利用率的有效措施,可以直接提高处理机的利用率和系统的吞吐量

      "对换" 技术自从 20 世纪 60 年代初期出现便引起了人们的重视;在早期的 UNIX 系统中就引入了对换功能,该功能一直保留至今,各个 UNIX 版本实现对换功能的方法,大体上是一样的;即在系统中设置一个对换进程,由该对换进程将内存中暂时不能运行的进程调出到磁盘的对换区;同样由该对换进程将磁盘上已具备运行条件的进程调入内存;在 Windows OS 中也具有对换功能;如果一个新进程在装入内存时发现内存不足,可以将已在内存中的老进程换出至磁盘,腾出内存空间;由于对换技术的确能有效地改善内存利用率,故现在已被广泛应用在 OS 中

2. 对换的类型

      在每次对换时,都是将一定数量的程序或数据换入或换出内存;根据每次对换时所对换的数量,可将对换分为如下两类 :

  (1). 整体对换 :处理机中级调度实际上就是存储器的对换功能,其目的是用来解决内存紧张问题,并可进一步提高内存的利用率和系统的吞吐量;由于在中级调度中,对换是以整个进程为单位的,故称之为 "进程对换" 或 "整体对换" ;这种对换被广泛地应用于多道程序系统中,并作为处理机中级调度

  (2). 页面(分段)对换 :如果对换是以进程的一个 "页面" 或 "分段" 为单位进行的,则分别称之为 "页面对换" 或 "分段对换" ,又统称为 "部分对换" ;这种对换方法是实现后面要讲到的请求分页和请求分段式存储管理的基础,其目的是为了支持虚拟存储系统;

      在此,我们只介绍进程对换,而分页或分段对换,将放在虚拟存储器中介绍

      为了实现进程对换,系统必须能实现三方面的功能 :对换空间的管理 、进程的换出和进程的换入

4.4.2 对换空间的管理
1. 对换空间管理的主要目标

      在具有对换功能的 OS 中,通常把磁盘空间分为 "文件区" 和 "对换区" 两部分

      1). 文件区管理的主要目标

      文件区占用磁盘空间的大部分,用于存放各类文件;由于通常的文件都是较长时间驻留在外存上,文件被访问的频率较低,故对文件区管理的主要目标是 :提高文件存储空间的利用率,然后才是提高文件的访问速度;因此,对文件区空间的管理采取离散分配方式

      2). 对换空间管理的主要目标

      对换空间只占用磁盘空间的小部分,用于存放从内存换出的进程;由于这些进程在对换区中驻留的时间是短暂的,而对换操作的频率比较高,所以对对换空间管理的主要目标是 :提高进程换入和换出的速度,然后才是提高文件存储空间的利用率;为此,对对换区空间的管理采取连续分配方式,很少考虑外存中的碎片问题

2. 对换区空闲盘块管理中的数据结构

      为了实现对对换区中的空闲盘块的管理,在系统中应配置相应的数据结构,用于记录外存对换区中的空闲盘块的使用情况;

      其数据结构的形式与内存在动态分区分配方式中所用的数据结构相似,即同样可以用空闲分区表或空闲分区链;在空闲分区表中的每个表目中,应包含两项 :对换区的首地址(盘块号表示) 、对换区的大小(盘块数表示)

3. 对换空间的分配与回收

      由于对换区的分配采用的是连续分配方式,因而对换空间的分配与回收,与动态分区方式的内存分配与回收类似;其分配算法可以是 首次适应算法 、循环首次适应算法 或 最佳适应算法 等;具体的分配操作与下图的分配过程相同;对换区的回收操作可分为 4 种情况 :

(1).  回收分区与(插入点的)前一个空闲分区相邻

(2). 回收分区与(插入点的)后一个空闲分区相邻

(3). 回收分区同时与(插入点的)前 、后空闲分区相邻

(4). 回收区既不与前一个分区相邻,也不与后一个分区相邻

对上述这几种情况的处理也与动态分区方式相同,此处不再赘述

4.4.3 进程的换出与换进

      当内核因执行某个操作而发现内存不足时;例如,当一个进程 P 由于创建子进程而需要更多的内存空间,但实际又没有足够的内存空间时,进程 P 便会调用(唤醒)"对换进程" ,对换进程的主要任务是实现进程的换出和换入

1. 进程的换出

      对换进程在实现进程换出时,是将内存中的某些进程调出至对换区,以便腾出内存空间

      换出过程可分为以下两步 :

      (1). 选择被换出的进程

      "对换进程" 在选择被换出的进程时,将检查所有驻留在内存中的进程;首先选择处于阻塞状态或睡眠状态的进程,当有多个这样的进程时,应当选择优先级最低的进程作为换出进程;在有的系统中,为了防止低优先级进程在调入内存后很快又被换出,还需考虑进程在内存的驻留时间;如果系统中已无阻塞进程,而现在的内存空间仍不足以满足需要,便选择优先级最低的就绪进程换出

      (2). 进程换出过程

      在选择换出进程后,在换出进程时,只能换出非共享的程序和数据段;而对于那些共享的程序和数据段,只要还有进程需要,就不能被换出

      在进行换出时,应先申请对换空间,若申请成功,就启动磁盘,将该进程的程序和数据传送到磁盘的兑换区上;若传送过程未出现错误,便可回收该进程所占用的内存空间,并对该进程的进程控制块和内存分配表等数据结构做相应的修改;若此时内存中还有可换出的进程,则继续执行换出过程,直到内存中再无阻塞进程为止

2. 进程的换入

      "对换进程" 将定时执行换入操作,"对换进程" 首先查看进程控制块集合中所有进程的状态,从中找出 "就绪" 状态但已换出的进程;当有许多这样的进程时,"对换进程" 将选择其中已换出到磁盘上时间最久(必须大于规定时间,如 2s)的进程作为换入进程,为其申请内存;如果申请成功,则直接将该进程从外存调入内存;如果申请失败,则需先将内存中的某些进程换出,腾出足够的内存空间,再将进程调入

      在 "对换进程" 成功地换入一个进程后,若还有可换入的进程,则再继续执行换入换出过程,将其余处于 "就绪且已换出" 状态的进程陆续换入,直到内存中再无 "就绪且已换出" 状态的进程为止,或者已无足够的内存来换入进程,此时 "对换进程" 才停止换入

      由于要交换一个进程需要很多的时间,因此,对于提高处理机的利用率而言,交换进程并不是一个非常有效的解决方法;目前用的比较多的对换方案是 :在处理机正常运行时,并不启动对换程序;但如果发现有许多进程在运行时经常发生缺页且显现出内存紧张的情况,才启动 "对换程序" ,将一部分进程调至外存;如果发现所有进程的缺页率都已明显降低,而系统的吞吐量已下降时,则可暂停运行 "对换程序"

4.5 分页存储管理方式

      连续分配方式会形成许多 "碎片" ,虽然可以通过 "紧凑" 方法将许多碎片拼接成可用的大块空间,但开销代价巨大;如果允许将一个进程直接分散地装入到许多不相邻接的分区中,便可充分地利用内存空间,而无须再进行 "紧凑拼接";

      基于这一思想而产生了离散分配方式;根据在离散分配时所分配地址空间的基本单位的不同,又可将离散分配分为以下三种:

      (1). 分页存储管理方式 : 在该方式中,将用户程序的地址空间分为若干个固定大小的区域,称为 "" 或 "页面" ;典型的页面大小为 1 KB ;相应地,也将内存空间分为若干个物理块或页框(frame),页面和页框大小相同这样可将用户程序的任一页面放入任一页框中实现了离散分配

      (2). 分段存储管理方式 : 这是为了满足用户要求而形成的一种存储管理方式;把用户程序的地址空间分为若干个大小不同的段,每段可定义一组相对完整的信息;在内存分配时,以段为单位,这些段在内存中可以不相邻接,所以也同样实现了离散分配

      (3). 段页式存储管理方式 : 这是分页和分段两种存储管理方式相结合的产物;它同时具有两者的优点,是目前应用较广泛的一种存储管理方式.

4.5.1  分页存储管理的基本方法

      1. 页面和物理块

      (1). 页面

      分页存储管理将进程的逻辑地址空间分成若干个页,并为各页加以编号,编号从 0 开始,如第 0 页 、第 1 页等相应地,也把内存的物理地址空间分成若干个块,同样为这些块加以编号,编号从 0 开始,如 0# 块 、1# 块等在为进程分配内存时,以块为单位,将进程中的若干个页,分别装入到多个可以不相邻接的物理块中由于进程的最后一页经常装不满一块(页小块大),而形成了不可利用的碎片,称为 "页内碎片"

      (2). 页面大小

      在分页系统中 :

      若选择的页面过小,优点是:可以减少内存碎片,从而减少总的内存碎片所占的空间,有利于提高内存利用率,缺点是:每个进程分成了较多的页面,导致进程的页表过长,页表占用大量内存,降低页面换进换出的效率;

      若选择的页面过大,优点是:减小页表的长度,从而减少页表占用的内存空间,提高页面换进换出的速度,缺点是:导致页内碎片增大

      总结:页面的大小应选择适中,且页面的大小应是 2 的幂,通常为 1 KB ~ 8 KB

      2. 地址机构

      分页地址中的地址结构 :

      该地址结构包含两部分内容:前一部分为页号 P,后一部分为 "位(偏)移量 W" ,即页内地址

      图中的地址长度为 32 位,其中 0 ~ 11 位是页内地址,即每页的大小为 4 KB ;12 ~ 31 位,为页号,地址空间最多允许有 1 M 页

      对某特定机器,其地址结构是固定的;若给定一个逻辑地址空间中的地址为 A,页面的大小为 L,则页号 P 和页内地址 d,可按下面公式求得 :

      其中,INT 是整除函数,即返回整数部分;MOD 是取余函数

      例如,其系统的页面大小为 1 KB,设 A = 2170 B,则由上面公式可以求得 P = 2,d = 122

      3. 页表

      在分页系统中,允许将进程的各个页面,离散地存储在内存的任一物理块中

      为保证进程仍然能够正确地运行,即可以在内存中找到每个页面所对应的物理块,系统为每个进程建立了一张页面映像表,简称 "页表"

      在进程地址空间内的所有页(0 ~ n),依次在页表中有一页表项,其中记录了相应页在内存中对应的物理块号,如下图所示;在配置了页表后,进程执行时,通过查找该表,即可找到每页在内存中的物理块号页表的作用是页号到物理块号的地址映射

      即使在简单的分页系统中,也常在页表的表项中设置一 "存取控制字段" ,用于对该存储块中的内容加以保护当 "存取控制字段" 仅有一位时,可用来规定该存储块中的内容是允许 "读/写" 还是 "只读" ;若 "存取控制字段" 为两位,则可规定为 "读/写" 、"只读" 、"只执行" 等存取方式

      如果有一进程试图去写一个只允许读的存储块时,将引起操作系统的一次中断;如果要利用分页系统去实现虚拟存储器,则还需增设一个数据项

4.5.2  地址变换机构

      为了能将用户地址空间中的逻辑地址转换为内存空间中的物理地址,在系统中必须设置 "地址变换机构" ;该机构的基本任务是实现从逻辑地址到物理地址的转换;由于页内地址和物理地址是一一对应的(例如,页面大小是 1 KB 的页内地址是 0 ~ 1023 ,其相应的物理块内的地址也是 0 ~ 1023 ,无需再进行转换);因此,地址变换机构的任务只是将逻辑地址中的页号转换为内存中的物理块号;又因为页面映射表的作用局势用于实现从页号到物理块号的变换,故地址变换任务是借助于页表来完成的

      1. 基本的地址变换机构

      进程在运行期间,需要对程序和数据的地址进行变换,即将用户地址空间中的逻辑地址转换为内存空间中的物理地址该地址转换操作的执行频率非常高,每条指令的地址都需要进行变换,因此需要使用硬件来实现

      页表功能是由一组专门的寄存器来实现的,一个页表项用一个寄存器;

      由于寄存器具有较高的访问速度,因而有利于提高地址变换的速度;但寄存器的价格较高,且大多数现代计算机的页表又可能很大,页表项总数可达几千甚至几十万,显然这些页表项不可能都用寄存器来实现;因此,页表大多驻留在内存中

      在系统中只设置一个页表寄存器 PTR(Page-Table-Register),在其中存放页表在内存的起始地址和页表的长度;当进程未执行时,页表的起始地址和页表长度存放在本进程的 PCB 中;当调度程序调度到某进程时,才将这两个数据装入 PTR 中;因此,在单处理机环境下,虽然系统中可以运行多个进程,但只需一个页表寄存器

      当进程要访问某个逻辑地址中的数据时,分页地址变换机构会自动地将有效地址(相对地址)分为页号与页内地址两部分,再用页号为索引去检索页表;查找操作由硬件执行

      在执行检索之前,先将页号与页表长度进行比较;如果页号大于或等于页表长度,表示本次所访问的地址已经超越进程的地址空间,则系统会发现这一错误并产生一个地址越界中断;若未出现越界错误,则将 "页表地址" 加上 "页号和页表长度的乘积" ,便得到该表项在页表中的位置,于是可从中得到该页的物理块号,将(物理块号)装入物理地址寄存器中;与此同时,再将有效地址寄存器中的页内地址送入物理地址寄存器的块内地址字段中;最终,便完成了从逻辑地址到物理地址的转换

      下图展示了分页系统的地址变换机构

      2. 具有快表的地址变换机构

      由于页表是存放在内存中的,CPU 每存取一个数据,都要两次访问内存;第一次是访问内存中的页表,从页表中找到指定页的物理块号,再将物理块号与页内偏移量 W 拼接,形成物理地址;第二次访问内存时,才是从第一次所得地址中读取所需数据(或向此地址在写入数据);因此,采用这种会降低计算机的处理速度为原来的一半;可见,以此高昂代价来换取存储器空间利用率的提高,得不偿失

      为了提高地址变换速度,可在地址变换机构中增设一个具有并行查询能力的特殊高速缓冲寄存器,又称 "联想寄存器"(Associative Memory),或称为 "快表" ,作用是存放当前访问的那些页表项

      此时的地址变换过程是 :在 CPU 给出有效地址后,由地址变换机构自动地将页号 P 送入高速缓冲寄存器,并将此页号与高速缓存中的所有页号进行比较,若高速缓存中存在匹配的页号,便表示所要访问的页表项在快表中;于是,可直接从快表中读出该页对应的物理块号,并送到物理地址寄存器中;若未在快表中找到对应页表项,则还须再访问内存中的页表,找到后,把从页表项中读出的物理块号送到地址寄存器中;同时再将此页表项存入快表的一个寄存器单元中,即重新修改快表;如果快表已满,则 OS 必须找到一个存在时间较长且被认为不再需要的页表项,将该页表项从快表中换出

      由于成本的关系,快表不可能做得很大,通常只存放 16 ~ 512 哥页表项,这对中 、小型作业来说,基本可以将全部页表项存放在快表中;但对于大型作业来说,则只能将部分的页表项放入快表中;由于程序和数据的访问常常具有局限性(局部性原理),因此据统计,从快表中能找到所需页表项的概率可达 90% 以上;这样,由于增加了地址变换机构而造成的速度损失可减少到 10% 以下,达到了可接受的程度

4.5.3  访问内存的有效时间

      从进程发出指定逻辑地址的访问请求,经过地址变换,到内存中找到对应的实际物理地址单元并取出数据,所需要花费的总时间,称为 "内存的有效访问时间(Effective Access Time ,EAT)"

      假设访问一次内存的时间为 t ,在基本分页存储管理方式中,有效访问时间(EAT)分为 :第一次访问内存时间(即,查找页表对应的页表项所耗费的时间 t)、第二次访问内存时间(即,将页表项中的物理块号与页内地址拼接成实际物理地址所耗费的时间 t)之和 :

EAT = t + t=2t

      在引入快表的分页存储管理方式中,通过快表查询,可以直接得到逻辑页所对应的物理块号,由此拼接形成实际物理地址,减少了一次内存访问,缩短了进程访问内存的有效时间;但由于快表的容量限制,不可能将一个进程的整个页表全部装入快表,所以在快表中查找所需页表项存在着命中率的问题;所谓命中率,是指使用快表并在其中成功查找到所需页面的页表项的概率;这样,在引入快表的分页存储管理方式中,有效访问时间的计算公式即为 :

EAT=a\times \lambda +(t+\lambda )(1-a)+t=2t+\lambda -t\times a

上式中,\lambda 表示查找快表所需要的时间,a 表示命中率,t 表示访问一次内存所需要的时间

      可见 ,引入快表后的内存有效访问时间分为查找到逻辑页对应的页表项的平均时间 a\times \lambda +(t+\lambda )(1-a) ,以及对应实际物理地址的内存访问时间 t ;假设对快表的访问时间 \lambda 为 20 ns(纳秒),对内存的访问时间 t 为 100 ns ,则下表列出了不同的命中率 a 与有效访问时间的关系 :

      正是由于引入了快表,CPU 访问数据所耗费的时间明显减少

4.5.4  两级和多级页表

      1. 两级页表(Two-Level Page Table)

      针对难以找到大且连续的内存空间来存放页表的问题,可用将页表进行分页的方法,使得每个页面的大小与内存物理块的大小相同,并为它们进行编号,即依次为 0#页 、1#页 ,... ,n#页,然后离散地将各个页面分别存放在不同的物理块中;

      同样,也要为离散分配的页表,再建立一张页表,称为 "外层页表(Outer Page Table)" ,在每个页表项中记录了页表页面的物理块号;

      示例 :以前面的 32 位逻辑地址空间为例

      当页面大小为 4 KB 时(12 位),若采用一级页表结构,应具有 20 位的页号,即页表项应有 1 M 个;在采用两级页表结构时,再对页表进行分页,使每页中包含 2^{10}(即 1024)个页表项,最多允许有 2^{10} 个页表分页;或者说,外层页表中的外层页内地址 P_2 为 10 位,外层页号 P_1 也为 10 位 ;此时的逻辑地址结构如下图

      由上图可以看出,在页表的每个表项中,存放的是进程的某页在内存中的物理块号,如 0# 页存放在 1# 号物理块中,1# 页存放在 4# 号物理块中;而在外层页表的每个页表项中所存放的是某页表分页的首地址,如 0# 页表存放在 1011# 号物理块中;我们可以利用外层页表和页表的两级页表结构,实现进程从逻辑地址到内存中物理地址的变换

      为了方便实现地址变换,在地址变换机构中,同样需要增设一个外层页表寄存器,用于存放外层页表的起始地址;OS 利用逻辑地址中的外层页号作为外层页表的索引,从中找到指定页表分页的起始地址,再利用 P_2 作为指定页表分页的索引,找到指定的页表项,其中即含有该页在内存中的物理块号,用该物理块号 P 和页内地址 d 即可构成访问的内存物理地址

      下图展示了两级页表的地址变换机构

      上述对页表实行离散分配的方法,虽然解决了对于大页表无需大片连续存储空间的问题,但并为解决用较少的内存空间去存放大页表的问题;换句话说,只用离散分配空间的办法并为减少页表所占用的内存空间;能够用较少的内存空间存放页表的唯一方法是 :仅把当前需要的一批页表项调入内存,以后再根据需要陆续调入

      在采用两级页表结构的情况下,对于正在运行的进程,必须将其外层页表调入内存,而对于页表则只需要调入一页或几页;为了表征某页的页表是否已经调入内存,还应在外层页表项中增设一个状态位 S ,其值若为 0 则表示该页表分页不在内存中,否则(值不为 0)说明其分页已调入内存

      进程运行时,地址变换机构则根据逻辑地址中的 P_1 去查找外层页表;若找到的页表项的状态位为 0 ,则产生一个中断信号,请求 OS 将该页表分页调入内存

      2. 多级页表

      对于 32 位的机器,采用两级页表结构是合适的,但对于 64 位的机器,采用两级页表是否仍然合适,须做以下简单分析

      如果页面大小仍采用 4 KB 即 2^{12} B ,那么还剩下 52 位,假定仍按物理块的大小(2^{12} 位)来划分页表,则将余下的 42 位用于外层页号;此时在外层页表中可能有 4096 G 个页表项,要占用 16 384 GB 的连续内存空间;这样的结果显然是无法接受的;因此,必须采用多级页表,将外层页表再进行分页,也就是将各页离散地装入到不相邻的物理块中,再利用第 2 级的外层页表来映射这些物理块之间的关系

      对于 64 位的计算机,如果要求其可以支持 2^{64}(= 1 844 744 TB)规模的物理存储空间,那么即使采用三级页表结构也是难以办到的,而在当前的实际应用中也没有这样的要求;所以最近推出的 64 位 OS 中,把可直接寻址的存储器空间减少为 45 位长度(即 2^{45})左右,这样便可利用三级页表结构来实现分页存储管理

4.5.5  反置页表(Inverted Page Table)

      1. 反置页表的引入

      在分页系统中,为每个进程配置了一张页表,进程逻辑地址空间中的每一页,在页表中都对应有一个页表项;在现代计算机系统中,通常允许一个进程的逻辑地址空间非常大,因此就需要有许多的页表项,进而也会占用大量的内存空间

      为了减少页表占用的内存空间,引入了反置页表;一般页表的页表项是按页号进行排序的,页表项中的内容是物理块号;而反置页表则是为每一个物理块设置一个页表项,并将它们按物理块的编号排序,其中的内容则是页号与其所属进程的标识符

      在 IBM 公司推出的许多系统中都采用了反置页表,如 AS/400 、IBM RISC System 和 IBM RT 等系统

      2. 地址变换

      在利用反置页表进行地址变换时,是根据进程标识符和页号,去检索反置页表;如果检索到与之匹配的页表项,则该页表项(中)的序号 i 便是该页所在的物理块号,可用该块号与页内地址一起构成物理地址,送到内存地址寄存器;若检索了整个反置页表仍未找到匹配的页表项,则表明此页尚未装入内存;对于不具有请求调页功能的存储管理系统,此时则表示地址出错;对于具有请求调页功能的存储管理系统,此时应产生请求调页中断,系统将把此页调入内存

      虽然反置页表可有效地减少页表占用的内存;例如,对于一个具有 64 MB(内存)的机器,如果页面大小为 4 KB ,那么反置页表只占用 64 KB 内存;然而在该表中只包含了已经调入内存的页面,并为包含尚未调入内存的页面

      因此,还必须为每个进程建立一个外部页表(External Page Table);该页表与传统的页表一样,当所访问的额页面在内存中时,并不需要访问外部页表,仅当发现所需的页面不在内存中时,才会使用这个外部页表;在外部页表中包含了各个页面在外存中的物理位置,通过外部页表可将所需的页面调入内存

      由于在反置页表中,为每一个物理块设置一个页表项,当内存容量很大时,页表项的数目还是会非常大;要利用进程标识符和页号去检索这么大的一张线性表是非常耗时的;于是,可利用 Hash 算法来进行检索,这样可以很快地找到在反置页表中的相应页表项;不过在采用 Hash 算法时,可能会出现所谓的 "地址冲突" ,即有多个逻辑地址被映射到同一个 Hash 表项上,必须妥善解决这一问题;这一问题会在文件系统中作进一步介绍

4.6  分段存储管理方式

4.6.1  分段存储管理方式的引入

      为什么要引入分段存储管理方式,可从下面两个方面说明 :

      一方面,通常的程序都可分为若干个段,如主程序段 、子程序段 A 、子程序段 B 、... 、数据段以及栈段等,每个段大多是一个相对独立的逻辑单位

      另一方面,实现和满足信息共享 、信息保护 、动态链接以及信息的动态增长等需要,也都是以段为基本单位

      总结以下就是,分段存储管理方式,更符合用户和程序员如下多方面的需要

      1. 方便编程

      通常,用户把自己的作业按照逻辑关系划分为若干个段,每个段都是从 0 开始编址,并有自己的名字和长度;因此,程序员们都迫切地要求访问的逻辑地址是由段名(段号)和段内偏移量(段内地址)决定的,这不仅可以方便程序员编程,也可使程序非常直观,更具可读性

      例如,下面的两条指令便使用段名和段内地址:

LOAD 1,[A] ! <D>

STORE 1 ,[B] | <C>

      其中,前一条指令的含义是:将分段 A 中 D 单元内的值读入寄存器 1;后一条指令的含义是:将寄存器 1 的内容存入 B 分段的 C 单元中

      2. 信息共享

      在实现对程序和数据的共享时,是以信息的逻辑单位为基础的;比如,为了共享某个过程 、函数或文件;分页系统中的 "页" 只是存放信息的物理单位(块),并无完整的逻辑意义,故在分页系统中,一个可被共享的过程往往可能需要占用数十个页面,这为实现共享增加了困难

      "段" 可以是信息的逻辑单位;因此,我们可以为该被共享过程建立一个独立的段,这就极大地简化了共享的实现;为了实现段的共享,存储管理应该能与用户程序分段的组织方式相适应

      3. 信息保护

      信息保护同样是以信息的逻辑单位为基础的,而且经常是以一个过程 、函数或文件为基本单位进行保护的;

      例如,希望函数 A 仅允许进程执行,而不允许读,更不允许写,那么只须在包含函数 A 的这个段上标上 "只执行标志" 即可;但是在分页系统中,函数 A 可能要占用若干个页面,而且其中的第一个和最后一个页面还会装有其他程序段的数据。它们可能有着不同的保护属性,如可以允许进程读写,这样就很难对这些页面实施统一的保护

      因此,分段管理方式能更有效和方便地实现对信息的保护功能

      4. 动态增长

      在实际应用中,往往存在着一些段,尤其是数据段,在数据段的使用过程中,数据量的不断增加导致数据段动态地增长,相应地,数据段所需要的存储空间也会动态增长;然而,对于数据段究竟会增长到多大,事先又很难确切地知道;对此,很难采取预先多分配的方法进行解决

      前面提到的其他几种存储管理方式都难以应对这种动态增长的情况,而分段存储管理方式却能较好地解决这一问题

      5. 动态链接

      前面的章节已经对动态链接做了介绍;

      为了提高内存的利用率,系统只将真正要运行的目标程序装入内存,也就是说,动态链接在作业运行之前,并不是把所有的目标程序段都链接到一起

      当程序要运行时,首先将主程序以及立即被用到的目标程序装入内存,即启动运行;在程序运行过程中,当需要调用某个目标程序时,才将该段(目标程序)调入内存并进行链接

      可见,动态链接要求的是以目标程序(段)作为链接的基本单位;因此,分段存储管理方式非常适合于动态链接

4.6.2  分段系统的基本原理

      1. 分段

      在分段存储管理方式中,作业的地址空间被划分为若干个段,每个段定义了一组逻辑信息

      例如,有主程序段 MAIN 、子程序段 X 、数据段 D 以及栈段 S 等,如图 4-19 所示

      每个段都有自己的名字;为了实现简单起见,通常可用一个段号来代替段名,每个段都从 0 开始编址,并采用一段连续的地址空间;段的长度由相应的逻辑信息组的长度决定,因此各段的长度并不相等;整个作业的地址空间被分割成多段,呈现出二维特性,即每个段既包含了一部分地址空间,又标识了逻辑关系;其逻辑地址由段号(段名)和段内地址组成

      分段地址中的地址具有如下结构 :

      在上面的地址结构中,允许一个作业最长有 64 K 个段,每个段的最大长度为 64 KB

      分段方式已得到许多编译程序的支持,编译程序能自动地根据源程序的情况产生若干个段;

      例如,Pascal 编译程序可以为全局变量 、用于存储相应参数及返回地址的过程调用栈 、每个过程或函数的代码部分(局部变量)等,分别建立各自的段;类似地,Fortran 编译程序可以为公公块(Common Block)建立单独的段,也可以为数组分配一个单独的段;装入程序将装入所有这些段,并为每个段赋予一个段号

      2. 段表

      在前面所介绍的动态分区分配方式中,系统为整个进程分配一个连续的内存空间;而在分段式存储管理系统中,则是为每个分段分配一个连续的分区;进程中的各个段,可以离散地装入内存中不同的分区中

      为保证程序能正常运行,就必须能从物理内存中找出每个逻辑段所对应的位置;类似于分页系统,分段存储系统为每个进程建立一张映射表,简称 "段表" ;每个段在 "段表" 中占据一个表项,其中记录了该段在内存中的起始地址(即 "基址")和段的长度,如下图 4-19

      段表可以存放在一组寄存器中,有利于提高地址转换速度;但更广泛的方法是将段表放在内存中

      在配置了段表后,执行中的进程可以查找段表,找到每个段所对应的内存区;可见,段表是用来实现从逻辑段到物理内存区的映射

      3. 地址变换机构

      为了实现,进程从逻辑地址到物理地址的变换功能,在系统中设置了段表寄存器,用于存放段表地址和段表长度 TL ;在进行地址变换时,系统将逻辑地址中的段号 S 与段表长度 TL 进行比较

      若 S > TL ,表示段号太大,即访问越界,于是产生越界中断信号;若未越界,则根据段表的起始地址和该段的段号,计算出该段对应段表项的位置(下标),从中读出该段在内存的起始地址;

      然后,再检查段内地址 d 是否超过该段的段长 SL ;若超过,即 d > SL ,同样发出越界中断信号;若未越界,则将该段的起始地址 d 与段内地址相加,即可得到要访问的内存物理地址;图 4-20 展示了分段系统的地址变换过程

      像分页系统一样,当段表放在内存中时,每访问一个数据,都必须访问两次内存,故而成倍地降低了计算机的速率;解决的方法和分页系统类似,也增设一个联想存储器,用于保存最近常用的段表项;一般情况下,由于段比页大,因而段表项的数目比页表项的数目少,其所需的联想存储器也相对较小,所以可以显著地减少存取数据的时间;与没有地址变换的常规存储器相比,其存取速度约慢 10% ~ 15%

      4. 分页和分段的主要区别

      由上述不难看出,分页和分段系统有许多相似之处;比如,两者都采用离散分配方式,且都是通过地址映射机构实现地址变换;但在概念上两者完全不同,主要表现在下面三个方面 :

      (1). 页是信息的物理单位

      采用分页存储管理方式,是为了实现离散分配方式,来消减内存的外零头,提高内存的利用率;或者说,分页仅仅只是系统管理上的需要,完全是系统的行为,对用户是不可见的;

      分段存储管理方式中的段,则是信息的逻辑单位;段通常包含的是一组意义相对完整的信息;分段的主要目的 :能更好地满足用户的需要

      (2). 页的大小固定,且由系统决定

      在采用分页存储管理方式的系统中,在硬件结构上,就把用户程序的逻辑地址划分为 "页号" 与 "页内地址" 两部分,也就是直接由硬件实现的,因而在每个分页系统中,只能有一种大小的页

      段的长度却不是固定的,取决于用户所编写的程序,通常由编译程序在对源程序进行编译时,根据信息的性质来划分

      (3). 分页的用户程序地址空间时一维的

      分页完全是系统的行为,所以在分页系统中,用户程序的地址是属于单一的线性地址空间,程序员只需利用一个记忆符即可表示一个地址

      分段是用户的行为,故在分段系统中,用户程序的地址空间是二维的,程序员在标识一个地址时,既需要给出段名,又需要给出段内地址

4.6.3  信息共享

      分段系统的一个突出优点,是易于实现段的共享,即允许若干个进程共享一个或多个分段,且对段的保护也十分简单易执行

      1. 分页系统中程序和数据的共享

      2. 分段系统中程序和数据的共享

4.6.4  段页式存储管理方式

      1. 基本原理

      2. 地址变换过程

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值