操作系统概念(第八章) 内存管理(二)

分页产生内部碎片,分段产生外部碎片。

分页(paging)

分页(paging)内存管理方案允许进程的物理地址空间可以使非连续的。分页避免了将不同大小的内存块匹配到交换空间上,前面叙述的内存管理方案都有这个问题,当位于内存中的代码和数据需要换出时,必须现在备份存储上找到空间,这是问题就产生了。备份存储也有前面所述的与内存相关的碎片问题,只不过访问更慢。

传统上,分页支持一直是由硬件来处理的。最近的设计是通过将硬件和操作系统相配合来实现分页。

基本方法

实现分页的基本方法设计将物理内存分为固定大小的块,称为帧(frame);而将逻辑内存也分为同样大小的块,称为页(page)。当需要执行进程时,其页从备份存储中调入到可用的内存帧中。备份存储也分为固定大小的块,其大小与帧相同。

由CPU生成个每个地址分为两个部分:页号(p)和页位移(d)。页号作为页表的索引。页表包含每页所在物理内存的基地址,这些基地址与页偏移的组合形成物理地址,就可送交物理单元。

操作系统概念(第八章) 内存管理(二)_第1张图片
页大小(与帧大小一样)是由硬件来决定的。通常为2的幂。选择页的大小为2的幂可以方便的将逻辑地址转换为页号和页偏移。如果逻辑地址空间为 2m ,且页大小为 2n 单元,那么逻辑地址的高 mn 位表示页号(页表的索引),而低 n 位表示页偏移。每页大小从512B到16MB不等。


设页大小为 a ,根据页号 p 得到基地址 f ,页偏移为 d ,则物理地址为 fa+d

分页是一种动态重定位。每个逻辑地址有分页硬件绑定为一定的物理地址。采用分页类似于使用一组基(重定位)地址寄存器,每个基地址对应这一个内存帧。

采用分页技术不会产生外部碎片:每个帧都可以分配给需要它的进程。不过分页有内部碎片。

每个页表的条目通常为 4 B,不过这是可变的,一个 32 位的条目可以指向 232 个物理帧的任何一个,如果帧为 4 KB,那么具有 4 B条目的系统可以访问 244 B大小。

操作系统概念(第八章) 内存管理(二)_第2张图片

当系统进程需要执行时,它将检查该进程的大小(按页计算)。进程的每页都需要一帧。因此,如果进程需要n页,那么内存中至少应有n个帧。如果有那么就分配给新进程。进程的第一页装入一个已分配的帧,帧号放入进程的页表中。下一页分配给另一帧,其帧号也放入进程的页表中。

分页的一个重要特点是用户视角的内存和实际的物理内存的分离。用户程序将内存作为一整块来处理,而且它只包括这一个进程。事实上,一个用户程序与其他程序一起,分布在物理内存上。

用户视角的内存和实际的物理内存的差异是通过地址转换硬件协调的。逻辑地址转换为物理地址,这种映射是用户所不知道的,但是受操作系统所控制。注意用户进程根据定义是不能访问非它所占用的内存的。它无法访问其页表所规定之外的内存,页表只包括进程所拥有的那些页。

由于操作系统管理物理内存,它必须知道物理内存的分配细节:哪些帧已占用,哪些帧可用,总共有多少帧等。这些信息通常保存在帧表中。在帧表(frame table)中,每个条目对应一个帧,以表示该帧是空闲还是已占用,如果被占用,是被哪个进程的哪个页所占用。

另外,操作系统必须意识到用户进程是在用户空间内执行,且所有逻辑地址必须映射到物理地址。如果用户执行一个系统调用(如进行I/O),并提供地址作为参数,那么这个地址必须映射成物理地址。操作系统为每个进程维护一个页表副本,就如同它需要维护指令计数器和寄存器的内容一样。当操作系统必须手工将逻辑地址映射成物理地址时,这个副本可用来将逻辑地址转换为物理地址。当一个进程可分配到CPU时,CPU调度程序可以根据该副本来定义硬件页表。因此,分页增加了切换时间。

硬件支持

每个操作系统都有自己的方法来保存页表。绝大多数都为每个进程分配一个页表。页表的指针与其他寄存器的值(如指令计数器)一起存入进程控制块。当调度程序需要启动一个程序时,它必须首先装入用户寄存器,并根据所保存的用户页表来定义正确的硬件页表值。

页表的硬件实现有很多方法。最为简单的是将页表作为一组专用寄存器(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)或删除,以确保下一个进程不会使用错误的地址转换。

操作系统概念(第八章) 内存管理(二)_第3张图片

页号在TLB中被查找到的百分比称为命中率

80% 的命中率意味着有 80% 的时间可以在TLB中找到所需的页号。

假如查找TLB需要 20ns ,访问内存需要 100ns ,如果访问位于TLB中的页号,那么采用内存映射访问需要 120ns 。如果不能在TLB中找到( 20ns ),那么必须先访问位于内存中的页表得到帧号( 100ns ),并进而访问内存中所需字节( 100ns ),这总共需要 220ns 。为了得到有效内存访问时间,必须根据概率对每种情况进行加权。

