计算机存储管理全面梳理(一)内存管理的基本原理和要求

在看了计算机组成原理中的存储系统一章后,结合操作系统中的内存管理和文件管理,对整个计算机的存储管理有了新的理解,这里就一起杂糅着总结一下。
首先抛出几个我的总体看完以后的新的理解:

  • 计算机的存储管理,是分层次的,从高速缓存到内存到外存,速度依次降低,大小逐渐增大,价格逐渐便宜。
  • CPU从这三层获取数据的速度逐渐降低,我们存储管理搞了这么多骚操作,其实就是为了省钱的同时,既提高访问速度(局部性原理,层层缓存),又提高可用的地址空间(虚拟内存)
  • 无论是哪个层次,所面对的问题都是地址的映射,空间的分配,以及空间不够时的置换替换策略(这三者之间是强相关的,比如分页的分配方式,地址映射就需要用到页表,请求式分页的分配方式,空间的分配和置换也是强相关的)。
  • 只有存在空间不够的情况才需要置换策略,比如cache置换,比如请求式的分页的内存。像连续分配或者普通的分页式分配则不需要置换策略,空间不够就不分配,不存在把哪个内存暂时置换出来的问题。
  • 各种空间管理方式之间不是独立的,是有内在的递进关系的。以内存分配为例,一开始是连续分配,但是发现连续分配很容易出现碎片化问题,就想到了不连续分配,不连续分配分为分段,分页和段页式,但是注意,到目前为止,只是内存分配的问题,整个程序还是需要一开始就完全加载到内存中。由此我们提出了虚拟内存的概念,为了配合虚拟内存,我们升级分段,分页和段页式为请求式的,页表项或者段表项中会多一些字段用于缺页时外存地址,会多一些字段用于内存置换时的标志。
  • 分页/段和虚拟内存是两回事,分页是一种空间分配方式,虚拟内存是是一种逻辑上扩充内存空间的方式,只不过虚拟内存使用请求式的分页或分段而已。
  • 连续分配和基本的分页分段只有分配策略,如首次适应算法,临近适应算法等,但是没有特别的置换策略,不存在程序还在运行时的置换,都是一次性全部分配,能分配则运行,运行完一次性全部卸载。而像请求式分页这种它的分配策略还牵扯到置换策略。
  • 现代操作系统的内存访问路径大致是这样的:首先程序给出指令或者数据的虚拟内存相对地址,然后通过地址重定位得到虚拟内存的实际地址,然后去查询段表或者页表是否已经加载到了内存中,如果加载到了内存中,且在cache中,则直接从cache返回,如果在内存而不再cache,则从内存中加载并更新cache(可能发生替换),如果不在内存中,则从页表项中找到外存地址,加载到内存中(可能发生替换),再返回给CPU并更新cache。

程序执行的过程

创建进程首先要将程序和数据装入内存,将用户源程序变为可在内存中执行的程序,通常需要以下几个步骤:

  • 编译。由编辑程序将用户源代码编译成若干目标模块。
  • 链接。有链接程序将编译后的一组目标模块及它们所需的库函数链接到一起,形成一个完整地装入模块。
  • 装入。将装入模块装入内存运行

    这里还需要有两点说明:
  • 绝对装入和可重定位装入都需要程序一开始就全部装入内存,所以他们的链接方式不可以是运行时动态链接。
  • 绝对装入和可重定位装入都是直接就生成了所有的物理地址,作业装入内存时,就需要分配要求的全部内存空间,整个运行期间不能在内存中移动,而绝对装入逻辑地址与物理地址完全相同,静态重定位程序中的地址是相对于起始地址的,在装入时一次性完成地址变换,故只能采取连续分配的方式。而动态运行时装入可以将程序分配到不连续的内存。

编译

编译过程的结果是多个目标模块,每个模块内的地址都是从0开始的相对地址。

链接

链接的结果是一个完整的装入模块,装入模块中的地址也是从0开始的相对地址。
程序的链接方式有三种:

静态链接

在程序运行之前,先将各目标模块及它们所需的库函数链接成一个完整地装配模块,以后不再拆开,将几个目标模块装配成一个装入模块时,需要解决两个问题:

  • 修改相对地址。编译后的所有目标模块都是从0开始的相对地址,当链接成一个装入模块时要修改相对地址
  • 变换外部调用符号,将每个模块中所用的外部调用符号也变为相对地址。

装入时动态链接

将用户源程序编译后的一组目标模块装入内存时,采用边装入,边链接的方式。其优点是便于修改和更新,实现对目标模块的共享。

运行时动态链接

