操作系统哲学原理(11)内存原理-基本内存管理

说明:该系类文章更多的是从从哲学视角看 操作系统 这门学科。同时也是 操作系统的学习笔记总结。因为博主 这些年主要是以研究安卓系统和 嵌入式Linux为主,因此这个系类文章也是这两个领域不可或缺的基石之一,尤其是对操作系统感兴趣的伙伴可特别关注。


  • 内存也成为计算机的主存储器;内存管理从根本上说就是操作系统对存储设备进行的抽象和封装。
  • 对于内存而言,最重要的是虚拟内存;通过这种机制,系统将内存扩展为无限大。而实际上就是利用时间换取空间。

11 基本内存管理

11.1 内存管理环境              

在理想的状态下,程序员/用户对内存的要求是:大容量、高速度、持久性。但是事实上我们面临的物理现实却是一个由缓存、主存、磁盘、磁带(一般不用)等组成的内存架构,结构如图所示:

内存管理机制负责的是:不需要用户担心存储位置对自己的影响。实现这种机制的做法就是虚拟内存。而虚拟内存就是提供给用户的一个“幻象”,即一种抽象,让用户感觉有比物理空间大很多的地址空间。

11.2 内存管理目标

一个东西的价值在于能否满足我们的需求,如果能,就有价值。而内存管理就是要提供这样一个有价值的虚幻,就是抽象。

内存管理的目标:

  • 地址保护:一个程序不能访问其他程序的地址空间。
  • 地址独立:程序发出的地址应该与物理主存地址无关。

这两个目标就是衡量一个内存管理系统是否完善的标准,是所有内存管理必须提供的基本抽象。(当然不同的操作系统还在此基础上提出了其他抽象)    

11.3 虚拟内存概念

把程序直接加载到物理内存的问题:

  • 编写的程序尺寸要小于物理主存容量。
  • 主存能够存放的程序数量有限。

最好的解决方法:不增加内存,即不增加成本;使用虚拟内存。虚拟内存提供的抽象如图所示:

操作系统是一个革命性的突破,也正是因为这样,操作系统变得更加复杂(因为有了虚拟内存,我们编写的程序不再受物理内存尺寸大小的约束,当然要受虚拟内存大小的约束;但是已经改善很多了)。由于虚拟内存主要是从缓存中读取数据满足用户的需求,所以给人以速度提升了的感觉;虚拟内存就是实际存储架构和程序员对内存的要求之间的一座桥梁;所谓速度提升、性能提高不过只是幻像,实际上并不是那么回事,但是用户的感觉是真的,这就是操作系统魔术师的一面。

11.4 操作系统在内存中的位置

从根本上说,计算机里面运行的程序有两种:管理计算机的程序(操作系统)和使用计算机的程序(用户程序)。操作系统和用户称各占一个区域,有两种方式:

  • 11-4a,比较容易理解:符合人们的惯性思维;在进行陷入、复位、中断的时候,控制移交给操作系统比较方便(因为操作系统的起始地址是0,无需另行记录操作系统所处的位置,程序计数器清零即可;而清零对于硬件来说可以在总线上直接生成)
  • 11-4b,不符合常人的想法:不容易被理解;交叉也是一种方案,但是没有半点好处,因此不考虑。

现代操作系统不仅有RAM,还有ROM,所以操作系统可以全部放在ROM里面,也可以部分放在ROM里面,这样又多了两种分配方式:

操作系统全部放在ROM里面,用户程序放在RAM中:不容易被破坏,但是ROM比较贵,通常情况下只有少量的ROM。因此容纳不下操作系统。而 操作系统部分放在ROM里面和位于用户空间的RAM里,用户程序位于中间的RAM:与上一种模式相比较好,同时可以将输入输出与内存访问统一起来。(即将输入输出设备里面的寄存器或其他存储媒介编入内存地址,使得访问输入输出与访问内存一样,这种输入输出称为内存映射的输入输出)这种模式又分为三种:

  • 没有使用内存映射的输入输出,ROM全部是操作系统。(CP/M早期操作系统的采用的模式)
  • 使用了内存映射的输入输出,ROM的一部分属于操作系统;另一部分属于输入输出。(现代操作系统常用的模式)
  • 使用了内存映射的输入输出,ROM全部属于输入输出设备。(适合没有操作系统的可编程器件)

11.5 单道编程的内存管理