有效内存访问时间 =0.80120+0.2220=140ns

对于这种情况,现在内存访问速度要慢 40

如果命中率为 98% ,那么

有效内存访问时间 = 0.98120+0.02220=122(ns)

由于提高了命中率(Hit ratio),内存访问时间只慢了 22%

保护

在分页环境下,内存保护是通过与每个帧相关联的保护为来实现的。通常,这些位保存在页表中。

可以用一个位来定义一个页是可读写还是只读的。每次地址引用都要通过页表来查找正确的帧码,在计算物理地址的同时,可以检查保护位来验证。对只读页进行写操作会向操作系统产生硬件陷阱(trap)(或内存保护冲突)。

可以很容易的扩展这一方法以提供更细致的保护,可以创建硬件以提供只读、读写、只执行保护。或者,通过为每种访问情况提供独立保护位,实现这些访问的各种组合;非法访问会被操作系统捕捉到。

还有一个位通常与页表中的每一条目相关联:有效-无效位。有效,表示相关的页在进程的逻辑地址空间内,因此是合法的页;无效,表示相关的页不在进程的逻辑地址空间内。通过使用有效-无效位可以捕捉非法地址。操作系统通过对该位可以允许或不允许对某页的访问。

有些系统提供硬件如页表长度寄存器(page-table length register,PTLR)来表示页表的大小,该寄存器的值可用于检查每个逻辑地址以验证其是否位于进程的有效范围内,如果检测无法通过,会被操作系统捕获。

共享页

分页的优点之一在于可以共享公共代码。

这种考虑对分时环境特别重要。考虑一个支持40个用户的系统,每个用户都执行一个文本编辑器。如果文本编辑器包括 150kb 的代码和 50kb 的数据空间。则需要 8000kb 来支持这40个用户。如果代码是可重入代码(reentrant code,也称为纯代码),则可以共享。如图所示,看到3个页的编辑器(每页 50kb )在三个进程间共享,而每个进程都有自己的数据页。通过这种方法,只需要在物理内存中保存一个编辑器副本。每个用户的页表映射到编辑器的同一物理副本,而数据页映射到不同帧。因此,为支持40位用户,只需要一个编辑器副本( 150k )再加上40个用户数据空间副本 50kb ,总的需求空间为 2150kb ,而不是 8000kb ,这是一个明显的节省。

操作系统概念(第八章) 内存管理(二)_第4张图片

可重入代码是不能自我修改的代码,它从不会在执行期间改变。两个或多个进程可以在相同的时间执行相同的代码。每个进程都有它自己的寄存器副本和数据存储,以控制进程执行的数据。两个不同进程的数据也将不同。

其他常用程序也可以共享,如编译器,窗口系统,运行时库,数据库系统等。

共享代码的只读特点不能只通过正确代码来保证,而需要操作系统来强制实现。

一个系统多个进程内存共享类似于一个任务的多线程地址空间共享。有的操作系统通过实现共享页来实现共享内存。

除了允许多个进程共享同样的物理页外,按页组织内存也提供了许多其他优点。

页表结构

共享页

绝大多数现代操作系统支持大逻辑地址空间( 232 ~ 264 )。这样,页表本身就非常大。

设想具有32位逻辑地址空间的计算机系统,如果系统的页大小为4kb( 212B )那么,一个页表可以包含一百万个条目( 232/212 ),假设每个条目有4B,那么每个进程需要4MB的物理地址空间来存储页表本身。显然,我们并不可能在内存中连续地分配这个页表。这个问题的一个简单解决方法是将页表划分为更小部分。划分方法有很多。

一种方法是使用两级分页算法。就是将页表再分页。仍以之前的32位系统为例,一个逻辑地址被分为20位的页码和12位的页偏移d。因为要对页表进行再分页,该页号可分为10位的页码p1和10位的页偏移p2。其中p1用来访问外部页表的索引,而p2是是外部页表的页偏移。由于地址转换由外向内,这种方法也称为向前映射页表(forward-mapped page table)

操作系统概念(第八章) 内存管理(二)_第5张图片
页码(page number)
页偏移(page offset)
操作系统概念(第八章) 内存管理(二)_第6张图片
Two-Level Page-Table Scheme

地址转移过程
外页表(outer-page table)
页表的页(page of page table)

VAX体系结构也支持两层分页的变种。逻辑地址分为4个区。逻辑地址头两位表示适当分区,中间21位表示区内的页号,后9位表示所需页中的偏移。操作系统可以只在进程需要时才使用某些分区。并且VAX体系结构对用户进程的页表进行换页,以减少对主存的使用。

对于64位的逻辑地址空间的系统分层结构不太适用,对于32位的还可以采用三层分页方案,甚至四层分页方案。

哈希页表(hashed page table)

处理超过32位地址空间的常用方法是使用哈希页表(hashed page table),并以虚拟页码作为哈希值。哈希页表的每一条目都包括一个链表的元素,这些元素哈希成同一位置(要处理器碰撞)。每个元素有3个域:

  • (1)虚拟页码
  • (2)所映射的帧号
  • (3)指向链表中下一个元素的指针。

