[操作系统] 虚拟内存

来源:互联网 发布:java五子棋视频教程 编辑:程序博客网 时间:2024/05/22 01:37

虚拟内存


  • 虚拟内存
    • 什么是虚拟内存
    • 为什么要使用虚拟内存
    • 分页
    • MMU如何工作
    • 页表
    • 分页式系统中的问题
    • 加速分页过程
      • 转换检测缓冲区
      • 软件TLB管理
    • 针对大内存的页表
      • 多级页表
      • 倒排页表


1. 什么是虚拟内存

每个程序拥有自己的地址空间,这个空间被分割成多个块,每一块称作一页。每一页有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。

2. 为什么要使用虚拟内存

尽管上一篇文章中提到的基址寄存器和界限寄存器可以用于创建地址空间的抽象。但是并没有考虑到应用程序与内存的发展。在现有情况下,需要运行的程序很可能大到内存无法容纳。所以便诞生了虚拟内存。

3. 分页

大部分虚拟内存系统都使用了分页的技术。在程序中,所产生的地址称为虚拟地址,它们构成了一个虚拟地址空间。在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存总线上,读写操作使用具有同样地址的物理内存字;而在使用虚拟内存的情况下,虚拟地址不是被直接送到内存总线上,而是被送到内存管理单元(MMU),并通过它把虚拟地址映射为物理内存地址。

分页

如图中的例子,有一台可以产生16位地址的计算机(2^16B),地址范围从0~64KB,这些地址都为虚拟地址。另外这台计算机只有32KB的物理内存。假如一个程序大小为64KB,虚拟内存可以容纳64KB的程序,但是因为大于物理内存的实际容量,所以这个程序并不能完全调入内存运行(磁盘上会有一个该程序的64KB副本,在需要时要被调入到内存中)。

虚拟地址空间按照固定大小划分成叫做页面的若干单元。在物理内存中与之对应的大小相等(通常情况下)的单元称为页框。在这个例子中为4KB,所以我们可以得到16个虚拟页面和8个物理内存中的页框。我们要记住内存与磁盘的交换总是以整个页面为单位执行的。

标记0K~4K的页面的虚拟地址为0~4095,相应的标记0K~4K的页框则表示它的物理地址为0~4095。当程序试图访问地址0时,执行一条带有该地址的指令,然后将该**虚拟地址**0送到MMU。MMU看到虚拟地址在页面0(因为页面0的虚拟地址范围是0~4095),根据映射结果(即图中页面里的数字),这一页面对应的是物理内存中的页框2(物理地址范围8192~12287),因此MMU该指令的地址变为8192。内存对MMU的存在一无所知,只是得到了访问地址8192的命令,从而MMU有效的将虚拟地址0映射成了物理地址8192。

上述例子中我们用的是页面的起始地址,如果我们想要访问虚拟地址20,过程也是一样的。只不过需要一个偏移量而已。MMU发现该虚拟地址在页面1,距离起始地址0的偏移量为20。并且发现对应的页框是2。MMU找到页框2的其实地址8192,然后加上偏移量地址就变成了8212。

到此,我们解决了如何将虚拟地址映射到物理地址的问题,但并没有解决虚拟内存大于物理内存的问题。如果程序想要访问的虚拟地址是32780(在虚拟页面8中,页面8的起始地址为32768)该怎么办?我们可以看到该页面中的标志是”X”,并不是对应的页框号,这表明该页并没有被映射,在实际的硬件中,用一个“在/不在”位来记录是否被映射的状态。

MMU发现该页面没有被映射,于是使操作系统陷入到操作系统,这个陷阱称为缺页中断。操作系统在物理内存中找到一个很少使用的页框,并把它写回到磁盘上。随后把需要访问的页面读到刚才被回收的页框中,修改映射关系然后重新启动引起陷阱的指令。假如操作系统决定回收页框1,需要对MMU映射做两处修改。首先将虚拟页面1改为位映射(因为回收前该页面映射到页框1),然后将虚拟页面8的”X”改为1。完成后,MMU就将虚拟地址32780(32K+12)映射到物理地址4108(4K+12)。

4. MMU如何工作

MMU工作原理

该图显示了在16个4KB页面情况下MMU的内部操作。

