操作系统概念 第八章 内存管理策略

第八章 内存管理策略

8.1 背景

8.1.1 基本硬件

CPU可以直接访问的通用存储只有内存和处理器内置的寄存器。机器指令可以用内存地址作为参数,而不能用磁盘地址作为参数。因此执行指令以及指令使用的数据,应处于这些可以直接访问的存储设备上。如果数据不在内存中,那么CPU使用它们之前应先把数据移到内存。

对于寄存器内容,大多数CPU可以在一个时钟周期内解释并执行一条或多条指令,对于内存(通过内存总线的事务访问)可能需要多个时钟周期。在这种情况下,由于没有数据以便完成正在执行的指令,CPU通常需要暂停(stall)。补救措施是在CPU与内存之间,通常是在CPU芯片上,增加更快的内存(高速缓存(cache))。

我们不仅关心访问物理内存的相对速度,还要确保操作的正确,保证系统操作的正确,应保护操作系统不被用户进程访问,还用保护用户进程不会相互影响。

首先我们需要确保每个进程都有一个单独的内存空间,这可以保护进程不相互影响。通过基地址和界限地址确定一个进程可以访问的合法地址范围,并确保该进程只能访问这个合法地址。基地址寄存器(base register)含有最小合法的物理内存地址,界限地址寄存器(limit register)指定了范围的大小。内存空间保护的实现是通过CPU硬件对在用户模式下产生的地址与寄存器的地址进行比较来完成的。若欲访问的地址不是合法地址,将会陷入操作系统,防止用户程序无意或故意修改操作系统或其他用户的代码。只有操作系统可以通过特殊的特权指令加载/修改基地址寄存器和界限地址寄存器。

在内核模式下执行的操作系统可无限制地访问操作系统及用户内存。这允许操作系统:加载用户程序到用户内存,转储出现错误的程序,访问和修改系统调用的参数,以及提供许多其他服务等。

8.1.2 地址绑定

在磁盘上等待调到内存以便执行的进程形成了输入队列(input queue)。
源程序中的地址通常是用符号表示,编译器通常将这些符号地址绑定(bind)到可重定位的地址。链接程序或加载程序再将这些可重定位的地址绑定到绝对地址。每次绑定都是从一个地址空间到另一个地址空间的映射。

  • 编译时(compile time):如果编译时就知道进程在内存中的驻留地址,就可以生成绝对代码(absolute code)。如果事先知道用户进程驻留在内存地址R处,那么生成的编译代码就可以从该位置开始并向后延伸。
  • 加载时(load time):如果编译时不知道进程将驻留在何处,那么编译器就生成可重定位代码(relocatable code)。最后绑定会延迟到加载时才进行。如果开始地址发生变化,只需重新加载用户代码以合并更改的值。
  • 执行时(runtime time):如果进程在执行时可以从一个内存段迁移到另一个内存段,那么绑定应延迟到执行时才进行。采用这种方案需要特定硬件。大多数通用计算机操作系统采用这种方法。

8.1.3 逻辑地址空间与物理地址空间

CPU生成的地址通常称为逻辑地址(logical address),而内存单元看到的地址(即加载到内存地址寄存器(memory-address register)的地址)通常称为物理地址(physical address)。编译时和加载时的地址绑定方法生成相同的逻辑地址和物理地址,执行时的地址绑定方案生成不同的逻辑地址和物理地址。这种情况下我们通常称逻辑地址为虚拟地址(virtual address)。由程序生成的所有逻辑地址的集合称为逻辑地址空间(logical address space),这些逻辑地址对应的所有物理地址集合称为物理地址空间(physical address space)。从虚拟地址到逻辑地址的运行时映射是由内存管理单元(Memory-Management Unit,MMU)的硬件设备来完成的。基地址寄存器在这里称为重定位寄存器(relocation register)。用户程序使用的地址相对于基地址寄存器进行重定位后得到物理地址。

8.1.4 动态加载

为了获得更好的内存空间利用率,可以使用动态加载(dynamic loading)。采用动态加载时,一个程序只有在调用时才会加载。所有程序都以可重定位加载格式保存在磁盘上。当一个程序需要调用一个没有加载的程序,可重定位来链接程序会加载所需的程序到内存,并更新程序的地址表以反映这一变化。

8.1.5 动态链接与共享库

???

8.2 交换

进程可以暂时从内存交换(swap)到备份存储(backing store),当再次执行时再调回内存。交换有可能让所有的进程的总的物理地址空间超过真实系统的物理地址空间,从而增加了系统的多道程序程度。

