现代系统提供了虚拟存储器的概念,它是对物理内存的抽象。虚拟存储器是硬件异常,硬件地址翻译,主存,磁盘文件,操作系统的完美交互,它为每一个进程提供了一个大的,一致的私有的地址空间。

    

   

一:物理和虚拟寻址

    

      使用虚拟寻址时,CPU利用一个虚拟地址(Virtual Address,VA)访问主存,这个虚拟地址在被送到主存之前,需要先转换成物理地址。这个过程叫做“地址翻译”。地址翻译需要CPU硬件和操作系统的合作,CPU上的MMU(存储器管理单元)这种硬件,利用存放在主存上的“页表”,将虚拟地址翻译成物理地址,该表由操作系统负责管理。如图:

寄居虚拟化与全虚拟化的关系_寄居虚拟化与全虚拟化的关系


二:虚拟存储器作为缓存的工具

    

 

    

未分配的:这些页是没有任何磁盘上的数据和他们相关联,也就不占用磁盘空间。

缓存的:当前缓存在物理存储器中的已分配页。

未缓存的:没有缓存在物理存储器中的已分配页。

 

    

   

1:页表

    

 

    上述的功能是由软硬件联合实现的。包括操作系统,MMU中的地址翻译硬件和存放在主存中的页表。页表的功能,就是提供虚拟页到物理页的映射。每次,地址翻译硬件都会读取页表来进行地址翻译。操作系统负责维护页表的内容,并且负责磁盘和DRAM缓存之间的页传送。

    页表,是页表条目(PageTable Entry, PTE)的数组。虚拟地址空间中的每个页在页表中的一个固定偏移量处都会有一个PTE。

    每个PTE中,由一个有效位和一个n位的地址字段组成。如果设置有效位,表明该虚拟页已经被缓存在物理内存中了,也就是说,有相应的物理页与之相对应。那个地址字段就表明了物理页的起始位置。

    

寄居虚拟化与全虚拟化的关系_页表_02


2:页命中

    

寄居虚拟化与全虚拟化的关系_虚拟地址_03

    

 

3:缺页

    

寄居虚拟化与全虚拟化的关系_虚拟地址_04

寄居虚拟化与全虚拟化的关系_寄居虚拟化与全虚拟化的关系_05

    

     缺页异常要调用内核中的缺页异常处理程序。该程序选择一个牺牲页,此例中就是存放在PP3中的VP4,如果VP4已经被修改了,那么内核就会将VP4写回磁盘。然后,内核修改VP4的页表条目,反映出VP4已经不再缓存了,接下来,内核从磁盘拷贝VP3到DRAM缓存的PP3,更新相应的PTE3,然后返回。

    当异常处理程序返回时,它会重启那个导致缺页的指令,该指令重新执行,这一次就是页命中的情况了。

 

    虚拟存储器的概念是在20世纪60年代提出的,远在SRAM缓存之前,所以虚拟存储器使用了和高速缓存不同的术语,但是原理上是一致的,比如在虚拟存储系统中,块称为页,磁盘和内存之间的传送叫做页面调度。当缺页时才进行页面调度,这种方法叫做按需页面调度,所有现代操作系统都使用这种策略。还有其他的策略是预测不命中,在实际引用之前就调入页。

 

4:分配页面

    分配页面的情况如图:

寄居虚拟化与全虚拟化的关系_缓存_06

    当调用malloc时,通过在虚拟空间中创建空间VP5,然后更新页表中的PTE5,使它指向虚拟空间中的新创建的VP5。

 

5:局部性

    

 

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

    虚拟存储器大大简化了存储器的管理,并且提供了一种存储器保护的方法。

    操作系统会为每一个进程都提供一个独立的页表,因而也就是一个独立的虚拟地址空间。如下图,进程i的页表将VP1映射到PP2,VP2映射到PP7。进程j的页表将VP1映射到PP7,VP2映射到PP10。多个虚拟页面可以映射到同一个共享物理页面上:

寄居虚拟化与全虚拟化的关系_页表_07

    

1:简化链接

       独立的地址空间允许每个进程的存储器映像都采用相同的基本格式,而不管代码和数据实际存储在物理空间的何处,比如,在linux中,每个进程都采用类似的存储格式。文本段总是从虚拟地址0x08048000(32位)或者0x400000(64位)处开始。数据段和bss段紧跟其后,栈在进程地址空间的最高部分,并且向下生长。这样的一致性极大的简化了链接器的设计和实现,允许链接器生成全连接的可执行文件,这些可执行文件是独立于物理存储器中代码和数据的最终位置的。

