概述
根据冯•诺依曼计算机体系结构的定义,CPU和内存是最为核心的系统构件,CPU用于程序指令的执行,而内存作为磁盘数据的缓存,用于为CPU提供指令和数据保存的环境。
内存作为一种稀缺资源,如果管理不当或导致内存耗尽,程就无法执行,或内存被破坏导致进程、系统崩溃。所以,需要设计一套机制来有效的管理和使用内存,这种机制就是“虚拟内存”。
软件设计领域流传着这样一句至理名言:“软件领域的任何问题,都可以通过增加一个间接的中间层来解决”,这一句话同样适用于虚拟内存。虚拟内存就是处于操作系统和硬件体系结构之间的,关于物理内存的一个软件抽象,其有效的解决了内存管理的诸多问题,比如多个进程如何有效的使用内存,以及内存的安全使用问题,从而提升整个计算机系统的性能,是进程和内存管理系统的设计基础。
虚拟内存系统是一个复杂的系统,它需要CPU(MMU)、磁盘文件、操作系统软件相互协同来实现。其为操作系统用户提供了三种能力:
- 为进程提供了一致的、独立的地址空间,抽象了内存访问接口。每个程序面对的“内存空间”都是一样的,不需要关心系统到底有多少物理内存,也不用关心进程如何使用这些内存,这些都交给虚拟内存去做,从而大大简化了程序的编译、链接以及运行模式。
- 通过虚拟内存机制实现了内存作为磁盘文件的高速缓存的目的。虚拟内存通过物理内存与磁盘文件之间的映射,将物理内存抽象为磁盘的缓存,并按需将磁盘文件“分块的”缓存到内存中,避免了大量的内存空间浪费,大大提升了内存的使用效率。
- 虚拟内存通过设置内存的访问权限,解决了内存的安全访问,比如访问一个非法的虚拟内存地址,就会触发“段错误”,从而保护了内存不会意外破坏。
下面几节从零开始,逐步讲解虚拟内存的基本原理。
地址寻址和地址空间
地址寻址
物理内存本质上是一个线性连续的、大小相同的大数组,这个数组的元素大小都是一个字节,每个字节都有自己的编号,即物理地址PA(Physical Address)。CPU使用PA进行内存寻址的过程十分的简单,CPU每次发出的都是实际的物理地址,对应内存的一个位置,过程如下:
图中,CPU想要读取从PA为4开始的4个字节。CPU直接使用物理地址进行寻址的这种方式,优点是简单、高效,目前在单片机系统中被广泛使用。但是,对于复杂的计算机系统来说,这种直接物理地址寻址的方式就很不适合了,这就引出了虚拟地址寻址的方式。
虚拟地址(Virtual address),将物理内存与真正的内存使用者间进行了隔离,解耦了内存使用者和物理内存之间的强关联性,然后,通过虚拟内存这个抽象层进行通信,虚拟地址就是通信协议。 这样说可能太抽象了,举个例子,大家应该都去过银行办业务,由于银行的柜台就那么几个,办业务的人数又多,所以,去银行的第一件事就是排队取号,大家注意拿到的这个号,并不代表去哪个柜台办业务,只是说你前面等待办业务的人数,不过需要等着叫号系统叫到你之后,才能办理业务,而且最终办理业务的柜台是随机的。好了,类比一下就是,银行柜台好比是物理内存,而排队序号就是虚拟地址,我们想办业务只能通过手里拿着的这个 “虚拟地址”,然后,必须通过叫号系统,将虚拟地址,即排队序号翻译成具体的物理地址,即柜台号,我们才能办理业务。
下图就是虚拟地址寻址的一个系统。
CPU使用虚拟地址就行内存寻址,CPU发出的虚拟地址,必须通过MMU这种地址翻译硬件,将虚拟地址翻译成物理地址才能最终读取到内存中的数据。CPU芯片上的叫做内存管理单元(Memory Management Unid)的硬件利用内存中的虚拟地址查询表动态的完成地址翻译工作(address translation)。
地址空间
地址空间(address space)是一个非负整数地址的有序集合。如果地址空间中地址是连续的,那么这就是一个线性地址空间(liner address space)。虚拟内存系统中,CPU从 虚拟地址空间(virtual address space) 中生成访问数据的虚拟地址,虚拟地址空间的定义如下:
N=2n{0,1,2,...,N−1}N=2^n \{0,1,2,...,N-1\}N=2n{0,1,2,...,N−1}
虚拟地址空间的大小由CPU的位数来决定,比如,32bit CPU,N=2^32。
**物理内存空间(physical address spcae)**表示计算系统中物理内存,其也是连续的,
M=2m{0,1,2,...,M−1}M=2^m \{0,1,2,...,M - 1\}M=2m{0,1,2,...,M−1}
这里需要注意的是,同一块物理内存,可以在不同的地址空间中进行表示。比如,对于物理内存中的一个字节,同时在物理内存空间和虚拟内存空间都有所表示,即物理地址和虚拟地址,这有点类似于平时宇宙的概念 :),这也是虚拟内存的基本思想,即物理内存在PAS和VAS之间的映射和转换。
虚拟内存作用
缓存
内存通过虚拟内存系统缓存磁盘上的虚拟地址空间 页表,页表命中、缺页,性能问题(程序的时间局部性),Linux统计缺页的工具,如何反应当前系统的性能状态。
理论上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,作为数组的索引。磁盘上数组的内容被缓存在内存中。这里说的数组单元可以是一个可执行的程序文件,或一个普通的文件。磁盘内容缓存说的就是,磁盘的一个文件内容通过虚拟内存缓存到内存中的过程。
页面
上面说了,虚拟内存可以将磁盘文件缓存到内存中,就像存储器层级结构中的CPU高速缓存一样,磁盘上的文件被分割一个个大小相等的块,作为磁盘和主存之间的传输单元。这种块在虚拟内存中称为虚拟页(Virtual Page,VP),VP的大小为P=2^p字节,类似的,物理内存也被分割为物理页(Physical Page, PP),也被称为页帧(page frame),大小与虚拟页相等,也是P=2^p。
任何时刻,虚拟页面的集合分为三个不相交的子集:
- 未分配的,VM系统未分配(未创建的)的页,未分配的页没有任何数据和它关联,也就不占用任何磁盘空间。
- 缓存的,已缓存在内存中的已分配的磁盘页。
- 未缓存的,未缓存在内存中的已分配的磁盘页。
下面展示一个具有8个虚拟页的虚拟内存系统。
如图,VP0、VP3虚拟页面暂未分配,VP2、VP5、VP7页面暂未缓存,VP1、VP4、VP6页面已经缓存到物理内存。
缓存组织结构
整个计算储存系统中,包含有很多级的缓存,如下图:
虚拟内存系统主要涉及L4和L5这两个存储层级,L4就是主存,L5就是本地二级存储,一般就是磁盘。L4缓存着从本地磁盘取出的磁盘块,也就是虚拟页。
下面这个表更详细的讲解了各级缓存的各种属性。
这里需要注意的磁盘要比主存慢100,000多倍,所以,如果主存中的缓存不命中,系统将会为此付出昂贵的代价,因为系统只能到比主存慢100,000多倍的磁盘上读出数据。而且,从磁盘的一个扇区去读第一个字节的时间开销比起读这个扇区中连续的字节要慢100 000倍(因为磁盘是机械部件,第一个字节需要机械寻址,这时很慢的!),因此需要谨慎的设计主存缓存的组织结构,以降低由于缓存不命中而带来的巨大的代价。基于以上原因,主存缓存的组织结构设计有以下几个关键点:
- 虚拟页设计的比较大,通常是4KB~2MB,这个比较好理解,虚拟页越大,主存中缓存的数据就越大,发生缓存不命中的概率就会小很多。
- 主存的缓存是全相联的,就是说任何虚拟页可以放置到任何物理页中,这样就可以使主存缓存尽量多的虚拟页,同样减少了缓存不命中的概率。
- 缓存的替换策略十分的重要,因为替换错了虚拟页的处罚非常之大。
- 由于磁盘的访问时间很长, 缓存的落盘策略总是使用写回方式, 而不是直写。
页表
我们已经知道了CPU通过虚拟地址访问物理内存的,那CPU是如何通过虚拟地址找对应的物理页的呢?答案就是通过一种叫做**页表(page table)**的数据结构,页表将虚拟页映射到物理页。上面提到过,MMU负责虚拟地址的翻译工作,其在每次翻译虚拟地址时,都会读取主存中的页表,查看虚拟地址对应的物理页是否命中,如命中,主存就会将数据返回到CPU,否则,不命中的话,将会启动缺页处理程序,进行缺页处理,后面会讲到缺页的原理。OS会负责页表的维护,以及缺页的处理过程。
下图展示了一个页表的基本结构:
每个页表项,称为一个PTE(Page Table Entry),页表就是由这些PTE组成的数组。上图的PTE中包含两部分:1)有效位;2)物理页编号或者磁盘页编号。
- 有效位,该位表示当前虚拟页是否被缓存在主存中,1表示已缓存,0表示为缓存。
- 物理页编号或者磁盘页编号,如果有效位为1,该值表示虚拟页在主存中的缓存的物理页号,如果为0,表示虚拟页在磁盘中的保存位置。这里需要的注意的是,如果虚拟页尚未分配的话,该值为null。
这里需要注意的一点是,页表必须包含所有的虚拟页,就说上面的图,共有8个虚拟页,那么页表必须也得有8个PTE,于每个虚拟页一一对应。
下面看一下上图中页表,可以看到,VP1、VP2、VP7、VP4已经缓存,VP3、VP6尚未缓存,剩下的VP0、VP5虚拟页尚未分配,所以,PTE地址那一项为null。这里也可以看到,主存的缓存方式是全相联的,所以任何物理页都可以包含任何虚拟页。
这里插一句,页表本质就是一个保存着虚拟地址与物理页之间的映射关系的数组,这里为什么选择数组来实现这种结构呢?我想有这几个原因:
- 通过下标来访问数组是所有数据结构中最快的,没有之一。而虚拟地址与物理页之间正好满足这方式。
- 通过硬件来实现数组的查询十分的简单。
- 数组实现简单、高效。
页命中
上节讲了页表的基本原理,这节模拟一下CPU使用虚拟地址访问一个字的过程,参考下
图。
- CPU想要读取一个字,该字包含在VP2中,发送一个虚拟地址。
- 地址翻译硬件,将虚拟地址翻译为页表中PTE的索引,即2,并从内存读取PTE。
- PTE的有效位为1,表示VP2虚拟页已经缓存,即表示页命中。
- 通过PTE中物理页的编号,找到主存中缓存的VP2,读出CPU想要的那个字到寄存器。
缺页
虚拟内存系统中,将主存的缓存不命中称为“缺页(page fault)”,缺页是一种硬件异常,最终会调用OS定义的缺页处理处理程序进行缺页处理。
下面通过一个例子演示下缺页的处理过程。
第一幅图是缺页之前的页表状态,1)CPU发出一个读取VP3中一个字的请求;2)地址翻译硬件通过虚拟地址确定VP3对应的PTE,通过PTE有效位确定,VP3未缓存到主存中,从而产生缺页异常;3)缺页异常调用缺页处理程序,并选定主存中的VP4为牺牲页,如果VP4为脏页,会将其换出到磁盘上;4)修改页表中VP4对应PTE,反应PP4不在缓存在主存中了。
第二幅图就是缺页之后的页表状态,5)将VP3从磁盘复制到主存的PP3,然后更新页表中VP3对应的PTE,反应PP3已经缓存到主存中了;6)缺页处理程序返回之后,CPU重新执行被中断的指令,尝试读取主存的VP3中的字,这次可以读取成功了。
这里体现了虚拟内存关于页面换入的策略,就是按需页面调度(demand paging),即,虚拟内存系统在初始化时,所有的虚拟页面都不会主动的缓存到主存中,只有发生主存缓存不命中时,才会将缺失的页调入到主存中。
页面分配
下面说一下,如何虚拟内存系统如何分配一个虚拟页面。当用户调用malloc或者mmap时,操作系统内核就分配相应数量的虚拟页,并更新页表PTE,如下图,分配一个虚拟页VP5,并更新了VP5对应的PTE指向磁盘中的VP5的位置。
虚拟内存的性能
通过上面几节,我们基本了解了虚拟内存的工作原理,可是仔细想一下,每次内存访问都会执行上面的流程,而缺页调度又会产生昂贵的性能代价,表面上,虚拟内存的性能应该十分低下才对,可事实恰恰相反,它工作的十分的好,这主要归功于程序的局部性(locality)。
局部性原则保证在任意时刻,程序趋向于在一个较小的**活动页面(active page)**集合上工作,这个集合一般会常驻内存,从而可以确保在程序初始开销之后,程序中的引用基本上会命中工作集中的页面,从而大大减少缺页导致的磁盘交互流程,从而确保系统的性能。
所以,只要我们的程序具有良好的时间局部性,虚拟内存系统就能工作的相当好。当然,如果某个程序较差的时间局部性,导致工作集的大小超过了物理内存的大小,那就会使程序处于一种不幸的状态,即“抖动(thrashing)”,这时页面会不停的换进换出,虽然虚拟内存可以工作,但是,程序的性能会像蜗牛一样缓慢,这时,应该考虑程序是否发生了抖动。