8.2.1 标准交换

标准交换是在内存与备份存储(通常是快速磁盘)之间移动进程。现代操作系统并不适用标准交换,它的交换时间太多,它提供的执行时间太少,不是合理的内存管理解决方案。

8.2.2 移动系统的交换

移动系统通常不支持任何形式的交换,移动设备通常采用闪存,而不是空间更大的硬盘作为它的永久存储。空间约束是移动操作系统设计者避免交换的原因之一;其他原因包括:闪存写入次数的限制以及内存与闪存之间的吞吐量差。

8.3 连续内存分配

在采用连续内存分配(contiguous memory allocation)时,每个进程位于一个连续的内存区域,与包含下一个进程的内存相连。

8.3.1 内存保护

通过上面描述的利用重定位寄存器和界限寄存器可以很好实现这一目的。当CPU调度器选择一个进程来执行时,作为上下文切换工作的一部分,分派器会用正确的值来加载重定位寄存器和界限寄存器。CPU产生的每个地址都需要与这些寄存器进行核对,可以保证操作系统和其他用户的程序和数据不受该运行程序的影响。重定位寄存器方案提供了一种有效的方式,以便允许操作系统动态改变大小。

8.3.2 内存分配

最简单的内存分配方法之一就是将内存分为多个固定大小的分区(partition)。当一个分区空闲时,可以从输入队列中选择一个进程,调入空闲的分区,当进程终止时,它的分区可用于其他进程。

对于可变分区(variable-partition)方案,操作系统有一个表记录可用内存和已经使用的内存。开始,所有内存都可用于用户进程,因此可以作为一大块的可用内存,称为(hole)。随着进程进入系统,它们将被加入输入队列,操作系统根据所有进程的内存需求和现有可以内存的情况,决定哪些进程可分配内存。当进程分配到空间,它就加载到内存,并开始竞争CPU。当进程终止时,它将释放内存,该内存可以被操作系统分配给输入队列内的其他进程。
任何时候都有一个可用块大小的列表和一个输入队列。操作系统根据调度算法对输入队列进行排序,内存不断分配给进程,直到下一个进程的内存需求不能满足为止。
可用的内存为分散在内存里的不同大小的孔的集合。孔太小时,可以等到有足够大的空间或者可以往下扫描输入队列,寻找内存需求较小的进程;孔太大就分为两块,一块分给进程,另一块还回孔集合。如果有相邻的孔,可以合并成一个大孔。

这种方法是通用动态存储分配问题(dynamic storage-allocation problem)(根据一组空闲孔来分配大小为 n 的请求)的一个特例。从一组可用孔中选择一个空闲孔的最为常用方法包括:首次适应(first-fit)、最优适应(best-fit)及最差适应(worst-fit)。

  • 首次适应:分配首个足够大的孔。可以从头开始查找,也可以从上次首次适应结束时开始,一旦找到便停止。
  • 最优适应:分配最小的足够大的孔。应查找整个列表,除非列表按大小排序。这种方法可以产生最小剩余孔。
  • 最差适应:分配最大的孔。可以产生最大剩余孔,该孔可能比最优适应产生的较小剩余孔更为适用。

模拟结果显示首次适应和最优适应在执行时间和利用空间方面都好于最差适应。首次适应和最优适应在利用空间方面难分伯仲,但是首次适应更快些。

8.3.3 碎片

上述首次适应和最优适应算法都有外部碎片(external fragmentation)的问题。 随着进程加载到内存和从内存退出,空闲内存空间被分为小的片段。当总的可用内存之后可以满足请求但并不连续时,就出现了外部碎片问题:存储被分成了大量的小孔。

通常维护一个很小的孔的开销要比孔本身大得多,所以通常按固定大小的块为单位分配内存。分配的内存可能比进程所需的大,大出的部分称为内部碎片(internal fragmentation),这部分内存在分区内部,但不能使用。

外部碎片的一种解决方法是紧缩(compaction):通过移动内存内容将所有空闲空间合并成一整块。紧缩不是总是可能的:如果重定位是静态的,并且在汇编时或加载时进行的,那么就不能紧缩。只有重定位是动态的,并且在运行时进行的,才可用紧缩。

外部碎片化问题的另一个可能的解决方案是:允许进程的逻辑地址空间是不连续的。有两种互补的技术可以实现这个解决方案:分段(8.4节)和分页(8.5节)。

8.4 分段

8.4.1 基本方法

