深入理解计算机系统第9章 虚拟存储器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010772289/article/details/86498837

第9章 虚拟存储器

虚拟存储器是现代操作系统提供的一种对主存的抽象概念。

9.1 物理和虚拟寻址

  • 物理地址: 计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址,第一个字节地址为0,接下来为1,再下一个为2.
  • 虚拟地址: CPU通过生成一个虚拟地址来访问内存,虚拟地址在被传送到存储器之前转换成物理地址。

将虚拟地址转换成物理地址叫地址翻译

9.2 地址空间

  • 虚拟地址空间: {0, 1, 2, …, N-1},其中 N=2^n, 一个包含N=2^n个地址的虚拟地址空间就叫做一个n位地址空间
  • 物理地址空间: {0, 1, 2, …, M-1},它与系统中物理存取器的M个字节相对应, M不要求是2的幂

9.3 虚拟存储器作为缓存的工具

虚拟存储(VM): 被组织成存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有唯一的虚拟地址。磁盘上数组的内容被缓存在主存中
虚拟页与物理页: VM系统将虚拟存储器分割成大小固定的,称为虚拟页。类似地,物理存储器也被分割成物理页。
虚拟页面的集合可分为:

  • 未分配的: VM系统还未分配或的页,没有任何数据与之关联,因此不占用任何磁盘空间。
  • 缓存的: 当前缓存在物理存储器中的已分配页。
  • 未缓存的: 没有缓存在物理存储器中的已分配页。
    一个VM系统是如何使用主存作为缓存的

1.DRAM缓存的组织结构

两个术语:

  • SRAM缓存: 表示位于CPU和主存之间的L1,L2,L3高速缓存.
  • DRAM缓存: 表示虚拟存储器系统的缓存,它在主存中缓存虚拟页.

DRAM缓存不命中比SRAM缓存不命中代价要大得多,因为DRAM缓存不命中要由磁盘来服务,而SRAM缓存不命中通常是由基于DRAM的主存来服务的。
由于大的不命中处罚,虚拟页往往很大,典型的是2KB~2MB。

2.页表

虚拟存储器系统必须有某种方法来判断一个虚拟页是否存放在DRAM中。如果是,还得确定这个虚拟页存放在哪个物理页中。若不命中,系统需判断这个虚拟页在磁盘的哪个位置,在物理存储器中选择一个牺牲页,并将虚拟页从磁盘拷贝到DRAM中,替换这个牺牲页。

页表: 存放在物理存储器中,将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时都会读取页表.

图9-4展示了一个页表的基本组织结构。页表就是一个页表条目(Page Table Entry, PTE)的数组。假设每个PTE是由一个有效位和一个n位地址字段组成。有效位表明该虚拟页当前是否被缓存在DRAM中。

  • 如果设置了有效位,那么地址字段就表示DRAM中相应物理页的起始位置,这个物理页中缓存了该虚拟页。
  • 如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。
    页表

3. 页命中

如图9-6中VP2,已经缓存在存储器中。

4.缺页

在虚拟存储器中,DRAM缓存不命中称为缺页。CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从存储器中读取PTE3,从有效位推断VP3并未缓存,并且触发一个缺页异常

缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如VP4。若VP4已被修改,那么内核就会将它拷贝回磁盘。

接着,内核从磁盘拷贝VP3到存储器中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但现在VP3已经缓存在主存中,那么页命中也能由地址翻译硬件正常处理了。

在虚拟存储器的说法中,块被称为页。在磁盘和存储器之间传送页的活动叫做交换或者页面调度。页从磁盘换入(或者页面调入)DRAM和从DRAM换出(或者页面调出)磁盘。
在这里插入图片描述

局部性

局部性保证了在任意时刻,程序往往在一个较小的活动页面集合上工作,这个集合叫做工作集。

如果工作集的大小超出了物理存储器的大小,那么程序将会产生一种不幸的状态,叫做颠簸,这时页面将不断换进换出。

9.4 虚拟存储器作为存储器管理的工具

虚拟存储器工作机制: 利用DRAM缓存来自通常更大的虚拟地址空间的页面。