对某些目标模块的链接,是在程序执行中需要该目标模块时才进行的。凡是在执行过程中未被用到的目标模块,都不会被调入内存和被链接到装入模块上。其优点是能加快程序的装入过程,还可节约大量的内存空间。

装入

内存的装入模块在装入内存时,同样有三种方式:

绝对装入

绝对装入的方式只适用于单道程序环境。在编译时,若知道程序将驻留在内存的某个位置,则编译程序将产生绝对地址的目标代码。绝对装入程序按照装入模块中的地址,将程序和数据装入内存。由于程序中的逻辑地址和实际内存地址完全相同,因此不需要对程序和数据进行修改。
程序中所用的绝对地址,可以在编译或汇编时给出,也可以由程序员直接赋予。

可重定位装入

在多道程序环境下,多个目标模块的起始地址通常都从0开始,程序中的其他地址都是相对于起始地址的,因此可采用可重定位装入方式。根据内存的当前情况,将装入模块装入内存的合适位置。
在装入时对目标程序中的指令和数据地址的修改过程称为重定位,又因为地址变换通常是在程序装入时一次完成的,故称为静态重定位。
当一个作业装入内存时,必须给它分配要求的全部内存空间,若没有足够的内存,则无法装入。此外,作业一旦进入内存,整个运行期间就不能在内存中移动,也不能再申请到内存空间。

动态运行时装入

也称为动态重定位。程序在内存中若发生移动,则需要采用动态的装入方式。装入程序把装入模块装入内存后,并不立即把装入模块中的相对地址转换为绝对地址,而是把这种地址转换推迟到程序真正执行时才进行。因此,装入内存后的所有地址仍然为相对地址。这种方式需要一个重定位寄存器的支持才可以。
动态重定位的优点:

  • 可以将程序分配到不连续的存储区。
  • 在程序运行之前可以只装入部分代码即可投入运行。然后在程序运行期间,根据需要动态申请分配内存。
  • 便于程序段的共享。

逻辑地址与物理地址

编译后,每个目标模块都从0号单元开始编址,这称为该目标模块的相对地址(或逻辑地址)。当链接程序将各个模块链接成一个完整的可执行程序时,链接程序顺序依次按各个模块的相对地址构成统一的从0号单元开始编址的逻辑地址空间(或虚拟地址空间)。
进程在运行时,看到和使用的地址都是逻辑地址。用户程序和程序员只需要知道逻辑地址,而内存管理机制则是完全透明。不同进程可以有完全形同的逻辑地址,因为这些逻辑地址可以映射到主存的不同位置。
物理地址是指内存中物理单元的集合,它是地址转换的最终地址。进程在执行时执行指令和访问数据,最后都要通过物理地址从主存中存取。当装入程序将可执行代码装入内存时,必须通过地址转换将逻辑地址转换为物理地址,这个称为地址重定位。

进程的内存映像

不同于存放在硬盘上的可执行程序文件,当一个程序调入内存运行时,就构成了进程的内存映像。一个进程的内存映像一般有几个要素:

  • 代码段:即程序的二进制代码,代码段是只读的,可以被多个进程共享。
  • 数据段:即程序运行时加工处理的对象,包括全局变量和静态变量。
  • 进程控制块(PCB):存放在系统区。操作系统通过PCB来控制和管理进程。
  • 堆:用来存放动态分配的变量。通过调用malloc函数动态地向高地址分配空间。
  • 栈:用来实现函数的调用。从用户空间的最大地址往低地址方向增长。
    代码段和数据段在程序调入内存时就指定了大小。而栈和堆则是会动态地扩展和收缩的。

内存保护

确保每个进程都有一个独立的内存空间。内存分配前,需要保护操作系统不受用户进程的影响,同时保护用户进程不受其他用户进程的影响。内存保护有两种方法:

  • 在CPU中设置一对上,下限寄存器,存放用户作业在主存中的下限和上限地址,每当CPU要访问一个地址时,分别和两个寄存器的值比较,判断有无越界。
  • 采用重定位寄存器(基地址寄存器)和界地址寄存器(限长寄存器)。重定位寄存器包含最小的物理地址,界地址寄存器含逻辑地址的最大值。这两个寄存器需要使用特权指令才能修改。

内存共享

并不是所有进程的内存空间都适合共享,只有只读区域才可以共享。可重入代码又称为纯代码,是一种允许多个进程同时访问但不允许被修改的代码。

内存的分配与回收

在操作系统由单道向多道发展时,存储管理方式便由单一连续分配发展到固定分区分配,为了适应不同大小程序的要求,又从固定分区分配发展到动态分区分配。为了更好地利用内存,进而从连续分配方式发展到了离散的分配方式——页式存储管理。