该算法按照如下方式工作:虚拟地址中的虚拟页号转换为哈希表号,用虚拟页号与链表中的每一个元素的第一个域相比较。如果匹配,那么相应的帧号(第二个域)就用来形成物理地址,如果不匹配,那么就对链表中的下一个节点进行比较,以寻找一个匹配的页号。


人们提出了这种方法的一个变种,比较适合64位的地址空间, 群集页表(clustered page table),类似于哈希页表,不过这种哈希表的每一条目不只包括一页信息,而且包括多页。一个页表条目可以存储多个物理页帧的映射。群集页表对于稀疏地址空间特别有用,稀疏地址空间中的地址引用不连续,且分散在整个地址空间。

反向页表(inversed page table)

通常,每个进程都有一个相关页表。该进程所使用的每个页都在页表中有一项(或者每个虚拟地址都有一项,不管是否有效)。这种页的表达方式比较自然,这是因为进程是通过页的虚拟地址来引用页的。操作系统必须将这种引用转换成物理内存地址。由于页表是按照虚拟地址排序的,操作系统能够计算出所对应条目在页表中的位置,并可以直接使用该值。这种方法的缺点之一是每个页表可能有很多项,这些表可能消耗大量的物理内存,却仅用来跟踪物理内存是如何使用的。

为了解决这个问题,引入反向页表(inversed page table)。反向页表对于每个真正的内存帧或页才有一个条目。每个条目包含保存在真正内存位置的页的虚拟地址以及拥有该页的进程的信息。因此,整个系统只有一个表,对每个物理内存的页只有一条相应的条目。

由于系统只有一个页表,而有很多地址空间映射物理内存,所以反向页表的条目中通常需要一个地址空间标识符,以确保一个特定进程的一个逻辑页可以映射到相应的物理帧。

如图,process-id作为地址空间的标识符。当需要内存引用时,由process-id和page-number组成的虚拟地址部分送交内存子系统,通过查找反向页表来寻找匹配。如果匹配找到,例如条目i,那么就产生了物理地址i+offset。如果没有匹配,那就是试图访问非法地址。

虽然这种方案减少了存储每个页表所需要的内存时间,但是当引用页时,它增加了查找页表所需要的时间。(由于反向页表按照物理地址排序,而查找的是虚拟地址,因此可能需要查找整个表来寻求匹配。这种查找会花费大量时间)

故可以通过使用哈希页表来将查找限制在一个或少数几个页表条目。但这样,每次访问哈希页表为整个过程增加了一次内存引用,因此一次虚拟地址引用至少需要两次内存读:一个查找哈希页表条目,另一个查找页表。为改善性能,可以在访问哈希页表时,先查找TLB。

采用反向页表的系统在实现共享内存时存在困难。共享内存通常作为被映射到一个物理地址的多虚拟地址(其中每一个进程共享内存)来实现。这种标准方法不能用到反向页表,因为此时每个物理页只有一个虚拟页条目,一个物理页不可能有两个(或更多)的共享虚拟地址。

解决这一问题一个简单方法是允许页表仅包含一个虚拟地址到共享物理地址的映射,这意味着对未被映射的虚拟地址的引用将导致页错误。

分段(segmentation)

采用分页内存管理有一个不可避免的问题,就是用户视角的内存和实际物理内存的分离。

基本方法

用户通常愿意将内存看作是一组不同长度的段的集合,这些段之间并没有一定的顺序。如对象、数组、堆栈、变量等,就像汇编语言中对先对段进行定义,然后指针指向段的位置一样。

分段(segmentation)就是支持这种用户视角内存管理方法。逻辑地址空间由一组段组成的。每个段都有名称和长度。地址指定了段名称和段内偏移。因此用户通过两个量来指定地址:段名称(segment-number)偏移(offset)

注意这一方案与分页的对比。在分页中,用户只指定一个地址,该地址通过硬件分为页码和偏移。

为实现简单起见,段是编号的,是通过段号而不是段名来引用的。因此,逻辑地址由有序对组成:

<segmentnumber,offset>

通常,在编译用户程序时,编译器会自动根据输入程序来构造段。

一个C编译器可能会创建如下段:

  • ①代码

  • ②全局变量

  • ③堆(内存从堆上分配)

  • ④每个线程采用的栈

  • ⑤标准的C库函数

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

硬件

用户虽然现在能够通过二维地址来引用程序中的对象,但是实际物理地址内存仍然是一维序列字节。因此,必须定义一个实现方式,以便将二维的用户定义地址映射为一维物理地址。这个地址是通过段表(segment table)来实现的。段表的每个条目都有段基地址和段界限。段基地址包含该段在内存中的开始物理地址,而段界限指定该段的长度。

一个逻辑地址由两部分组成:段号s和段内的偏移d。段号用来做段表的索引,逻辑地址的偏移d用位于0和段界限之间。

你可能感兴趣的:(操作系统,内存管理)