目前,我们都假设有一个单独的页表,将一个虚拟地址空间映射到物理地址空间。实际上,操作系统为每个进程都提供了一个独立的页表,因而也就是一个独立的虚拟地址空间。图9-9展示了基本思想。进程i的页表将VP1映射到VP2,VP2映射到PP7。进程j的页表将VP1映射到PP7,VP2映射到PP10。注意,多个虚拟页面可以映射到同一个共享物理页面上.
VM如何为进程提供独立的地址空间
按需页面调度独立的虚拟地址空间的结合,对系统中存储器的使用和管理造成了深远的影响。特别的,VM简化了链接和加载、代码和数据共享,以及应用程序的存储器分配。

  • 简化链接: 独立的地址空间允许每个进程的存储器影像使用相同的基本格式,而不管代码和数据实际存放在物理存储器的何处。如图8-13,一个Linux系统每个进程都使用类似的存储器格式。文本节总是从0x08048000处开始(对于32位地址空间),或从0x400000处开始(对于64位空间)。数据和bss节紧跟在文本届后面。栈占据进程地址空间最高部分,并向下生长。这样的一致性极大的简化了链接器的设计和实现。
  • 简化加载: 第7章ELF可执行文件中.text和.data节是连续的。要把这些节加载到一个新建的进程中,Linux加载器分配虚拟页的一个连续的片,从地址0x08048000处开始(对于32位地址空间)或0x400000处开始(对于64位空间),把这些虚拟页标记为无效的(即未被缓存的),将页表条目指向目标文件中适当的位置。加载器从不实际拷贝任何数据从磁盘到存储器。在每个页初次被引用时,虚拟存储器系统会按照需要自动调入数据页。
  • 简化共享: 一般,每个进程都有自己私有的代码、数据、堆和栈区域,是不和其他进程共享的。这种情况下,操作系统创建页表,将相应的虚拟页映射到不同的物理页面。然而,一些情况下,需要进程共享代码和数据。例如,每个进程必须调用相同的内核代码,而每个C程序都会调用C标准库中的程序,如printf。操作系统通过将不同进程中适当的虚拟页面映射到相同的物理页面,从而安排多个进程共享这部分代码的一个拷贝,而不是在每个进程都包括单独的内核和C标准库的拷贝。
  • 简化存储器分配: 当用户进程中的程序要求额外的堆空间时(如调用malloc时),操作系统分配某些(如k个)连续的虚拟存储器页面,且将它们映射到物理存储器中任意位置的k个任意的物理页面。由于页表的工作方式,操作系统没必要分配k个连续的物理页面,页面可以随机分散在物理存储器中。

9.5 虚拟存储器作为存储器保护的工具

每次CPU生成一个地址时,地址翻译硬件都会先读一个PTE(即页表条目),可以在PTE上添加一些许可位来控制对一个虚拟页面内容的访问。

9.6 地址翻译

地址翻译是一个N元素的虚拟地址空间中的元素和一个M元素的物理地址空间中元素之间的映射。MMU利用页表来实现这种映射,如下图.
地址翻译

1.多级页表

假设系统只用一个单独的页表来进行地址翻译(实际是操作系统为每个进程都提供了一个独立的页表),并假设有一个32位的地址空间、4KB的页面和一个4字节的PTE,那么也需要一个4*(232/4*210)=4MB的页表驻留在存储器中。对于地址空间为64位的系统,问题会更复杂。如何解决?

常使用层次结构的页表来压缩页表。假设32位虚拟地址空间被分为4KB的页,而每个PTE(即页表条目)都是4字节。还假设这一时刻,虚拟地址空间形式如下: 存储器的前2K个页面分配给了代码和数据,接下来6K个页面还未分配,再接下来1023个页面也没分配,接下来的一个页面分配给了用户栈。图9-17展示了如何为这个虚拟地址空间构造一个两级的页表层次结构。

一级页表每个PTE负责映射虚拟地址空间的一个4MB的片(chunk),这里每一片都是由1024个连续的页面组成。假设地址空间是4GB,1024个PTE已经足够覆盖整个空间.
二级页表中每个PTE负责映射一个4KB的虚拟存储器页面。

这种方法从两个方面减少了存储器要求:

  • 若一个一级页表中PTE为空,那相应的二级页表就根本不会存在。这节约了很多,因为对于典型的程序,4GB地址空间大部分都是未分配的。
  • 只有一级页表需要总是在主存中;虚拟存储器系统可以再需要时创建、页面调入或调出二级页表,这减少了主存的压力;只有最经常使用的二级页表才需要缓存在主存中。
    一个两级页表层次结构

9.7 案例研究

1.Linux虚拟存储器系统

Linux为每个进程维护了一个单独的虚拟地址空间,如图9-26。内核虚拟存储器位于用户栈之上。

内核虚拟存储器包含内核中的代码和数据结构。

  • 内核虚拟存储器的某些区域被映射到所有进程共享的物理页面。例如,每个进程共享内核的代码和全局数据结构。
  • 内核虚拟存储器的其他区域包含每个进程都不相同的数据。如页表、内核在进程的上下文中执行代码时使用的栈,以及记录虚拟地址空间当前组织的各种数据结构。
    一个Linux进程的虚拟存储器

1.Linux虚拟存储器区域

