文章目录
- 0.前言
- 8.1 背景
- 8.1.1 基本硬件
- 8.1.2 地址绑定
- 8.1.3 逻辑地址空间和物理地址空间
- 8.1.4 动态加载(dynamic loading)
- 8.1.5 动态链接(dynamically linking)与共享库
- 8.3 连续内存分配(contiguous memory allocation)
- 8.3.1 内存映射与保护
- 8.3.2 内存分配
- 8.3.3 碎片(fragmentation)
- 8.4 分页(paging)
- 8.4.1 基本方法
- 8.4.2 硬件支持
- 8.4.3 保护
- 8.4.4 共享页
- 8.5 页表结构
- 8.5.1 层次页表
- 8.5.2 哈希页表(hashed page table)
- 8.5.3 倒置页表(inversed page table)
- 8.6 分段(segmentation)
- 8.6.1 基本方法
- 8.6.2 硬件
0.前言
从第5章,我们讨论了一组进程如何共享一个CPU。正是由于CPU调度,我们可以调高CPU的利用率和计算机响应用户的速度。然而,为了实现性能的改进,应将多个进程保存在内存中;也就是说,必须共享内存。
本章讨论内存管理的各种方法。内存管理算法很多:从原始的裸机方法,到分页和分段的方法。每种方法都有各自的优点和缺点。为塔顶内存选择内存管理方法取决于很多因素,特别是系统的硬件设计。正如将会看到的,许多算法都会需要硬件支持,导致许多操作系统内存管理与系统硬件相结合。
本章目标:
- 详细描述内存硬件的各种组织方法
- 探讨进程内存分配的各种技术
- 详细探讨现代计算机系统的分页如何工作
8.1 背景
内存是现代计算机运行的中心。内存有很大一组字或字节组成,每个字或字节都有它们自己的地址。CPU根据程序计数器(PC)的值从内存中提取指令,这些指令可能会引起进一步对特定内存地址的读取和写入。
一个典型指令执行周期,首先从内存中读取指令。接着该指令被解码,且可能需要从内存中读取操作数。在指令对操作数执行后,其结果可能被存回到内存。内存单元只看到地址流,而并不直到这些地址是如何产生的(由指令计数器、索引、间接寻址、实地址等)或它们是什么地址(指令或数据)。
8.1.1 基本硬件
CPU所能直接访问的存储器只有内存和处理器内的寄存器。机器指令可以用内存地址作为参数,而不能用磁盘地址作为参数。如果数据不在内存中,那么CPU使用前必须先把数据移到内存中。
CPU内置寄存器通常可以在一个CPU时钟周期内完成访问。对于寄存器的内容,绝大多数CPU可以在一个时钟周期内解析并执行一个或多个指令,而对于内存就不行。完成内存访问需要多个CPU时钟周期,由于没有数据以便完成正在执行的指令,CPU通常需要暂停(stall)。由于内存访问频繁,这种情况是难以忍受的,解决方法是在CPU与内存之间增加高速内存。这种协调速度差异的内存缓冲区,称为高速缓存(cache)。(这一方面是计算机组成原理的内容)
除了保证访问物理内存的相对速度之外,还要确保操作系统不会被用户进程所访问,以及确保用户进程不会被其他用户进程访问。这种保护可通过硬件来实现,硬件实现由许多方法,将在之后讨论。
其中一种可能方案为:
首先确保每个进程都有独立的内存空间,为此,需要确定进程可访问的合法地址的范围,并确保进程只能访问其合法地址。通过**基地址寄存器(base register)和界限地址寄存器(limit register)**可以实现这种保护。
基地址寄存器(base register)含有最小的物理内存地址,界限地址寄存器(limit register)决定了范围的大小。例如:如果基地址寄存器为300040而界限寄存器为120900,那么程序可以访问从300040到420940的所有地址。
内存空间保护的实现,是通过CPU硬件对用户模式所产生的每个地址与寄存器的地址进程比较来完成的。如果访问了不该访问的地址,则会陷入到操作系统中,并作为致命错误处理。
只有操作系统可以通过特殊的特权指令来加载基地址寄存器和界限地址寄存器。由于特权指令只可在内核模式下执行,而只有操作系统在内核模式下执行,所以只有操作系统可以加载基地址寄存器和界限地址寄存器。这种方案允许操作系统修改两个寄存器的值,而不允许用户程序去修改他们。
操作系统在内核模式下,可以无限制地访问操作系统和用户内存。因此操作系统可以将用户程序装入用户内存,在出错时输出这些程序,访问并修改系统调用的参数等。
8.1.2 地址绑定
通常,程序以二进制可执行文件的形式存储在磁盘上。为了执行,程序被调入内存并放入进程空间内。
根据所使用的内存管理方案,进程在执行时,可以在磁盘和内存之间移动。在磁盘上等待调入内存以便执行的进程形成输入队列(input queue)。
通常的步骤是从输入队列中选取一个进程并装入内存。进程在执行时,会访问内存中的指令和数据。最后,进程终止,其地址空间将被释放。
许多系统允许用户进程放在物理地址的任意位置。这种组合方式会影响用户程序能够使用的地址空间。在绝大多数情况下,用户程序在执行前,会经过好几个步骤,在这些步骤中,地址可能有不同的表示形式,源程序中的地址通常是用符号(如count)来表示,编译器通常将这些符号地址绑定(bind)在可重定位的地址(如:从本模块开始的第14字节)。链接程序或加载程序再将这些可重定位的地址绑定成绝对地址(如74014)。每次绑定都是从一个地址空间到另一地址空间的映射。
通常,将指令与数据绑定到内存地址有以下几种情况:
编译时(compile time): 如果编译时就知道进程将在内存中的驻留地址,那么就可以生成绝对代码(absolute code)。如果将来开始地址发生变化,那么就必须重新编译代码。
加载时(load time): 当编译时不知道进程将驻留在内存的什么地方,那么编译器就必须生成可重定位代码(reloadable code)。绑定会延迟到加载时才进行。如果开始地址发生变化。只需要重新加载用户代码已引入改变值。
执行时(execution time): 如果进程在执行时可以从一个内存段移到另一个内存段,那么绑定必须延迟到执行时才发生。绝大多数通用计算机操作系统采用这种方法。
8.1.3 逻辑地址空间和物理地址空间
生成的地址通常称为逻辑地址(logical address),而内存单元所看到的地址(即加载到内存地址寄存器(memory-address register)中的地址)通常称为物理地址(physical address)。
编译和加载时的地址绑定方法生成相同的逻辑地址和物理地址。但是,执行时的地址绑定方案导致不同的逻辑地址和物理地址。对于这种情况,通常称逻辑地址为虚拟地址(virtual address)。由程序所生成的所有逻辑地址称为逻辑地址空间(logical address space),与这些逻辑地址相对应的物理地址的集合称为物理地址空间(physical address space)。
运行时从虚拟地址到物理地址的映射由被称为**内存管理单元(memory-management unit,MMU)**的硬件设备来完成。有很多可选择的方法来完成这种映射,如使用一个简单的MMU方案来实现这种映射,这是一种基地址寄存器方案的推广,基地址寄存器在这里称为重定位寄存器(relocation register),用户进程所生成的地址在送交内存之前,都加上重定位寄存器的值。
假如,基地址为14000,那么用户对地址346的访问将映射为地址14346。
用户程序绝对不会看到真正的物理地址。如,程序可以创建一个指向位置346的指针,将他保存在内存中,使用它,与其他地址进行比较等等,所有这些操作都是基于346进行的。只有当它作为内存地址时(例如,在简介加载和存储时),它才进行相对于基地址寄存器的重定位。用户程序处理逻辑地址时,内存映射硬件将逻辑地址转变为物理地址。所引用的内存地址只有在引用时才最后定位。
逻辑地址空间绑定到单独的一套物理地址空间,这一概念对内存的管理至关重要。
8.1.4 动态加载(dynamic loading)
一个进程的整个程序和数据如果都必须处于物理内存中,则进程的大小受物理内存大小的限制。
为了获得更好的内存空间使用率,使用动态加载(dynamic loading),即一个子程序只有在调用时才被加载。
所有的子程序都以可重定位的形式保存在磁盘上。主程序装入内存并执行。当一个子程序需要调用另外一个子程序的时候,调用子程序首先检查另一个子程序是否已经被加载。如果没有,可重定位的链接程序将用来加载所需要的子程序,并更新程序的地址表以反应这一变化。接着控制传递给新加载的子程序。
动态加载的优点是不用子程序绝不会被加载,如果大多数代码需要用来处理异常情况,如错误处理,那么这种方法特别有用。对于这种情况,虽然总体上程序比较大,但是所使用的部分可能小很多。
动态加载不需要操作系统提供特别的支持。利用这种方法来设计程序主要是用户的责任。
8.1.5 动态链接(dynamically linking)与共享库
有的操作系统只支持**静态链接(static linking)**此时系统语言库的处理与其他目标模块一样,由加载程序合并到二进制程序镜像中。
动态链接的概念与动态加载相似。只是这里不是将加载延迟到运行时,而是将**链接延迟到运行时。**这一特点通常用于系统库,如语言子程序库。没有这一点,系统上的所有程序都需要一份语言库的副本,这一需求浪费了磁盘空间和内存空间。
如果有动态链接,二进制镜像中每个库程序的应用都有一个存根(stub)。存根是一小段代码,用以指出如何定位适当的内存驻留的库程序,或如果该程序不在内存中应如何安装入库。不管怎样,存根会用子程序地址来代替自己,并开始执行子程序。因此,下次再执程序代码时,就可以直接进行,而不会因动态链接产生任何开销。采用行该子这种方案,使用语言库的所有进程只需要一个库代码副本就可以了。
动态连接也可用于库更新。一个库可以被新的版本所替代,且使用该库的所有程序会自动使用新的版本。没有动态链接,所有这些程序必须重新链接以便访问。
为了不使程序错用新的、不兼容版本的库,程序和库将包括版本信息。多个版本的库都可以装入内存,程序通过版本信息来确定使用哪个库副本。
因此,只有用新库编译的程序才会收到新库的不兼容变化影响。在新程序装入之前所链接的其他程序可以继续使用老库。这种系统也称为共享库。
与动态加载不同,动态链接通常需要操作系统帮助。如果内存中的进程是彼此保护的,那么只有操作系统才可以检查所需子程序是否在其他进程内存空间内,或是允许多个进程访问同一内存地址。
8.3 连续内存分配(contiguous memory allocation)
内存必须容纳操作系统和各种用户进程,因此应该尽可能有效地分配内存的各个部分。
内存通常分为两个区域:一个用于驻留操作系统,一个用于用户进程。操作系统可以位于低内存或高内存,影响这一决定的主要因素是中断向量的位置。由于中断向量通常位于低内存,因此程序员通常将操作系统放到低内存。
通常需要将多个进程同时放入内存中,因此需要考虑如何为输入队列中需要调入内存的进程分配内存空间。
采用连续内存分配(contiguous memory allocation)时,每个进程位于一个连续的内存区域。
8.3.1 内存映射与保护
通过采用重定位寄存器和界限地址寄存器可以实现保护。
重定位寄存器含有最小的物理地址值;界限地址寄存器含有逻辑地址的范围值。
这样每个逻辑地址必须小于界限地址寄存器。MMU动态第将逻辑地址加上重定位寄存器的值后影射成物理地址。映射后的物理地址再送交内存单元。
当CPU调度器选择一个进程来执行时,作为上下文切换工作的一个部分,调度程序会用正确的值来初始化重定位寄存器和界限地址寄存器,由于CPU所产生的每一地址都需要与寄存器进程核对,所以可以保证操作系统和其他用户程序和数据不受该进程运行所影响。
重定位寄存器机制为允许操作系统动态改变提供了一个有效方法。如某驱动程序(或其他操作系统服务)不常使用便可以不必在内存中,这类代码有时称为暂时(transient)操作系统代码,它们根据需要调入或调出。因此,使用这种代码可以在程序执行时动态改变操作系统的大小。
8.3.2 内存分配
最简单的内存分配方法之一是将内存分为多个固定大小的分区(partition)。每个分区只能容纳一个进程。那么多道程序的程度会受分区数限制。如果使用这种多分区方法(multiple-partition method),当一个分区空闲时,可以输入队列中选择一个进程,以调入到空闲分区。当进程终止时,其分区可以被其他进程所使用。这种方法现在已不再使用。对于固定分区方案的推广(称为MVT),它主要用于批处理环境。也可用于纯分段内存管理的分时操作系统。
在**可变分区(variable-partition)**方案中,操作系统有一个表,用于记录那些内存可用和哪些内存已被占用。一开始,所有内存都可用于用户进程,因此可以作为一大块可用内存,称为孔(hole),当新进程需要内存时,为该进程查找足够大的孔,如果找到,可以从该孔进程分配所需的内存,孔内未分配的内存可用于下次再用。
随着进程进入系统,它们将被加入输入队列中。操作系统根据调度算法来对输入队列进行排序。内存不断地分配给进程,直到下一个进程的内存需求不能满足为止,如果没有足够大的孔来装入进程,操作系统可以等到有足够大的空间,或者往下扫描输入队列以确定是否其他内存需求较小的进程可以被满足。
通常,一组不同大小的孔分散在内存中。当新进程需要内存时,系统为进程查找足够大的孔。如果孔太大,那么就分成两块:一块分配给新进程,另一块还回到孔集合,当进程终止时,它将释放其内存,改内存将还给孔集合。如果孔与其他孔相邻,那么将这些孔合并为大孔。这时,系统可以检查是否有进程在等待内存空间,新合并的内存空间是否满足等待进程。
这种方法是通用动态存储分配问题的一种情况(根据一组空闲孔来分配大小为n的请求),这个问题有许多解决方法。从一组可用孔中选择一个空闲孔的最为常用方法有首次适应(first-fit)(第一个对够大的孔)、最佳适应(best-fit)(最小的最够大的孔)、最差适应(worst-fit)(分配最大的孔)。
- 首次适应(first-fit):分配第一个足够大的孔,查找可以从头开始,也可以从上次首次适应结束时开始。一旦找到足够大的空闲孔,就可以停止。
- 最佳适应(best-fit):分配最小的足够大的孔。必须查找整个列表,除非列表按照大小排序。这种方法可以产生最小剩余孔。
- 最差适应(worst-fit):分配最大的孔,同样必须查找整个列表,除非列表按照大小排序。这种方法可以产生最大剩余孔。该孔可能比最佳适应方法产生的最小剩余孔更有用。
模拟结果显示:首次适应和最佳适应方法在执行时间和利用空间方面都好于最差适应方法。首次适应和最佳适应方法在利用空间方面难分伯仲,首次适应方法更快些。
8.3.3 碎片(fragmentation)
首次适应和最佳适应算法都有外部碎片问题(external fragmentation)。随着进程装入和移出内存,空闲内存空间被分割为小分段,
当所有总的空用内存之和可以满足请求,但并不连续时,这就出现了外部碎片问题。最坏的情况下,每两个进程之间就有空闲块(或浪费)。如果这些内存是一整块,那么就可以再运行多个进程。
在首次适应和最佳适应之间的选择可能会影响碎片的量。另一个影响因素是从空闲块的哪端开始分配。不管使用哪种算法,外部碎片始终是个问题。
根据内存的总大小和平均进程大小的不同,外部碎片化的重要程度也不同。例如,对采用首次适应方法的统计说明,对于首次适应方法不管怎么优化,假定N个可分配块,那么可能有0.5N个块为外部碎片。即1/3内存可能不能使用,这一特性称为50%规则。
内存碎片可以是内部的,也可以是外部的。如果内存以固定大小的块为单元来分配,进程所分配的内存可能比所要的要大。这两个数字之差称为内部碎片(internal fragmentation)这部分内存在分区内,但又不能使用。
一种解决外部碎片问题的方法是紧缩(compaction),紧缩的目的是移动内存内容,以便所有空闲空间合并成一整块。但是紧缩并非总是可能的。如果重定位是静态的,并且在汇编时或装入时进行的,那么就不能紧缩。紧缩仅在重定位是动态的并在运行时可采用。如果地址被动态重定位,可以首先移动程序和数据,然后再跟据新基地址基地的值来改变址寄存器。如果采用紧缩,还要评估其开销,最简单的合并算法是简单地将所有进城移到内存的一端,而将所有的孔移到内存的另一端,以生成一个大的空闲块。这种方案开销较大。
另一种解决方法外部碎片问题的方法是允许物理地址为非连续的。这样只要有物理内存就可以为进程分配。这种方案有两种互补的实现技术:分页和分段。这两种技术也可以合并。
分页产生内部碎片,分段产生外部碎片
8.4 分页(paging)
分页(paging)内存管理方案允许进程的物理地址空间可以使非连续的。分页避免了将不同大小的内存块匹配到交换空间上,前面叙述的内存管理方案都有这个问题,当位于内存中的代码和数据需要换出时,必须现在备份存储上找到空间,这是问题就产生了。备份存储也有前面所述的与内存相关的碎片问题,只不过访问更慢。
传统上,分页支持一直是由硬件来处理的。最近的设计是通过将硬件和操作系统相配合来实现分页。
8.4.1 基本方法
实现分页的基本方法设计将物理内存分为固定大小的块,称为帧(frame);而将逻辑内存也分为同样大小的块,称为页(page)。当需要执行进程时,其页从备份存储中调入到可用的内存帧中。备份存储也分为固定大小的块,其大小与帧相同。
设页大小为a,根据页号p得到基地址f,页偏移为d,则物理地址为f∗a+d
分页是一种动态重定位。每个逻辑地址有分页硬件绑定为一定的物理地址。采用分页类似于使用一组基(重定位)地址寄存器,每个基地址对应这一个内存帧。
采用分页技术不会产生外部碎片:每个帧都可以分配给需要它的进程。不过分页有内部碎片。
每个页表的条目通常为4B,不过这是可变的,一个32位的条目可以指向232个物理帧的任何一个,如果帧为4KB,那么具有4B条目的系统可以访问244B大小。
当系统进程需要执行时,它将检查该进程的大小(按页计算)。进程的每页都需要一帧。因此,如果进程需要n页,那么内存中至少应有n个帧。如果有那么就分配给新进程。进程的第一页装入一个已分配的帧,帧号放入进程的页表中。下一页分配给另一帧,其帧号也放入进程的页表中。
分页的一个重要特点是用户视角的内存和实际的物理内存的分离。用户程序将内存作为一整块来处理,而且它只包括这一个进程。事实上,一个用户程序与其他程序一起,分布在物理内存上。
用户视角的内存和实际的物理内存的差异是通过地址转换硬件协调的。 逻辑地址转换为物理地址,这种映射是用户所不知道的,但是受操作系统所控制。注意用户进程根据定义是不能访问非它所占用的内存的。它无法访问其页表所规定之外的内存,页表只包括进程所拥有的那些页。
由于操作系统管理物理内存,它必须知道物理内存的分配细节:哪些帧已占用,哪些帧可用,总共有多少帧等。这些信息通常保存在帧表中。在帧表(frame table)中,每个条目对应一个帧,以表示该帧是空闲还是已占用,如果被占用,是被哪个进程的哪个页所占用。
另外,操作系统必须意识到用户进程是在用户空间内执行,且所有逻辑地址必须映射到物理地址。如果用户执行一个系统调用(如进行I/O),并提供地址作为参数,那么这个地址必须映射成物理地址。操作系统为每个进程维护一个页表副本,就如同它需要维护指令计数器和寄存器的内容一样。当操作系统必须手工将逻辑地址映射成物理地址时,这个副本可用来将逻辑地址转换为物理地址。当一个进程可分配到CPU时,CPU调度程序可以根据该副本来定义硬件页表。因此,分页增加了切换时间。
8.4.2 硬件支持
每个操作系统都有自己的方法来保存页表。绝大多数都为每个进程分配一个页表。页表的指针与其他寄存器的值(如指令计数器)一起存入进程控制块。当调度程序需要启动一个程序时,它必须首先装入用户寄存器,并根据所保存的用户页表来定义正确的硬件页表值。
页表的硬件实现有很多方法。最为简单的是将页表作为一组专用寄存器(register)来实现。这些寄存器应用高速逻辑电路来构造,以便有效的进行分页地址的转换。由于对内存的每次访问都要经过分页表,因此效率很重要。CPU装入或修改页表寄存器的指令是特权级的,因此只有操作系统才可以修改内存映射图。
如果页表比较小(例如256个条目),页表使用寄存器还是比较合理的。但是,绝大多数当代计算机都允许页表非常大(如100万个条目)。对于这些机器,采用快速寄存器来实现页表就不可行了,因而需要将页表放在内存中,并将页表基寄存器(page-table base register,PTBR) 指向页表。改变页表,只需要改变这一寄存器就可以了,这也大大降低了切换时间。
采用这种方法的问题是访问用户内存位置需要一些时间。如果要访问位置i,那么必须先用PTBR中的值再加上页号i的偏移,来查找页表。这一任务需要内存访问,根据所得的帧号,再加上页偏移,就得到了真实的物理地址,接着访问内存中所需的位置。采用这种方法,访问一个字节需要两次内存访问(一次用于页表条目,一次用于字节),这样内存访问的速度就减半,在绝大多数情况下这种延迟是无法忍受的。
对这一问题的标准解决方案是采用小但专用快速的硬件缓冲,这种缓冲称为转换表缓冲区(translation look-aside buffer,TLB)。TLB是关联的快速内存**。TLB条目由两部分组成:键(标签)和值。** 当关联内存根据给定值查找时,它会同时与所有键进行比较。如果找到条目,那么就得到相应的值域。这种查找方式比较快,不过硬件也比较昂贵,通常,TLB中的条目数并不多,通常在64~1024之间。
TLB与页表一起按如下方式使用:TLB只包括也表中的一小部分条目。当CPU产生逻辑地址后,其页号提交给TLB。如果页码不在TLB中(称为TLB失效),那么就需要访问页表。将页号和帧号增加到TLB中。如果TLB中的条目已满,那么操作系统会选择一个来替换。替换策略有很多,从最近最少使用替换(LRU)到随机替换等。另外,有的TLB允许有些条目固定下来。通常内核代码的条目是固定下来的。
有的TLB在每个TLB条目中还保存地址空间标识码(address-space identifier,ASID)。ASID可用来唯一标识进程,并为进程提供地址空间保护。当TLB试图解析虚拟页号时,它确保当前运行进程的ASID与虚拟页相关的ASID相匹配。如果不匹配,那么就作为TLB失效。除了提供地址空间保护外,ASID允许TLB同时包含多个进程的条目。如果TLB不支持独立的ASID,每次选择一个页表时(例如,上下文切换时),TLB就必须被冲刷(flushed)或删除,以确保下一个进程不会使用错误的地址转换。
页号在TLB中被查找到的百分比称为命中率。
80%的命中率意味着有80%的时间可以在TLB中找到所需的页号。
假如查找TLB需要20ns,访问内存需要100ns,如果访问位于TLB中的页号,那么采用内存映射访问需要120ns。如果不能在TLB中找到(20ns),那么必须先访问位于内存中的页表得到帧号(100ns),并进而访问内存中所需字节(100ns),这总共需要220ns。为了得到有效内存访问时间,必须根据概率对每种情况进行加权。
有效内存访问时间 =0.80∗120+0.2∗220=140(ns)
对于这种情况,现在内存访问速度要慢40
如果命中率为98%,那么
有效内存访问时间 =0.98∗120+0.02∗220=122(ns)
由于提高了命中率(Hit ratio),内存访问时间只慢了22%
8.4.3 保护
在分页环境下,内存保护是通过与每个帧相关联的保护为来实现的。通常,这些位保存在页表中。
可以用一个位来定义一个页是可读写还是只读的。每次地址引用都要通过页表来查找正确的帧码,在计算物理地址的同时,可以检查保护位来验证。对只读页进行写操作会向操作系统产生硬件陷阱(trap)(或内存保护冲突)。
可以很容易的扩展这一方法以提供更细致的保护,可以创建硬件以提供只读、读写、只执行保护。或者,通过为每种访问情况提供独立保护位,实现这些访问的各种组合;非法访问会被操作系统捕捉到。
还有一个位通常与页表中的每一条目相关联:有效-无效位。有效,表示相关的页在进程的逻辑地址空间内,因此是合法的页;无效,表示相关的页不在进程的逻辑地址空间内。通过使用有效-无效位可以捕捉非法地址。操作系统通过对该位可以允许或不允许对某页的访问。
有些系统提供硬件如页表长度寄存器(page-table length register,PTLR)来表示页表的大小,该寄存器的值可用于检查每个逻辑地址以验证其是否位于进程的有效范围内,如果检测无法通过,会被操作系统捕获。
8.4.4 共享页
分页的优点之一在于可以共享公共代码。
这种考虑对分时环境特别重要。考虑一个支持40个用户的系统,每个用户都执行一个文本编辑器。如果文本编辑器包括150kb
的代码和50kb的数据空间。则需要8000kb来支持这40个用户。如果代码是可重入代码(reentrant code,也称为纯代码),则可以共享。如图所示,看到3个页的编辑器(每页50kb)在三个进程间共享,而每个进程都有自己的数据页。通过这种方法,只需要在物理内存中保存一个编辑器副本。每个用户的页表映射到编辑器的同一物理副本,而数据页映射到不同帧。因此,为支持40位用户,只需要一个编辑器副本(150k)再加上40个用户数据空间副本50kb,总的需求空间为2150kb,而不是8000kb,这是一个明显的节省。
可重入代码是不能自我修改的代码,它从不会在执行期间改变。两个或多个进程可以在相同的时间执行相同的代码。每个进程都有它自己的寄存器副本和数据存储,以控制进程执行的数据。两个不同进程的数据也将不同。
其他常用程序也可以共享,如编译器,窗口系统,运行时库,数据库系统等。
共享代码的只读特点不能只通过正确代码来保证,而需要操作系统来强制实现。
一个系统多个进程内存共享类似于一个任务的多线程地址空间共享。有的操作系统通过实现共享页来实现共享内存。
除了允许多个进程共享同样的物理页外,按页组织内存也提供了许多其他优点。
8.5 页表结构
8.5.1 层次页表
8.5.2 哈希页表(hashed page table)
8.5.3 倒置页表(inversed page table)
8.6 分段(segmentation)
采用分页内存管理有一个不可避免的问题,就是用户视角的内存和实际物理内存的分离。
8.6.1 基本方法
用户通常愿意将内存看作是一组不同长度的段的集合,这些段之间并没有一定的顺序。如对象、数组、堆栈、变量等,就像汇编语言中对先对段进行定义,然后指针指向段的位置一样。
分段(segmentation) 就是支持这种用户视角内存管理方法。逻辑地址空间由一组段组成的。每个段都有名称和长度。地址指定了段名称和段内偏移。因此用户通过两个量来指定地址:段名称(segment-number)和偏移(offset)。
注意这一方案与分页的对比。在分页中,用户只指定一个地址,该地址通过硬件分为页码和偏移。
为实现简单起见,段是编号的,是通过段号而不是段名来引用的。因此,逻辑地址由有序对组成:
<segment−number,offset>
通常,在编译用户程序时,编译器会自动根据输入程序来构造段。
一个C编译器可能会创建如下段:
- 代码
- 全局变量
- 堆(内存从堆上分配)
- 每个线程采用的栈
- 标准的C库函数
在编译时链接的库可能分配为不同的段。加载程序时会装入所有这些段,并为他们分配段号。
8.6.2 硬件
用户虽然现在能够通过二维地址来引用程序中的对象,但是实际物理地址内存仍然是一维序列字节。因此,必须定义一个实现方式,以便将二维的用户定义地址映射为一维物理地址。这个地址是通过段表(segment table)来实现的。段表的每个条目都有段基地址和段界限。段基地址包含该段在内存中的开始物理地址,而段界限指定该段的长度。
一个逻辑地址由两部分组成:段号s和段内的偏移d。段号用来做段表的索引,逻辑地址的偏移d用位于0和段界限之间。