该图中虚拟地址为0010000000000100(对应10进制的8196),该16位地址被分为:

  1. 高4位的页号:0010(对应10进制的2)
  2. 低12位的偏移量:000000000100(对应10进制的4)

使用4位页号因为足以表示0~15这16个页面,12位的偏移量可以表示一页内全部4096个地址。页号可以作为页表的索引并以此得出对应于虚拟页面的页框号。该例中,可以找到对应的虚拟页面为0010即10进制的2,发现”在/不在”位为1,即该页面存在映射且映射的值为110。将该值复制到输出寄存器的高三位,加上虚拟地址中低12位的偏移量即得到物理地址1100000000000100。

要注意的是,使用其他位数来表示页号也是可以的,不同大小对应不同页面大小。

5. 页表

页表的目的是把虚拟页面映射为页框。从数学角度来说,页表是一个函数,它的参数是虚拟页号,结果是物理页框号。通过这个函数可以把虚拟地址中的虚拟页面替换为页框,继而得到物理地址。

页表项的结构(不同机器并不相同):

  1. 页框号:最重要的域,因为页映射的目的就是找到该值。
  2. 在/不在位:该项表示映射是否存在。该位是0就表示对应的虚拟页面不在物理内存中,访问该页面会引起缺页中断。
  3. 保护位:指出一个页允许什么类型的访问。例如,0表示允许读/写,1表示只读。
  4. 修改位:如果一个页面在内存中被修改过,则必须写回磁盘。如果未被修改过,则可以不写回。因为是从磁盘中读入内存的,所以磁盘中存在它的有效副本。这一位经常被称为脏位。
  5. 访问位:用来帮助选择要被淘汰的页面。
  6. 禁止高速缓存位:对于那些映射到设备寄存器(针对I/O设备)而不是常规内存内的页面,非常重要。例如,一个I/O设备要对操作系统作出响应,如果从高速缓存中读取副本,可能是过时的副本。

6. 分页式系统中的问题

在分页式系统中,我们需要考虑两个问题

  1. 虚拟地址到物理地址的映射必须非常快。因为每次访问内存,都需要进行映射。所有的指令最终都必须来自内存,并且很多指令也会访问内存中的操作数。

  2. 如果虚拟地址空间很大,页表也会很大。计算机至少使用32位的虚拟地址,而现在大多数位64位。假设页长为4KB,32位的地址空间对应的大小是4,194,304KB,即需要100万页,那么页表需要100万条页表项。另外,每个进程都有自己的地址空间,即每个进程都有自己的页表。

所以我们需要采用下面提及的一些方法

7. 加速分页过程

加速分页过程可以解决映射速度较慢的问题。

1. 转换检测缓冲区

假设一条指令要把一个寄存器中的数据复制到另一个寄存器。在不分页的情况下,这条指令访问一次内存,即从内存中取指令。而有了分页后,因为要访问页表引起多次访问内存,这样必定会影响性能。

研究者发现,大多数程序总是对少量的页面进行多次访问。因此实际上只有很少的页表项会被反复读取。解决方法是使用一种硬件设备称为转换检测缓冲区(TLB),可以将虚拟地址直接映射到物理地址,而不必再访问页表,有时这种设备也被称为相联存储器。它通常在MMU中,包含少量的表项。每个表项记录一个页面的信息(包含虚拟页号、修改位、保护位、和对应的页框),每一项与页表中的域一一对应,但增加了虚拟页号(页表中可以没有)和有效位,记录表项是否在使用。

TLB

工作原理:将一个虚拟地址放入MMU进行转换,硬件首先通过将该虚拟页号与TLB中所有表项进行匹配,如果虚拟页面在其中,且要进行的访问不违反保护位。则将页框号直接从TLB取出,而不必访问页表。如果不符合保护位,则产生保护错误。当虚拟页号不在TLB中时(失效)。则进行正常的页表查询,然后从TLB中淘汰一个表项,并使用新的页表项代替。当一个表项从TLB被清除时,将修改位复制到内存中的页表项,除了访问位,其他值不变。当页表项从页表装入到TLB时,所有值都来自内存。

2. 软件TLB管理

当发生TLB实效时,不再是由MMU到页表中查找并取出需要的页表项,而是生成一个TLB失效并将问题交给操作系统解决。系统必须先找到该页面,然后从TLB中删除一个项,接着装载一个新的项,最后在执行先前出错的指令。