2:简化加载

     虚拟存储器还使得容易向存储器中加载可执行文件和共享对象文件。比如linux下的ELF可执行文件,在创建进程时,linux加载器分配多个连续虚拟页的片,并把这些虚拟页设置为无效,然后将页表条目指向目标文件中适当的位置。这个时候,加载器并不拷贝任何数据从磁盘到存储器,只有在每个页在初次引用的时候,才根据需要进行页置换。

    

3:简化共享

    

    

4:简化存储器分配

    

 

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

    

    

寄居虚拟化与全虚拟化的关系_虚拟地址_08

    

    

 

五:地址翻译

    

寄居虚拟化与全虚拟化的关系_寄居虚拟化与全虚拟化的关系_09

    

        n位的虚拟地址包含两个部分:p位的虚拟页面偏移(VPO),n-p位的虚拟页号(VPN),因为物理页面和虚拟页面的大小是一样的,所以VPO也就是物理页面偏移(PPO)。MMU利用VPN作为索引来选择合适的PTE,比如VPN0选择PTE0,VPN1选择PTE1,以此类推。然后将页表中的物理页号(PPN)和VPO(=PPO)结合起来,就得到了相应的物理地址。

 

    

寄居虚拟化与全虚拟化的关系_页表_10

    

    

    

    

    

 

    

寄居虚拟化与全虚拟化的关系_缓存_11

    

    

    

    

    

   

1:结合高速缓存和虚拟存储器

     既使用高速缓存又使用虚拟存储器的系统中,都存在是使用虚拟地址还是物理地址来访问高速缓存的问题。大多数系统都是选择物理地址的。如图:

寄居虚拟化与全虚拟化的关系_虚拟地址_12

    

   

2:TLB加速地址翻译

     因为每次CPU产生一个虚拟地址,MMU都必须查阅一个PTE。这样在最糟糕的情况下,都会从存储器中取一次数据,代价就是几十到几百个周期。如果PTE碰巧在高速缓存L1中,那么这种开销可以下降到1个或者2个周期。然而,许多系统仍然试图消除这种开销。方法就是在MMU中包含一个PTE的缓存,称为翻译后备缓冲器(TLB)。

    

寄居虚拟化与全虚拟化的关系_页表_13

 

    下图展示了当TLB命中和不命中时的步骤,这里的关键是所有的地址翻译步骤都是在芯片上的MMU中执行的,因此非常快。

寄居虚拟化与全虚拟化的关系_缓存_14

寄居虚拟化与全虚拟化的关系_虚拟地址_15

 

    1:CPU产生一个虚拟地址

    2、3:MMU从TLB中取出相应的PTE

    4:MMU将这个虚拟地址翻译成一个物理地址,并将物理地址发送到高速缓存/主存

    5:高速缓存/主存将请求的数据返回给CPU。

    

 

3:多级页表

    

    

    

寄居虚拟化与全虚拟化的关系_页表_16

    

    

    

 

    

 

    

    

    

寄居虚拟化与全虚拟化的关系_缓存_17

 

4:地址翻译实例研究

    

存储器按照字节寻址;

存储器访问是针对1字节的字;

虚拟地址14位, 物理地址12位;

页面大小64字节;

TLB四路组相连,总共有16个条目;

L1是物理寻址,直接映射的,行大小为4字节,总共有16组;

 

    

    

寄居虚拟化与全虚拟化的关系_虚拟地址_18

    

寄居虚拟化与全虚拟化的关系_寄居虚拟化与全虚拟化的关系_19

 

    

    

 

寄居虚拟化与全虚拟化的关系_虚拟地址_20

    

 

    

0x03d4 = (00 0011 1101 0100)b

        将上面的二进制,按照页面偏移,TLB组索引,TLB标记重新分段是这样的:(000011 11 010100),组索引是11,标记是000011。所以查找TLB的第3组,标记位为3的有效位是1,说明TLB命中,因而直接查得PPN位0D。所以,物理地址为:001101 010100(0x354),根据这个物理地址查找L1,重新分段是001101 0101 00。所以,查找L1的第5组,标记位是0D的有效位是1,所以L1命中,块偏移为00,所以块0中的数据36就是对应于虚拟地址0x03d4的值。

   

    上面的情况是TLB命中了,如果TLB不命中,可以查看页表,因为VPN位为00001111,所以查看页表也能得到PPN位为0D。

 

六:linux虚拟存储系统

    

寄居虚拟化与全虚拟化的关系_虚拟地址_21

        它包括:代码段,数据段,堆,共享库,栈,还有内核虚拟存储部分。内核虚拟存储部分包含每个进程都共享的内核代码和数据。一组连续的虚拟页面,这样内核就可以方便的访问物理存储器中的页面了。内核存储的剩余部分是每个进程都不同的数据,比如页表,内核栈,进程数据结构等等。

 

    