单道编程环境下,只有两个程序:操作系统程序与用户程序。而操作系统所占的内存空间是恒定的,所以我们可以将用户程序总是加载到同一个内存地址上(即用户程序总是从一个地方开始执行,因此,用户程序的地址可以事先计算出来)。在程序运行前将物理地址计算好的方式叫做静态地址编译。固定地址的内存单元非常简单。这个时候实际上并不需要任何内存管理单元。以内程序发出的已经是物理地址,在执行的过程中不需要地址翻译(这个时候程序运行速度块,因为越过了程序地址翻译的步骤)。同时 固定地址的缺陷有:

  • 整个程序需要加载到内存空间中,导致比物理内存大的程序无法运行。
  • 只运行一个程序很浪费空间,剩下的内存也无法使用。
  • 无法在不同的操作系统上运行,因为不同操作系统占用的空间不一样大。这样在一个系统环境下编译出来的程序很可能无法在另一个系统环境下执行。

11.6 多道编程的内存管理

为了克服单道编程的缺陷,我们发明了多道编程,随着多道编程度数的增加,CPU和内存使用率也随着增加(增加也有限度,超过这个限度,多道程序之间的资源竞争反而造成系统效率的下降)。虽然多道编程可以大大改善CPU和内存的使用效率,改善用户的响应时间,但是操作系统的复杂性也随之飙升。(因为在多道编程的情况下,无法将程序加载到固定的内存地址上,即无法使用静态编译)。这样我们必须在程序加载完以后才能计算物理地址,也就是程序运行时进行地址编译。动态地址翻译的示意图如下:

多道编程的内存管理是如何进行动态地址编译的,那得看内存管理的策略。多道编程下的内存管理策略有两种:固定分区和非固定分区。

11.6.1 固定分区的多道编程内存管理

将内存分为固定的几个区域,每个分区大小固定;最下面的是操作系统的分区,上面的几个为用户分区,分区大小不一;根据程序大小情况选择适当分区,如下所示:

问题:

  • 由于程序大小和分区可能不匹配,存在小程序占用大分区的情况,从而造成虽然内存里有小分区闲置,但是无法加载大程序。
  • 如果还有空闲的分区,但等待程序不在该分区的等待队列上(因为程序执行的时间不同),就将造成有空间而不能运行程序的尴尬处境。

11.6.2 地址翻译的方法

由于程序加载到内存的地址是不固定的(有多个地址可以加载),我们必须对地址进行翻译。一个程序是整个加载进去的,因此里面的虚地址(这里的虚地址就是偏移量)只要加上所占区域的起始地址就可以获得物理地址,因此,翻译的过程非常简单,即:

物理地址=虚拟地址+程序所在区域的起始地址

由于多个程序都在内存空间,需要对地址进行保护,但是只要其访问的地址不超过本身大小的连续空间,则为合法访问。因此地址保护也变得非常简单,只要满足以下条件即可:

程序所在区域的起始地址<=有效地址<=程序所在区域的起始地址+程序长度

由此可见。我们需要两个端值:基址和极限,即可达到地址翻译和地址保护的目的。这两个端值由两个寄存器来存放;分别是基址寄存器和极限寄存器。在固定分区下,基址就是固定内存分区中各个区域的起始地址,而极限寄存器则是所加载程序的长度(注意是程序的长度,不是各个分区的上限)。这样,每次程序发出的地址需要和极限比较大小:

  • 如果大于极限,则属于非法访问,程序陷入内核,终止进程。
  • 如果小于极限,则属于合法访问,加上偏移,获得物理地址,就可以合法访问这个物理地址。

由此可见,对于每个程序,似乎都占用了一个内存空间从0到极限的计算机。如下所示:

基址和极限是两个很重要的参数,只有内核能够改变它们。如果要切换程序,只需保存基址和极限寄存器的值,按照新程序的情况重新设置即可。

11.6.3 动态地址翻译的优点

动态地址翻译增加了系统的消耗,但是带来的优点远超过静态地址编译:

  • 灵活:无需依赖编译器和加载器来进行静态编译,可以将程序加载到任何地方。
  • 实施地址保护:对每个访问地址进行检查,而动态地址翻译恰好能做到这一点。     虚拟内存的概念得以实现:虚拟内存的根本是将内存扩展到磁盘上,将磁盘也做为内存的一部分。(一个程序可以一半放在磁盘上,一半放在内存上执行,即一个程序发出的访问地址可能在内存,也可能在磁盘上)。而且有了动态地址翻译,编译器和用户程序就不用再考虑物理地址了。

