内存管理的算法有很多,从简单的裸机方法,到分页分段策略,各种方法都有其优缺点。为特定系统选择内存管理方法取决于很多因素,特别是系统的硬件设计,尽管现在的设计已经将硬件和操作系统紧密的结合在一起,但是很多算法仍然需要硬件的支持。
前言
内存是现代计算机的运行的中心,内存由很大一组字或字节组成,每个字节或字都有他们的自己的地址,CPU根据程序计数器(PC)的值从内存中提取指令,这些指令可能会进一步对特定内存地址的读取和写入。
基本硬件
CPU 所能直接访问的存储设备只有内存和处理器内的寄存器,机器指令可以用内存地址作为参数,而不能用磁盘地址作为参数,因此,执行指令以及指令使用的数据必须在这些直接可访问的存储设备上,如果数据不存在内存中,那么在CPU使用前必须先把数据移动到内存中。
高速缓存
CPU内置存储器通常可以在一个CPU 时钟周期内完成,对于寄存器中的内容,绝大多数CPU可以在一个时钟周期内解析并执行一个或多个指令,而对于内存(其访问通过内存总线上的事务进行)就不行了,完成内存访问可能需要多个CPU时钟周期,由于没有数据以便完成正在执行的指令,CPU通常需要暂停(stall)。由于内存访问频繁,这种情况是难以忍受的,解决方法就是在CPU 与内存之间,增加高速内存,这种协调速度差异的内存缓存区,称为高速缓存
除了保证访问物理内存的相对速度之外,还要确保操作系统不被用户进程访问,以及确保用户进程不被其他用户进程访问。这中保护可以通过硬件来保护,硬件的实现方案有许多种,这里看其中的一种。
首先需要确保每个进程都有独立的内存空间。为此,需要确定进程可以访问合法的地址访问,并确保进程只访问其合法地址。
通过两个寄存器即基地址寄存器和界限地址寄存器,可以实现这种保护,基地址寄存器 含有最小的合法物理内存地址,而界限地址寄存器决定了范围的大小。
例如: 如果基地址寄存器为400040,而界限寄存器为120900, 那么程序可以合法访问从 400040 到 520940(含)的所有地址。
内存保护的实现,是通过CPU硬件地址对用户模式所产生的每一个地址与寄存器的地址进行比较来完成的,如用户模式下执行的程序试图访问操作系统内存或其他用户内存。则会陷入到操作系统,并作为致命的错误处理。如下图:
只有操作系统可以通过特殊的特权指令来加载基地址寄存器和界限地址寄存器。由于特权指令只可在内核模式下执行,而只有操作系统在内核模式下执行,所以只有操作系统可以加载基地址寄存器和界限地址寄存器。这种方案允许操作系统修改这两个寄存器的值,而不允许用户程序修改它们。
操作系统在内核模式下执行,可以无限制的访问操作系统和用户的内存,因此操作系统可以将用户程序装入用户内存,在出错时输出这些程序,访问并修改系统调用的参数等。
地址绑定
通常,程序以二进制可执行文件的形式在磁盘上存储,为了执行,程序被调入内存并在进行空间中,根据所使用的内存管理方案,进程在执行时可以在磁盘和内存之间移动。在磁盘上等待调入内存以便执行的进程形成输入队列
通常的步骤是从输入队列中选取一个进程并装入内存,进程在执行时,会访问内存中的指令和数据,最后进程终止,其地址空间将被释放。
许多系统允许用户进程放在物理内存的任意位置,这种组织方式会影响用户程序能够使用的地址空间。在绝大多数情况下,用户程序在执行前,需要经过几个步骤,其中有的是可选的,在这些步骤中,地址可能有不同的表示形式,源地址中的地址通常是用 符号 来表示的(如 count),编译器通常将这些符号地址绑定在可重入定位的地址(如”从本模块开始的第 14 个字节”)。链接程序或加载程序再将这些可重定位的地址绑定成绝对地址(如 74014)。每次绑定都是从一个地址空间到另一个地址空间的映射。
通常,将指令与数据绑定到内存地址有以下的几种情况:
- 编译时(compile time ): 如果在编译时就知道进程将在内存中驻留地址,那么就可以生成绝对代码。例如,如果事先就知道用户进程驻留在内存地址 R 处,那么所生成的编译代码就可以从该位置开始并向后扩展,如果将来开始地址发生变化,那就必须重新编译代码。
- 加载时(load time): 如果在编译时并不知道进程将驻留在内存的什么地方,那么编译器就必生成可重定位代码(relocatable code)。对于这种情况,最后绑定会延迟到加载时才进行,如果开始地址发生变化,只需重新加载用户代码以引入改变值。
- 执行时(execution time): 如果进程在执行时可以从一个内存段移动到另一个内存段,那么绑定必须延迟到执行时才进行。采用这种方法需要特定的硬件,绝大多数通用计算机操作系统采用这种方法。
逻辑地址空间与物理地址空间
CPU所生成的地址通常称为 逻辑地址(logical address), 而内存单元所看到的地址(即加载到内存地址寄存器(memory-address register)中的地址)通常称为 物理地址(physical address)。
编译和加载时的地址绑定方法生成相同的逻辑地址和物理地址,但是,执行时的地址绑定方案导致不同的逻辑地址和物理地址,对于这种情况,通常称逻辑地址为虚拟地址。
运行时从虚拟地址到物理地址的映射是被称为内存管理单元(memory-management unit, MMU)的硬件设备来完成的。有多种可选择的方法来完成这种映射,在此,用一个简单那个的MMU 方案来实现这种映射。这个也是前面所说的基地址寄存器方案的推广,基地址寄存器在这里称为重定位寄存器(relocation register)。 用户进程所生成的地址在交送内存之前,都将加上重定位寄存器的值。例如: 如果基地址为14000,那么用户对位置 0 的访问将动态地重定位 为14000;对地址346的访问将映射为位置 14346。
用户程序决不会看到真正的物理地址。程序可以创建一个指向位置346的指针,将它保存在内存中,使用它,将与其他地址进行比较,等等,所有这些操作都是基于346进行的,只有当它作为内存地址时(例如,在间接加载和存储时),它才进行相对于基地址寄存器的重定位,用户程序处理逻辑地址,内存映射硬件将逻辑地址转变为物理地址。
现在有两种不同的地址:逻辑地址(范围为 0 到 max)和物理地址(范围为 R + 0到 R + max,其中R为基地址),用户只生成逻辑地址,且认为进程的地址空间为 0 到 max。用户提供逻辑地址,这些地址在使用前必须映射 到物理地址。
动态加载
前面讨论的是一个进程的整个程序和数据必须处于物理内存中,以便执行。因此进程的大小受物理内存大小的限制。为了获得更好的内存空间使用率,可以使用动态加载(dynamic loading)。采用动态加载时,一个子程序只有在调用时才被加载,所有的子程序都可以重定位的形式保存在磁盘上,主程序装入内存并执行。当一个程序需要调用另一个自称序时,调用子程序,并更新程序的地址表以反映这一变化。接着,控制传递给新加载的子程序。
优点:
- 不用的子程序不会被加载。如果大多数代码需要用来处理异常情况,如错误处理,那么这种方法就特别的有用,对于这种情况,虽然总体上程序比较大,但是所使用的部分(即加载的部分)可能小很多。
- 动态加载不需要操作系统提供特别的支持。
动态链接与共享库
动态链接是将动态链接的系统库 由加载程序合并到二进制程序镜像中。动态链接的概念与动态加载相似。只是这里不是将加载延迟到运行时,而是将链接延迟到运行时。这一特点通常用于系统库,如语言库子程序库,没有这一点,系统上的所有程序都需要一份语言库的副本,而这一要求是浪费了磁盘空间和内存空间。
如果有动态链接,二进制镜像中对每个库程序的引用都有一个存根,
存根是一小段代码,用来指出如何定位适当的内存驻留库程序,或如果该程序不在内存时应该如何装入库。
当执行存根时,它首先检查所需的子程序是否已存在内存中。如果不在,就将子程序装入内存,不管如何,存根都会用子程序地址来替换自己,并开始执行子程序。因此,下次再执行该子程序代码时,就可以直接进行,而不会因动态链接产生任何开销。采用这种方案,使用语言库的所有进程只需要一个库代码副本就可以了。
与动态加载不同,动态链接通常需要操作系统帮助。如果内存中进程是彼此保护的,那么操作系统才可以检查所需子称序是否在其他进程内存空间内,或是允许多个进程访问同一内存地址。