寄居虚拟化与全虚拟化的关系_缓存_22

        内核为系统中的每个进程都维护了一个单独的任务结构:task_struct。这个结构包含或者指向内核运行该进程所需要的所有信息,比如PID,用户栈指针,可执行文件名称,PC等。

    

    

        vm_start:段的起始地址。

        vm_end:段的结束地址。

        vm_port:段的读写权限。

        vm_flags:指明该段是共享的还是私有的,以及其他的信息。

        vm_next:指向链表中的下一个vm_area_structs结构。

 

    

        1:虚拟地址是否合法:它会把A和链表中的每个vm_area_structs结构中的vm_start和vm_end进行比较,如果不合法,就会触发一个段错误。

    

    

 

七:存储器映射

       linux以及类unix系统,通过将虚拟地址空间和物理磁盘上的某个文件进行关联映射,以初始化虚拟地址空间。这个过程成为存储器映射。

    

        a:Unix中的普通文件,比如ELF可执行文件。

        b:匿名文件,也就是内核创建的,内容全是0。CPU第一次引用这样的区域内的虚拟页时,内核就在物理内存中找到一个牺牲页面,如果这个牺牲页面已经被修改,则将这个页面换出来,用二进制0覆盖牺牲页面,并更新页表。注意到磁盘和存储器之间没有实际的数据传送,所以,映射到匿名文件的区域中的页面也叫做请求二进制零的页。

 

1:共享对象(共享库)

    许多进程可能需要使用同样的只读文本区域,比如共享库等。如果每个进程都在物理内存中保持这些常用代码的复制拷贝,那就是极端的浪费了。存储器映射可以控制多个进程共享对象。

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

    

寄居虚拟化与全虚拟化的关系_虚拟地址_23

    因为每个对象都有一个唯一的文件名,内核可以迅速的判定进程1已经映射了这个对象,这就可以使进程2中的页表指向同一个物理页面。这样物理内存中只需保存一个共享对象的拷贝就可以了。

 

        私有对象映射,如图所示:

寄居虚拟化与全虚拟化的关系_虚拟地址_24

    私有对象使用一种叫做写实拷贝的技术,一个私有对象的开始生命周期的方式基本上与共享对象一样的,物理内存中只保存私有对象的一份拷贝。两个进程将一个私有对象映射到它们各自的虚拟空间中的不同区域,但是共享这个对象同一个物理拷贝。对于每个映射私有对象的进程,相应的页表条目标记为只读,并且区域结构被标记为私有的写实拷贝。只要没有进程试图修改该私有区域,那么他们就可以继续共享一份拷贝。

        但是,当有进程试图修改该私有区域时,就会触发一个保护故障,保护故障处理程序注意到保护异常是由于进程试图写私有的,写实拷贝的区域中的页面引起的,他就会在物理内存中创建这个页面的一个新拷贝,然后更新页表条目指向这个新的拷贝,然后恢复这个页面的写权限。然后故障处理程序返回,CPU重新执行写操作,这样就可以在新创建的页面上正常执行了。

 

2:fork函数:

    

        当fork返回时,新进程的虚拟空间刚好和调用fork时存在的虚拟空间相同,当父子进程的任意一个进行写操作的时候,写时拷贝机制就会创建新的页面。因此,也就为每个进程保持了私有空间的概念。

 

3:execve函数:

    

    

    

      2:映射私有区域,为新程序的文本,数据,bss和栈区域创建新的区域结构。所有这些区域都是私有的,写时拷贝的,如下图所示:文本段和数据段被映射为a.out文件中的文本和数据区域。bss段是请求二进制零的,映射到匿名文件,他的大小是包含在a.out中的。堆和栈区域也是请求二进制零的,初始长度为0.

    

    

寄居虚拟化与全虚拟化的关系_页表_25

八:动态存储器分配

    

     动态内存分配器维护者一个进程的虚拟存储区域,也就是堆,一般来说,它是向上增长的,紧接在未初始化的bss段的后面。对于每个进程,内核都会维护着一个指针brk,它指向堆的顶部。

    

    

   

1:malloc和free

        void *malloc(size t size);

        void free(void *ptr);

    

    

    malloc,可以通过mmap和munmap函数,显示的分配和释放堆空间,或者可以使用sbrk函数:void *sbrk(int incr);

     sbrk函数可以将内核的brk指针增加incr个字节来扩展和收缩堆。如果成功,就返会brk指针的旧值,否则返回-1。如果incr为0,那么sbrk就会返回brk指针的当前值。incr可以是负的,这样就可以收缩堆了。

    

   