在动态编译环境下,一个虚拟地址仅在被访问的时候才需要加载到内存,其他时候不需要占用;由于动态编译可以改变编译参数和过程,因此在程序加载到不同的物理位置时,不同虚拟地址占用同一物理地址时做出正确的编译。在使用基址和极限的管理模式下,不同程序进入物理内存时,只需要改动基址寄存器和极限寄存器的值。

11.6.4 非固定分区的内存管理

固定分区有很多缺陷:

  • 程序大小与分区大小不匹配,怎么办?
  • 很僵硬,要是程序比分区大,怎么办?
  • 地址空间无法增加,程序在运行时空间增加了怎么办?

之所以有这些缺陷是因为分区是固定的,我们就想到将其改为非固定分区来管理内存空间。非固定分区的想法是:除了给系统划分固定的分区外,其余的内存空间作为一个整体存在,来一个程序,分配一片空间。模型如图所示:

但是这里也有问题,那就是如果程序在执行的过程中需要更多的空间该怎么办?一开始就分配足够大的空间。(实际中不可行)就算可以,在分配完增长空间后,还需要考虑一个问题,那就是程序增长的主要来源是栈和数据。那么 最简单的方式:数据和栈在一个方向上增长,独立性高但是空间利用率低。如图所示:

让数据和栈向相反的方向增长,这样,空间利用率几乎就是满的,如图所示:

对于空间的分配,操作系统根本无法静态地知道需要多少空间,那么需要做的就是:如果程序的空间不够,就给程序换一片更大的内存空间,这种将程序倒到磁盘上,再加载到内存上的这种管理方式叫做交换。

11.6.5 交换(swap)

@1 交换:将一个进程从内存中倒到磁盘上,再将其从磁盘加载到内存的过程。

@2 交换的主要目的是:

  • 给程序找到一片更大的空间,从而防止一个程序因为空间不足而崩溃。
  • 实现进程切换。(不过使用交换进行进程切换由于时间成本过高,一般不这样做)

@3 交换的模型如下:

交换和非固定分区一样,但是一个程序在执行的过程中可能发生切换,其基址和极限均有可能发生变化(但是对内存来讲,这并不难;只要每次加载程序的时候将基址和极限寄存器的内容进行重载即可)

11.6.6 重叠(overlay)

  • 交换的机制对大于物理内存容量的程序而言是无能为力的,因为怎么交换都不行。那么对于则会纵情况,就需要另一种机制,重叠。
  • 重叠:将程序按照功能分为一段一段功能相对完整的单元。一个单元执行完再执行下一个,而一旦执行下一个单元就不会再执行前面的那个单元了。所以我们可以把后面的程序单元覆盖到前面的程序单元上。这样就可以解决大于物理内存容量的程序执行的问题了。
  • 但是这里也有问题,将内存管理的部分功能交给用户是个不好的方法。况且不是每个人都能划清执行单元的。因此从根本上讲,这不是操作系统的解决方案。

11.6.7 双基址

如果我们运行两个一样的程序,只是数据不同,我们自然会想到能否让两个进程共享部分内存空间。如果只用基址极限的管理模式则无法实现这样的功能。单基址的方案如下:

那么这就需要新的机制了;双基址,即设定两组基址和极限,数据和代码分别用两组基址和极限来表示,这样就没有问题了。

11.7 闲置空间管理

在管理内存的时候,操作系统需要知道内存有多少空余,需要跟踪内存的使用,跟踪的方法有两种:

@1 字位图表示法:用字位0和1分别表示空闲空间和已占用空间。内存分配位图如下:

@2 链表表示法:将分配单元按是否闲置链接起来。内存分配的链表表示如图(前面的数字:闲置空间起始分配的单元号;后面的数字:大小)

在链表表示下,虚找一块闲置空间需要扫面链表,但是这样做速度很慢。为了提高查找闲置单元的速度,可以将闲置空间和被占用空间分开设置,这样就形成了双链表管理模式。位图表示法和链表表示法的比较如下:

  • 位图:空间成本固定;内存发生变化时,修改状态简单;但是没有容错能力。
  • 链表:程序少时比较好;程序多时节点较多,时间、空间成本上升;内存发生变化时,修改状态繁琐。
     
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

图王大胜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值