海贼王米娜2怎么玩在秃毛哥初高中的时候因为电脑性能无法满足打游戏需求,经常在百度上寻求优化方法,什么清理垃圾,卸载程序,各种奇巧淫技都用上了,其中就有人说到把虚拟内存调高的方法。当初看到这个方法时,真的眼前一亮,居然还能自己来配置内存的大小,也不管这个虚拟是啥意思,一顿操作猛如虎啊。毕竟不管你是不是程序员,大部分人都知道内存对电脑或者手机性能的重要性,往大了买总没错。 到如今成为从业者,我试图从专业的角度来剖析一下为什么内存对手机和电脑系统是重要的,以及虚拟内存的概念,工作方式和原理。但我们的重点会放在虚拟内存上。之所以专门为虚拟内存写一篇文章,主要出于以下几点考虑:
正如我在文章开头说的,提起虚拟内存,大部分程序猿都能来上两句:解决操作系统物理内存不足的问题;进程之间数据隔离;提升了系统的安全性等等,说的都没错。那么,如果我把内存扩大之后能把虚拟内存关掉吗?虚拟内存的性能如何,很好吗,为什么好?虚拟内存中,各个进程的数据依然是写入物理内存中的,如何做到数据隔离的?如果被面试官问到了这些问题,大家都能回答上来吗?
扯远了,我们回到虚拟内存的话题上。那么什么是虚拟内存?虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上物理内存通常被分隔成多个内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
操作系统的硬件资源是所有进程共享的,cpu的多核技术,进程调度算法等都是为了适应用户对资源需求的增长。但是共享主存会有一些特殊的挑战。当然单台服务器的内存可以做的很大了(了解到现在有单台服务器48TB内存的),但对于大部分家用机来说,内存的限制还是存在的。
我们知道,进程的执行是需要将数据先装进内存的。如果有太多的进程要执行,就需要更多的内存,当内存不够时,有的进程就没办法得到执行,一般操作系统会要求你先关掉某些程序。
并且,大量的进程数据都写入内存的话,对于管理来说也有很大的挑战。如果某个进程不小心写了另一个进程使用的存储,就会出现各种意想不到的结果。也许这样的话,对于程序猿的要求就会更高,因为我们需要更多的关注内存的使用。也许一个内存溢出的错误,不单单影响自己的程序,甚至可能将系统上其他的程序搞崩,因为你改了其他进程的数据。
但是幸好,虚拟内存技术被提出来了。它的出现,使我们的工作变得简单,让计算机的可靠性得到极大提升。但由于它总是在幕后工作,往往被人忽略。不过这也不妨碍它成为计算机系统中最重要的概念之一。
主存上每个字节都有固定的地址,这个地址我们称为物理地址。cpu通过物理地址,直接从主存上获取数据的方式我们称为物理寻址。下图描述的就是在一个32位机上物理寻址地址为4的字的过程,即从地址4开始加载了4个字节的数据。
这个是早期pc使用的寻址方式,现代处理器使用的是虚拟寻址,如下图。虚拟寻址在硬件上增加了地址翻译(MMU),并且cpu拿到的也不再是物理地址了,而是虚拟地址。虚拟地址通过MMU置换出物理地址,然后再从主存中拿到数据。
知道了虚拟寻址的基础概念后,我们现在从宏观来看看操作系统如何使用虚拟内存的。如下图展示的是在一个32位按字节寻址且具有2G主存的操作系统上虚拟内存工作的示意图。 系统中同时运行有3个进程,由于有虚拟内存的存在,每个进程都能分配到4G的虚拟内存(32位操作系统最大支持4G)。虚拟内存在工作时,会将虚拟内存和物理内存按照相同的固定大小切分,一般来说是4KB~2MB,图中按照2MB来划分。因此,每个进程的4GB虚拟空间会被分割成2048块,物理内存的2GB空间被分割成1024块。由于主存只有2GB空间,并且需要在3个进程中共享(本例中是3个,实际更多),很显然主存是不够用的。不过由于我们已经把虚拟内存和物理内存都分块了,所以我们只需要把cpu运行时需要用到的数据块加载到内存即可,其他的数据块只要找个地方存起来,在cpu重新用到的时候重新加载回内存即可。
计算机系统中,除了内存能存如此大块的数据外,也只剩下磁盘了。虽然磁盘相对主存来说可能慢了100000多倍,但没关系,只作为存储使用的话,磁盘还是很靠谱的。不过这里也引发了一个问题,当操作系统发现主存中的数据都不是自己想要的,需要从磁盘重新加载回数据块时,这个“惩罚”力度是很大的,因为这里需要进行一次磁盘操作。到这里应该会有同学质疑,那是不是使用了虚拟内存就会导致我们的程序变慢呢?因为局部性原理的存在,虚拟内存实际上工作的很好。局部性原理保证了在任意时刻,程序往往在一个较小的活动页集合上工作。因此,大部分情况下需要重新从磁盘加载数据的概率很低,性能并没有太多的折损。但是,当程序的活动页集合的大小超过了物理存储器的大小时,程序会进入到一种颠簸的状态,频繁地进行页面地换入换出,这时候程序会以龟速运行。作为程序猿需要知道有这种可能性,尽可能地写出时间局部性好的程序。
既然我们已经将每个程序的虚拟内存划分成了2048块,而且有的块可能已经加载到内存中,可能被挪到了磁盘中,甚至可能还没有分配。很自然的想法就是我们需要有个地方能管理这些数据块,因此也就有了页表这个东西(因此数据块也被称为页,在本例中每页是2MB)。每个进程都有自己的页表,页表条目(PTE)和虚拟内存的块数量一致,每个条目中会记录对应块相关的信息。其中比较重要的是:
物理页号或磁盘地址:记录数据块在内存或者磁盘中的位置。如果为null,说明数据块还没有被用到
页表条目(PTE)当然还记录了其他信息,但是总的来说整个页表不会很大。比如在我们的例子中,每个程序对应的页表也就2048个条目,所需的内存空间很小很小。因此,每个程序的页表是存储在主存中的,即常驻内存。MMU在将虚拟内存翻译成物理地址时,会先定位到虚拟地址所在的数据块,比如虚拟地址编号0 ~ 2097151在虚拟块0(VP0),虚拟地址2097152 ~ 4194303在虚拟块1(VP1),然后查询数据块对应的页表条目(PTE),通过有效位判断数据块在主存还是磁盘中。如果在主存中,我们称为页命中;如果不在主存,称为缺页。
页命中时,取数的效率很高,直接通过页表中记录的物理页号地址,然后根据虚拟页偏移量即可生成物理地址,如下图。其中虚拟地址中的虚拟页偏移量(VPO)和物理地址中的物理地址偏移量(PPO)是一样的,因为这个偏移量主要是用来标明数据在数据块中的位置,而虚拟地址空间和物理地址空间都是按照一样的大小来切分的,偏移量保持一致就行。在我们的例子中,虚拟地址是32位的,页表有2048项,所以虚拟页号(VPN)有11位,剩余21位作为虚拟页偏移量(VPO)。物理页一共有1024个,所以物理页号(PPN)使用10位即可。
缺页时,操作系统会产生一个缺页中断,即把当前运行中的程序打断,然后调用内核中的缺页异常处理程序将需要的页面加载到内存中更新页表,最后恢复原来的程序,重新进行一次地址翻译。此时所需的数据块已经在内存,不再中断,然后按照上面说的地址翻译过程拼接出物理地址取数即可。如下图
当然了,缺页中断会涉及到复杂的页面调度算法,比如主存被占满的情况下,如何选择一个页面淘汰等等。这里不详细介绍页面调度算法了,有机会在写一篇细说。最后,作为对比,我们也看下页命中的情况,MMU是如何取数的。
我们已经了解了虚拟内存寻找的过程,即每次cpu产生一个虚拟地址,MMU就必须查阅一个页表条目(PTE),以便将虚拟地址翻译为物理地址。由于页表存储在主存中,和直接物理寻址相比,这里就需要多一次内存查询,代价是几十到几百个周期。所以,许多系统都引入了翻译后背缓冲器(TLB)来加速这个过程。 TLB是个小的虚拟寻址缓存,每一行都保存了一个页表条目(PTE)。如此一来,MMU在每次地址翻译时,先从TLB中查找PTE,如果TLB命中,就免去了主存查询,所有的地址翻译步骤都是在芯片上的MMU执行的,因此非常快。
到目前为止,虚拟内存技术的关键点已经介绍完了。虽然还有很多细节没有提到,比如页面置换算法,多级页表等。但我相信大家在理解了上述内容的基础上继续深入学习的话会事半功倍。最后,在理解了虚拟内存运行方式的基础上,回过头来看看虚拟内存到底给操作系统带来了哪些影响。
虚拟内存技术将虚拟地址空间和物理地址空间划分成了固定大小的块,并借助页表来管理数据块的调度。将即将要用的数据缓存到主存上,暂时不用的数据置换到磁盘上。
操作系统为每个进程都提供了一个独立的页表和完整的虚拟地址空间,并将虚拟地址到物理地址的转换对外透明,极大的方便了应用程序的开发人员。具体表现在:
简化了链接: 由于每个进程都有各自独立且相同的虚拟地址空间,这就允许每个程序的存储结构具有相同的基本格式,比如文本节,程序段,堆和栈的起止位置,栈的增长方向等等。这样的一致性极大的简化了链接器的涉及和实现。
简化加载: 虚拟内存使得容易向存储器中加载可执行文件和共享对象文件。比如我们可以修改页表条目,让页表条目指向我们希望加载到内存中的数据块,它们可能还在磁盘上,但没关系,在需要这些数据的时候,操作系统会以缺页中断的形式将数据加载到内存中。unix提供的mmap系统调用就允许应用程序自己做内存映射,它其实就是通过将虚拟页映射到任意文件的任意位置来实现的。
简化共享: 虚拟内存为每个程序提供了独立的空间,每个程序有自己的代码,数据,堆和栈。但有些情况下,不同进程之间还是会有共享代码和数据的情况,比如操作系统内核代码就是共享的。内核代码在主存中只要有一份就够了,操作系统会将不同进程适当的虚拟页映射到相同的物理页面,从而达到共享内核代码的目的。一些库函数也是类似的道理。
简化内存分配: 程序猿只和虚拟内存打交道了,代码中不管是用new,malloc或者其他什么方式开辟内存空间,都只是在虚拟内存空间中申请虚拟页面。直到使用到的时候才会将他们映射到物理存储器中。这也是为什么我们的代码能开辟出超过物理内存大小的内存块。
这很好理解,因为操作系统是不能允许进程之间互相篡改私有数据,不能允许用户程序来修改内核代码和数据,不能允许程序修改自己的只读数据。虚拟内存让这种数据隔离变得很容易。首先,每个进程有各自独立的地址空间,互不影响;其次,通过在页表中加入一些控制位,可以标识出该数据块所拥有的访问权限,从而在地址翻译的过程中对数据块做保护,因为每个虚拟地址总是需要通过地址翻译找到对应的物理地址的。
说了这么多,大家应该对虚拟内存有足够深的了解了,最后,我们来探讨这个有趣的话题。我自己在网上找了很多相关的提问,认为应该关闭虚拟内存的同学主要持有以下两个观点:
家用机的物理内存一般不会太大,关闭虚拟内存意味着所有程序都要加载到物理内存中执行,物理内存很快会被占满,此时操作系统一般会要求你关闭一些程序释放出内存才能启动新程序。
虚拟内存作为现代操作系统默认开启的能力,关闭后可能导致系统变得不稳定。因为没有了虚拟内存带来的好处,比如存储器保护的能力
局部性原理从理论上告诉我们虚拟内存的可行性,加上TLB的引入,我们可以认为虚拟内存对性能的影响微乎其微。
现代操作系统不论是Windows还是macOS对虚拟内存都做了足够的优化,甚至页面的读写对ssd也做了相应的优化。
|