当编写程序时,程序员并不关心他所写的程序在物理空间中的具体的结构、分布等。程序员认为程序就是主程序加上一组方法、过程或函数所构成的,它还可以包括各种数据结构。

分段(segmentation)就是支持这种用户试图的内存管理方案。逻辑地址空间是由一组段构成,每个段都有名称和长度。地址指定了段名称和段内偏移。因此用户通过两个量来指定地址:段名称和段偏移。为了实现简单,段是编号的,通过段号而不是段名称来引用。因此逻辑地址是由有序对(two tuple)组成: <段号,偏移>

通常,在编译用户程序时,编译器会根据输入程序来自动构造段。一个C编译器可能会创建如下段:

  • 代码
  • 全局变量
  • 堆(内存从堆上分配)
  • 每个线程使用的栈
  • 标准的C库

在编译时链接的库可能分配不同的段。加载程序会装入所有这些段,并为它们分配段号。

8.4.2 分段硬件

虽然用户现在能够通过二维地址(段号,偏移)来引用程序内的对象,但实际物理内存仍是一维的字节序列。我们使用段表(segment table)来实现从用户定义的二维地址到一维物理地址的映射。段表的每个条目都有段基地址(segment base)和段界限(segment limit)。段基地址包含该段在内存中的开始物理地址,段界限指定该段的长度。

每个逻辑地址由两部分组成:段号 s 和段偏移 d 。段号用作段表的索引(根据段号找到段表中正确的条目),段偏移应位于0和段界限之间,如果不是这样将会陷入操作系统。如果偏移 d 合法,那么与基地址相加就可以得到物理地址。

8.5 分页

分段允许进程的物理地址空间是不连续的,分页(paging)是提供这种又是的另一种内存管理方案。分页避免了外部碎片和紧缩,而分段不可以。分页也避免了将不同大小的内存块匹配到交换空间的麻烦问题(当位于内存的代码和数据段需要换出时,应在备份存储上找到空间,备份存储也有同样的与内存相关的碎片问题,但是访问更慢,因此紧缩是不可能的)。

8.5.1 基本方法

实现分页的基本方法涉及将物理内存分为固定大小的块,称为页帧(frame);讲逻辑内存也分为同样大小的块,称为页面(page)。当需要执行一个进程时,它的页从文件系统或备份存储等源处,加载到内存的可用帧。备份存储划分为固定大小的块,它与单个内存帧或多个内存帧的大小一样。

由CPU生成的每个地址分为两部分:页码(page number)(p)和页偏移(page offset)(d)。页码作为页表的索引,根据页码在页表中查找对应条目。页表包含每页所在物理内存的基地址,基地址和页偏移组合形成了物理内存地址。页的大小为2的幂。如果逻辑地址空间为 2m ,且页大小为 2n 字节,那么逻辑地址高 m-n 为表示页码,而低 n 位表示页偏移。

由于操作系统管理物理内存,它应知道物理内存的分配细节:哪些帧已分配,哪些帧空闲,总共有多少帧等等。这些信息通常保存在称为帧表(frame table)的数据结构中。

8.5.2 硬件支持

每个操作系统都有自己保存页表的方法。有的为每个进程分配一个页表。页表的指针,与其他寄存器的值(如指令寄存器),一起存入进程控制块。当分派器需要启动一个进程时,它应首先加载用户寄存器,根据保存的用户页表来定义正确的硬件页表值。

页表的硬件实现最为简单的一种方法时,将页表作为一组专用的寄存器来实现。这些寄存器应用高速逻辑电路来构造,以高效的进行分页地址的转换,每次访问内存都要经过分页映射,因此效率十一i个重要的考虑因素。

如果页表比较小,页表使用寄存器还是令人满意的。但是当页表非常大时,这个方案就不可行了,此时需要将页表放在内存中,并将页表基地址寄存器(Page-Table Base Register,PTBR)指向页表。采用这种方法访问位置 i ,首先用 PTBR 加上 i 的页码作为偏移来查找页表。根据所得帧码,再加上页偏移就得到了真实物理地址。采用这种方案,访问一个字节需要两次内存访问(一次用于页表条目,一次用于字节)。大多数情况下,这样内存访问速度减半带来的延迟是无法忍受的。

这个问题的标准解决方案是采用专用的、小的、查找快速的高速硬件缓冲:转换表缓冲区(Translation Look-aside Buffer,TLB)。TLB是关联的高速内存。TLB条目由两部分组成:键(标签)和值。当关联内存根据给定值查找时,它会同时与所有的键进行比较,如果找到,就得到相应值的字段。