2:分配器的要求和目标

    分配器的要求如下:

        a:处理任意请求序列,分配器只要求每个释放请求必须对应于一个当前已分配块,因此,分配器不能假设分配和释放请求的顺序,一个应用可以有任意的分配请求和释放请求序列。

        b:立即响应请求:分配器必须立即响应分配请求,不能为了提高性能而重新排列或者缓冲请求。

        c:只是用堆:分配器分配的块必须保存在堆中。

        d:对齐要求:分配器必须对其块,多数系统中,要求分配器返回的块是8字节边界对其的。

        e:不修改已分配的块:块一旦分配了,就不允许分配器修改它或者移动它。

 

    

    

 

3:碎片

    

    

    

 

4:实现问题

    要实现一个分配器,就需要考虑下面的问题:

空闲块的组织:如何记录空闲块

放置:如何选择一个合适的空闲块来放置一个已分配的块。

分割:将分配块放置在某个空闲块之后,如何处理空闲块中的剩余部分。

合并:如何处理刚刚释放的块。

 

5:隐式空闲链表

    

寄居虚拟化与全虚拟化的关系_虚拟地址_26

 

        这种情况下,块是由一个头部,有效载荷,填充部分组成。在头部中,编码了这个块的大小(包括头部和填充位),以及这个块是已分配的还是空闲的。

    

    

 

    

寄居虚拟化与全虚拟化的关系_寄居虚拟化与全虚拟化的关系_27

        可以将堆组织成为一个连续的已分配块和空闲块的序列,称之为隐式空闲链表,是因为空闲块是通过头部中的大小字段来隐含的链接者的。分配器可以遍历堆中的所有块,从而间接的遍历整个空闲块。注意,一般我们还需要一些特殊标记的块来标记结束块。本例中已一个设置了已分配位而大小是0的块为结束块。

    

 

6:放置已分配的块

    当一个应用请求k个字节的块的时候,分配器搜索空闲链表,查找一个足够大可以放置请求大小的空闲块。分配器执行这种搜索的的方式是通过放置策略确定的。一些常见的策略有首次适配,下一次适配,最佳适配。

    

    

    最佳适配:检查每个空闲块,选择最合适的空闲块。

 

7:分割空闲块

    当选择了合适的空闲块之后,就需要另一个策略决定需要分配这个空闲块的多少空间。一个选择是整个空闲块,虽然这种方式比较块,但容易造成内部碎片。另一种策略是将空闲块分割,第一部分是分配块,剩余的部分成为新的空闲块。

   

8:获取额外的堆空间

    如果分配器不能为请求块找到合适的空闲块,一种选择是合并那些相邻的空闲块来创建一个更大的空闲块,但是如果生成的空闲块还是不够大,或者说空闲块已经最大程度的合并了,那么分配器就会通过调用sbrk函数,向内核申请更大的堆空间。

   

9:合并空闲块

    

        为了解决假碎片问题,任何实际的分配器都必须合并相邻的空闲块。这就牵扯到什么时候合并的问题了,分配器可以选择立即合并,也可以选择推迟合并。立即合并简单明了,但是容易产生抖动,也就是块反复的合并和分割。一般来说,快速的分配器通常会选择某种形式的推迟合并

 

10:带边界标记的合并

    

        因而提出了一种新的块格式,就是边界标记。如图所示:就是在每个块的尾部加一个脚部。脚部是头部的一个副本。这样,每个分配器就可以查看前一个块的脚部来判断前一个块的起始位置和是否空闲了。这个脚部总是在距离当前块开始位置一个字的距离。

寄居虚拟化与全虚拟化的关系_缓存_28

    

        有一种优化方法,可以使已分配块不必有脚部,因为脚部的作用就是看前一个块是否是空闲块,那么可以把前一块的已分配/空闲标记位放在当前块的头部中,这样,当块是已分配块的时候,就不必拥有脚部了。因为释放一个已分配块的时候,可以查看该分配快的头部,判断前一块是已分配块,还是空闲块。

        不过,空闲块还是需要脚部的。

   

11:显示空闲链表

    

    

寄居虚拟化与全虚拟化的关系_页表_29

        因为程序不需要空闲块中的主体,所以可以在主体中存放指针,比如可以存放指向前一个和后一个空闲块的指针。

    使用双向链表而不是隐式空闲链表,这样首次适配的分配时间从块总数的线性时间减少到了空闲块数的线性时间。但是,释放块的时间可以是线性的,也可以是常数的,这取决于空闲链表块中的排序策略。