虚拟内存提供了的三个重要的能力:(1)它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,他高效地使用了主存。(2)它为每个进程梯控了一致的地址空间,从而简化了存储器管理.(3)它保护了每个进程的地址空间不被其他进程破坏。
虚拟存储器被组织成一个由存放在磁盘上的N个连续字节大小的单元组成的数组。虚拟存储器的内容是被缓存在主存中的,他们通过页进行交换。术语DRAM缓存表示虚拟存储器系统的缓存,它在主存中缓存许页。任意时刻,虚拟页面都分为三个不相交的子集:
页表存放在物理存储器中,,它将虚拟页面映射到物理页面,页表条目称为PTE,包含有效位和地址位,地址位包含物理页号,如果有效位为1,则其地址位指向DRAM中相应的物理页的首地址,表示已缓存,如果有效位为0,若地址位为空,则该虚拟页未分配,否则其地址位指向该虚拟页在磁盘上的起始位置。
当DRAM缓存不命中时称为缺页,会触发一个缺页异常,缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,并从磁盘中拷贝所需要的虚拟页到物理存储器,,更新页表的内容,随后返回,返回时,它会重新启动引起缺页异常的指令,这时指令可以正常执行。
地址翻译的步骤如下:
页命中:
第一步:处理器生成一个虚拟地址,并把它传给MMU。
第二步:MMU生成PTE地址,并从高速缓存/主存中请求得到它。
第三步:高速缓存/主存向MMU返回PTE。
第四步:MMU构造物理地址,并把它传送给高速缓存/主存。
第五步:高速缓存/主存返回所请求的数据字给处理器。
页不命中:
第一步:处理器生成一个虚拟地址,并把它传给MMU。
第二步:MMU生成PTE地址,并从高速缓存/主存中请求得到它。
第三步:高速缓存/主存向MMU返回PTE。
第四步:PTE中的有效位为0,MMU触发缺页异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
第五步:缺页处理程序确定出物理存储器中的牺牲页,如果这个页面已经修改过,则把它换出到磁盘。
第六步:缺页处理页面调入新的页面,并更新存储器中的PTE。
第七步:缺页处理程序返回到原来的进程,并再次执行引起缺页异常的指令。
物理寻址的高速缓存和虚拟存储器结合的主要思路是地址翻译发生在高速缓存查找之前,页表条目可以缓存,就像其他的数据字一样。
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常具有较高的相连性。如图所示,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。
下图展示了TLB命中时所包括的步骤。这里的关键是,所有地址翻译步骤都是在芯片上的MMU中执行的,因此非常快。
第一步:CPU产生一个虚拟地址。
第二步和第三步:MMU中TLB中取出相应的PTE。
第四步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。
第五步:高速缓存/主存将所请求的数据字返回给CPU。
当TLB不命中时,MMU必须从L1缓存中取出相应的PTE,新取出的PTE存放在TLB中,可能会覆盖掉一个已经有的条目。
多级页表:
多级页表作为一种层次结构的页表,的作用是用来压缩页表。使用层次结构的页表从两个方面减少了对存储器的要求。第一,如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在,这代表着一种巨大的潜在节约。第二,只有一级页表才需要总在主存中,虚拟存储器可以在需要时创建、页面调入或者调出二级页表,这就减小了对主存的压力,只有经常使用的二级页表才需要存在主存中。
下图是使用K级页表的地址翻译。
Linux虚拟存储器系统
Linux将虚拟存储器组织成一些区域的集合,也叫做段的集合,一个区域就是已分配的虚拟存储器的连续片,这些页是以某种方式相关联的,例如代码段、数据段、堆和共享段。每个虚拟页面都被保存在某个区域中,而不属于某个区域的虚拟页是不存在的。区域的概念允许虚拟地址空间有间隙。
内核为系统中的每个进程维护一个单独的任务结构(源代码中的task_struct)。任务结构中的元素包含或者指向内核运行运行该进程所需要的所有信息,例如PID和指向用户栈的指针等等。
上图中的pdg字段指向第一级页表的基址,而mmap指向一个vm_area_structs(区域结构)的链表,其中每个vm_area_structs都描述了当前虚拟地址空间的一个区域。当内核运行这个进程时,就把pdg存放在CR3控制寄存器中。
Linux的缺页异常处理,第一步先检查它访问的虚拟地址A是不是合法额,换句话说A是否在某个区域结构定义的区域内。第二部检查试图进行存储器访问是不是合法的,也就是说进程是否有读写或者执行这个区域内页面的权限。第三步,执行正常的页面交换。
存储器映射:
Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,称之为存储器映射。虚拟存储器区域可以映射到两种类型的对象的一种:(1)Uinx文件系统中的普通文件,一个区域可以映射到一个普通磁盘文件的连续部分。文件区被分为页大小的片,每一片包含一个虚拟页面的初始内容。因为执行按需进行页面调度,所以这些虚拟页面没有实际交换进入物理存储器,知道CPU第一次引用到页面。(2)匿名文件,匿名文件由内核创建,包含的全是二进制0.映射到匿名文件的页面有时也叫做请求二进制0的页,因为是用二进制0覆盖牺牲页,因此此时磁盘和存储器之间没有实际的数据传送。
共享对象:
一个对象可以被映射到虚拟存储器中的一个区域,要么作为共享对象,要么作为私有对象。对共享对象的任何写操作对于所有共享这个对象的进程都是可见的。
私有对象是使用一种写时拷贝的技术被映射到虚拟存储器中的。在物理存储器中只保留私有对象的一份拷贝,对于每个映射到私有对象的进程,相应的私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时拷贝。当有进程试图写私有区域某个页面时,就会触发一个保护故障。当保护故障注意到保护异常是由于进程试图写私有写时拷贝的页面而引起的,它就会在物理存储器中创建这个页面的一个新的拷贝,更新页表条目指向这个新的拷贝,然后恢复这个页面的可写权限。当故障返回时,CPU重新执行这个写操作。
fork函数
当fork函数被当前的进程调用时,内核为新进程创建各种数据结构,bing分配给它唯一的PID。为了给这个新的进程创建虚拟存储器,它创建了当前进程的mm_struct、区域结构和页表的原样拷贝。它将两个进程中的每个页面都标记为只读,每个区域结构都标记为私有的写时拷贝。因此fork函数返回时,新进程现在的虚拟存储器刚好和调用fork时存在的虚拟存储器相同。通过写时拷贝保持了私有地址空间的抽象概念。
execve函数
用于加载并运行函数,需要步骤如下:
可以用mmap函数来创建新的虚拟存储器区域,并将对象映射到这些区域中。
动态存储器分配
动态存储器分配器维护着一个进程虚拟存储器,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟存储器片,要么是已分配的,要么是空闲的。造成堆的利用率低的主要原因是碎片,有两种形式:外部碎片和内部碎片。分配器在对空闲块的管理策略上,有用隐式空闲链表和显示空闲链表。分配器有两种类型,显示分配器要求应用显示地释放它们的存储器块。隐式分配器(垃圾收集器)自动释放任何未使用的和不可达的块。