TLB与页表一起使用的方法如下:TLB只包含少数的页表条目,当CPU产生一个逻辑地址后,它的页码就发送的TLB,如果找到这个页码,它的帧码也就立即可用,可用于访问内存。这些步骤可作为CPU的流水线的一部分来执行;与没有实现分页的系统相比,这没有降低性能。如果页码不在TLB中(称为TLB未命中(TLB miss)),那么就需要访问页表,当得到帧码时,再用它来访问内存,另外将页码和帧码添加到TLB。如果TLB已满,那么会选择一个来替换,替换策略有很多,如最近最少使用替换(LRU),轮转替换,随即替换等。

8.5.3 保护

分页环境下的内存保护是通过与每个帧关联的保护位来实现的,通常这些位保存在页表中。如用一个位定义一个页是可读可写或只可读。还有一个位与页表中的每一条目相关联:有效-无效位(valid-invalid bit)。在通过页表进行访问内存操作时,需检查这些保护位,根据保护位的内容提供对应的访问权限。非法访问将会陷入操作系统。

8.5.4 共享页

分页的优点之一是可以共享公共代码。如果代码是可重入代码(reentrant code)或纯代码(pure code),则可以共享。可重入代码是不能自我修改的代码,它在执行期间不会改变。因此两个及以上进程可同时执行相同的代码。假设一个支持 40 个用户的系统,每个都要执行一个文本编辑器(包含150KB的代码和40KB的数据空间),则需要8000KB来支持这40个用户。但这里,文本编辑器代码部分可共享,每个进程的所需的代码只需通过页表映射到编辑器的同一物理副本,然后每个进程都有自己的数据页即可,这样总的需求空间位2150KB。

8.6 页表结构

组织页表的一些最常用技术:分层分页、哈希页表和倒置页表。

8.6.1 分层分页

考虑一种两层分页算法,就是将页表再分页。假设一个系统具有32位逻辑地址空间和 4K 大小的页。一个逻辑地址被分为20位的页码和12位的页偏移(d)。两层分页可这样实现:将该页码再分为10位的页码(p1)和10位的页偏移(p2)。其中 p1 是用来访问外部页表的索引,p2 是内部页表的页偏移。这种方法地址转换由外向内,这种方案也称为向前映射页表(forward-mapped page table)。使用这种方法如何通过逻辑地址映射到物理地址:首先通过 p1 在外部页表找到对应条目(这个条目包含下一级的页码,下一级仍为页表),通过上一级得到的页码和偏移量,在页表的页(这个页的内容仍为页表)中找到对应条目(对应物理地址的页码和偏移量),最后利用这个页码和偏移量找到物理地址。

64位的 UltraSPARC 将需要7个级别的分页,如此多的内存访问是不可取的。从这个例子可以看出对于64位的架构,为什么分层页表通常被认为是不适当的。

8.6.2 哈希页表

处理大于32位地址空间的常用方法是使用哈希页表(hashed page table),采用虚拟页码作为哈希值。哈希页表的每个条目都包含一个链表,该链表的元素哈希到同一位置(该链表用来解决处理碰撞)。每个元素由三个字段组成:1)虚拟页码;2)映射的帧码;3)指向链表下一个元素的指针。

该算法工作原理如下:虚拟地址的虚拟页码哈希到哈希表。用虚拟页码从前往后与链表内元素的第一个字段进行比较,如果匹配那么相应帧码就用来形成物理地址。

8.6.3 倒置页表

倒置页表(inverted page table):对于每个真正的内存页或帧,倒置页表才有一个条目。每个条目包含保存在真正内存位置上的页的虚拟地址,以及拥有该页进程的信息。

采用倒置页表的系统内的每个虚拟地址为一个三元组:<进程id,页码,偏移>。这里id用来作为地址空间的标识符。当发生内存引用时,由<进程id,页码>组成的虚拟地址被提交到内存子系统。然后搜索倒置页表来寻找匹配,如果找到匹配条目,如条目 i ,则生成物理地址 <i,偏移>。

这种方案减少了存储每个页表所需的空间(整个系统只有一个页表),但它增加了由于引用页查找页表所需的时间。采用倒置页表的系统在实现共享内存时会有困难。因为每个物理页只有一个虚拟页条目,一个物理页不可能有两个(或多个)共享的虚拟地址。解决这个问题的一个简单技术是,只允许页表包含一个虚拟地址到共享物理地址的映射(???)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值