Linux将虚拟存储器组织成一些区域(也叫做段)的集合。一个区域(area)就是已经存在着的(已分配的)虚拟存储器的连续片(chunk)。如,代码段、数据段、堆、共享库段,以及用户栈都是不同的区域。区域允许虚拟地址空间有间隙。内核不用记录那些不存在的虚拟页,这样的页也不占用存储器、磁盘或者内核本身的任何额外资源。

图9-27展示了一个进程中虚拟存储器的内核数据结构。内核为系统中每个进程维护一个单独的任务结构(源代码中的task_struct)。任务结构中元素包含或者指向内核运行该进程所需要的所有信息(例如,PID、指向用户栈的指针、可执行目标文件的名字以及程序计数器)

task_struct中一个条目指向mm_struct,它描述了虚拟存储器的当前状态,其中pgd字段指向第一级页表的基址,而mmap指向一个vm_area_structs的链表,其中每个vm_area_structs都描述了当前虚拟地址空间的一个区域(area)。
图9-27Linux是如何组织虚拟存储器的

2.Linux缺页异常处理

MMU试图翻译虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序执行以下步骤:

  1. 虚拟地址A是合法的吗? 缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较,如果这个指令不合法,那么缺页处理程序就触发一个段错误。
  2. 试图进行的存储器访问是否合法?也就是进程是否有读、写或者执行这个区域内页面的权限?若不合法,那么缺页处理程序会触发一个保护异常。
  3. 缺页处理: 选择一个牺牲页面,若该页面被修改过,则将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,现在MMU可以正常翻译A了.
    图9-28Linux缺页处理

9.8 存储器映射

Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程称为存储器映射。

一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件之间换来换去。交换文件也叫作交换空间或者交换区域(swap area)。在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。回忆,在安装ubuntu系统时,我们会分配一个swap区域,想必是这个目的吧。

1.再看共享对象

进程这一抽象能够为每个进程提供自己私有的虚拟地址空间,可以免受其他进程的错误读写。不过,许多进程有同样的只读文本区域。例如,每个运行Unix外壳程序tcsh的进程都有相同的文本区域。而且,许多程序需要访问只读运行时库代码的相同拷贝。如每个C程序都需要来自标准C库的诸如printf这样的函数。若每个进程都在物理存储器中保持这些常用代码的复制拷贝,那就极度浪费空间。幸好存储器映射提供了多个进程共享对象的机制。

一个对象可被映射到虚拟存储器的一个区域,要么作为共享对象,要么作为私有对象

  • 如果一个进程将一个共享对象映射到它的虚拟地址空间的一个区域内,那么这个进程对这个区域的任何写操作,对于那些也把这个共享对象映射到它们虚拟存储器的其他进程而言也是可见的。而且,这些变化也会反映在磁盘上的原始对象中。
  • 对一个映射到私有对象的区域做的改变,对于其他进程来说是不可见的,并且进程对这个区域所做的任何写操作都不会反映在磁盘上的对象中。一个映射到共享对象的虚拟存储器区域叫做共享区域,类似地,也有私有区域.

即使对象被映射到了多个共享区域,物理存储器中也只需要存放共享对象的一个拷贝

写时拷贝: 私有对象是使用写时拷贝的技术被映射到虚拟存储器中的。一个私有对象开始时和共享对象一样,在物理存储器中只保存私有对象的一份拷贝。对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读。只要没有进程试图写它自己的私有区域,它们就可以继续共享物理存储器中对象的一个单独拷贝。直到有一个进程试图写私有区域内的某个页面,这个写操作会触发一个保护故障。故障处理程序会在物理存储器中创建这个页面的一个新拷贝,更新页表条目指向这个新的拷贝,然后恢复这个页面的可写权限。当故障处理程序返回时,CPU重新执行这个写操作,现在在新建的页面上就可以执行写操作了。写时拷贝充分使用了稀有的物理存储器。

如图9-30,a)中两个进程将一个私有对象映射到它们的虚拟存储器的不同区域,但共享这个对象同一个物理拷贝. b)为进程2写私有区域的一个页。
图9-30一个私有的写时拷贝对象

2. 再看fork函数

fork被当前进程调用时,它创建了当前进程的mm_struct、区域结构和页表的原样拷贝。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时拷贝。子进程与父进程的虚拟存储器相同。当这两个进程中有一个进行写操作,写时拷贝机制会创建新页面。也就为每个进程保持了私有地址空间。

3. 再看execve函数

假设进程执行execve(“a.out”, NULL, NULL)时。加载并运行a.out需要以下几步:

  • 删除已存在的用户区域: 删除当前进程的虚拟地址的用户部分的已存在的区域结构
  • 映射私有区域: 为新程序的文本、数据、bss和栈区域创建新的区域结构。所有新区域都是私有的,写时拷贝的。
  • 映射共享区域: 若a.out程序与共享对象(或目标)链接,如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
  • 设置程序计数器(PC): 设置当前进程上下文的程序计数器,使它指向文本区域的入口点。
    图9-31加载器是如何映射用户地址空间的区域的