无论是使用硬件还是软件来处理TLB失效,常见方法都是找到页表并执行索引操作以定位要访问的页面。用软件代替硬件找到页表并执行索引操作以定位将要访问的页面。这样产生的问题是,页表可能不在TLB中,会导致处理过程中额外的TLB失效。采用的解决方法是可以再内存中的固定位置,维护一个大的TLB表项的软件高速缓存(该高速缓存的页面总是被保存在TLB中)来减少TLB失效。

两种不同的失效:

  1. 当一个页面访问在内存中而不再TLB中时,会产生软失效。此时要做的是更新TLB,不需要磁盘I/O。
  2. 当页面本身不在内存中,则产生硬失效,此时需要一次磁盘存取以装入该页面。

7. 针对大内存的页表

如何处理巨大的虚拟地址空间

1. 多级页表

多级页表

在上图的例子中,32位的虚拟地址被划分为10位的PT1域,10位的PT2域和12位的偏移量。因为偏移量(Offset)为12位,所以对应的页面长度为2^12B,即4KB,共有2^20个页面。

引入多级页表的原因是避免把全部页表一直保存在内存中。特别是那些从不需要的页表。例如,一个需要12MB内存的进程,最底端为4MB程序正文段,然后是4MB的数据段,顶端是4MB的堆栈段,在数据段上方和堆栈段下方是大量没有使用的空闲区。

在图示的例子中,针对一个4GB(32位地址对应的大小)大的虚拟地址空间,用一个具有1024个表项的顶级表。并用PT1域的值对应,这时虚拟空间会被分为1024个4MB的空间,即顶级表中每一个表项对应的是一个4MB的空间。

顶级表的表项0指向程序正文的页表,表项1指向数据页表,表项1023指向堆栈页表,其他表项没有使用。PT1的值作为顶级表的索引,PT2的值作为二级页表的索引。

假如,有一个32位虚拟地址0x00403004(对应10进制的4,206,596),位于数据部分12292字节处。通过该虚拟地址的2进制0000 0000 0100 0000 0011 0000 0000 0100可得,PT1=1,PT2=3,Offset=4。

先用PT1作为索引访问顶级页表中的表项1,则地址范围为4MB~8MB。然后使用PT2作为索引访问对应4MB~8MB的二级页表,应为表项3,虚拟地址范围应是12288~16383绝对地址此时为(4MB+12288~4MB+16383 = 4206592~4210687)把该值加上偏移量4,则为实际的物理地址4206596。

在该方法中,实际只需要4个页表,顶级页表,正文段(0~4MB),数据段(4~8MB),堆栈段(顶端4MB)的二级页表。顶级表中其他1021个表项的”在/不在位都设为0”,访问它们时强制产生缺页中断。操作系统应该采取行动,如杀死进程。

可以将二级页表扩充为三级、四级、甚至更多级。级别越多,灵活性越大,但会增加复杂性。而且,此方法对于64位的机器并不友好,所需要页表太大,可使用下面的方法。

2. 倒排页表

实际物理内存中每一个页框有一个表项,而不是每一个虚拟页面有一个表项。例如,对于64位虚拟地址4KB的页,1GB内存,一个倒排页表只需要262144个页表项 (1GB/4KB)。可以节省大量空间(当虚拟内存比物理内存大的时候)。

但是从虚拟地址到物理地址的转换很困难。当进程n访问虚拟页面p时,硬件不再能通过把p当做索引来查找物理页框。取而代之,必须搜索整个倒排表来查找一个表项(n, p)。此外,该搜索必须对每一个内存访问操作都要执行一次,而不仅仅是在发生缺页中断时执行。

可以使用TLB解决,记录频繁使用的页面,地址转换就可能变的和正常页表一样快。但是当TLB失效时,需要搜索整个倒排表。一个解决方法是,可以为此建立一张散列表,使用虚拟地址来散列。当前所有在内存中具有相同散列值的虚拟页面被链接在一起。如果散列表中的槽数和物理页面数一样多,那么散列表的冲突链的平均长度会是1个表项。一旦页框号被找到,新的(虚拟页号,物理页框号)对就会被装载到TLB中。

参考书目:现代操作系统第三版

原创粉丝点击