4.使用mmap函数的用户级存储器映射

Unix进程可以使用mmap函数来创建新的虚拟存储器区域,并将对象映射到这个区域。

mmap函数要求内核创建一个新的虚拟存储器区域,并将文件描述符fd指定的对象的一个连续的片(chunk)映射到这个新的区域。连续的对象片大小为length字节,从文件开始偏移offset字节的地方开始。

munmap函数删除虚拟存储器的区域: munmap函数删除从虚拟地址start开始的length字节组成的区域。

#include <unistd.h>
#include <sys/mman.h>

void *mmap(void *start, size_t length, int port, int flags, int fd, off_t offset);
                          返回: 若成功则为指向映射区域的指针,若出错则为MAP_FAILED(-1)

int munmap(void *start, size_t length);   返回: 若成功返回0,出错返回-1

9.9 动态存储器分配

动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆(heap)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。

分配器两种风格:

  • 显式分配器: 应用显式地释放任何已分配的块。
  • 隐式分配器: 分配器检测一个已分配块何时不再被程序使用,那就释放这个块。隐式分配器也叫垃圾收集器,自动释放未使用的已分配块的过程称为垃圾收集.

1.malloc和free

#include <stdlib.h>

void *malloc(size_t size);

void free(void *ptr);

malloc: 分配至少size字节的存储器块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。在Unix系统上,malloc返回一个8字节(双字)边界对齐的块。

动态存储器分配器,例如malloc,可以使用mmap和munmap函数,显式地分配和释放堆存储器,或者使用sbrk函数:

#include <unistd.h>

void *sbrk(intptr_t incr);

sbrk函数通过将内核的brk指针增加incr来扩展和收缩堆。

2.碎片

碎片:

  • 外部碎片
  • 内部碎片

3.分配器的数据结构

分配器需要一些数据结构,允许它来区别块边界,以及区别已分配块和空闲块。

1). 隐式空闲链表
一个简单的堆块的格式如图9-35,此时,块由一个字的头部、有效载荷,以及可能的额外填充组成。头部编码了这个块的带下,以及这个块是已分配的还是空闲的。
图9-35一个简单的堆块的格式
假设块格式如图9-35所示,我们可以将堆组织为一个连续的已分配块和空闲块的序列,如图9-36所示.这种结构称为隐式空闲链表。分配器可以通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。

隐式空闲链表的优点是简单。显著缺点是任何操作的开销,例如放置分配的块,要求空闲链表的搜索与堆中已分配块和空闲块的总数呈线性关系。
图9-36用隐式空闲链表来组织堆
放置已分配的块:放置策略:

  • 首次适配
  • 下一次适配
  • 最佳适配

2). 显式空闲链表
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
图9-48使用双向空闲链表的堆块的格式

9.10 垃圾收集

垃圾收集器定期识别垃圾块(即程序不再需要的已分配块),并相应的调用free,将这些块放回到空闲链表中.

一种垃圾收集算法: Mark&Sweep(标记&清除)算法.

1.垃圾收集器基本知识

垃圾收集器将存储器视为一张有向可达图,如图9-49。该图的节点被分成一组根节点和一组堆节点。每个堆节点对应于堆中一个已分配的块。根节点对应于一种不在堆中的位置,它们中包含指向堆中的指针。
图9-49垃圾收集器将存储器视为一张有向图
当存在一条从任意根节点出发并到达p的有向路径时,我们说节点p是可达的。在任何时刻,不可达节点对应于垃圾。垃圾搜集器是维护可达图的某种表示,并通过释放不可达节点并将它们返回给空闲链表,来定期回收它们。

2.Mark&Sweep垃圾收集器

由标记(mark)阶段和清除(sweep)阶段组成,标记阶段标记出根节点的所有可达的已分配的后继,而后面的清除阶段释放每个未被标记的已分配块。在标记阶段的末尾,任何未标记的已分配块都被认定是不可达的,是垃圾,可以在清除阶段回收。
图9-51mark和sweep函数的伪代码
图形化解释:
如图9-52,堆由6个已分配块组成,其中每个块都是未标记的。第3块包含一个指向第1块的指针。第4块包含指向第3块和第6块的指针。根指向第4块。在标记后,第1块、第3块、第4块和第6块被做了标记,因为它们都是根节点可达的。第2块和第5块是未被标记的,因为它们是不可达的。清除阶段之后,这两个不可达块被回收到空闲链表。
图9-52标记和清除示例

猜你喜欢

转载自blog.csdn.net/u010772289/article/details/86498837