通过上一篇文章的扯淡,我们应该已经明白了存储器的层次结构 不管什么程序最后的直接/间接的编译结果都是0和1(我们直接理解为汇编)。(这点不知道的欢迎阅读我的另一篇文章关于跨平台的一些认识)比如这句汇编代码mov eax,0x123456;它的意思是将内存0x123456处的内容送往eax这个寄存器。各个应用的数据共同存在内存中的。假设有一个音乐播放器应用的汇编代码中引用了0x123456这个内存地址。但是同时运行的应用有很多那其他应用也完全有可能引用0x123456这个地址。那为什么竟然没起冲突和错误呢进程是计算机领域最重要的概念之一什么是进程进程是关于某次数据集合的一次运行活动 是运行在它自己地址空间的一段自包容程序 解释的通俗的点 一个程序在运行时我们会得到一个假象该进程好像是独占地使用CPU和内存CPU是没有间断地一条接一条的执行该程序的指令所有的内存空间都是供该进程的代码和数据分配使用的。(这点不严谨其实内存还有一部分要分给内核kernel)。说起来这个程序就好像得到了全世界一样。CPU是我的内存也全部我的妹子们还是我的。当然这是假象而已。但是这些假象又是怎么做到的呢程序中都会引用库API比如每个C程序都要引用stdio.h库的printf()在程序运行时库代码也要被加入到内存这么多程序都引用了这个库难道我内存中需要加很多份吗这自然不可能那么库代码又是怎么被所有进程共享的呢这些让我们细思恐极的疑问都将通过这篇文章来给大家解答。物理和虚拟寻址在访问者看来主存就是一个有M个字节大小的单元组成的数组每字节都有一个唯一的物理地址(Physical Address, PA)。 它的访问地址和数组一样第一个地址为0后面地址依次为1,2,3-----M-2, M-1;这叫做线性地址空间。这种自然的访问内存的方式我们称之为物理寻址(physical addressing)。注意在访问内存时对于任意一个地址(不管是第0个还是第M-1个)访问该地址的时间总是相同的。在各种数据结构中我们都说hash表是最快的比红黑树之类的都要快那hash表为什么最快那是因为hash表内部本质上是使用了数组。所以还是数组最快那数组为什么最快这是因为我们知道数组的起始地址以及某个元素的序号就可以得到该元素在内存中的地址而对于内存访问任意一个地址访问时间总是相同的。而类似链表树等结构却只能靠遍历了。(不过好的hash算法还是很难设计的这是另外一个话题了)。图10一个使用物理寻址的系统上图是一个物理寻址的示例这是一条加载指令它读取从物理地址4开始的4个字节CPU通过内存总线将指令和地址传递给主存主存读取从物理地址4处开始的4个字节返回给CPU。因为这篇文章主要讨论 虚拟内存是关于L4级主存和磁盘之间的交互问题为行文方便文章中有时候直接说内存代指主存。所以这些不要误以为是指L1L2之类的缓存。如果看不懂这段话啥意思务必看看我的上一篇文章什么是内存一存储器层次结构,然后再来看这篇文章。早期计算机使用物理寻址方式但是到了现在的多任务计算机时代普遍使用的是虚拟寻址(virtual addressing)。如下图所示图11一个使用虚拟寻址的系统CPU 通过一个虚拟地址virtual address,VA来访问主存这个虚拟地址在被送到主存之前会先转换成一个物理地址。将虚拟地址转换成物理地址的任务叫做地址翻译address translation。地址翻译需要 CPU 硬件和操作系统之间的配合。 CPU 芯片上叫做内存管理单元Menory Management Unit, MMU的专用硬件利用存放在主存中的查询表来动态翻译虚拟地址该表的内容由操作系统管理。有少数现代计算机系统依旧在使用物理寻址方式比如DSP嵌入式系统超级计算机系统。这些系统的主要任务是执行单一任务不像通用性计算机那样需要执行多任务。可以想象到物理寻址方式更快。这个道理和关于跨平台的一些认识文章中理论上java比C慢的道理是一样的。前面解释完虚拟地址那么关于文章开头时提的那些疑问可能有些人心里面都有数了。因为那些地址都是虚拟地址并非真实的物理内存当中的地址。基本思想已经懂了那么剩下的我们就更具体的讨论细节。进程地址空间图12:进程地址空间上图是一个64位的进程地址空间编译器在编译程序时将结果编译成32/64位的地址空间。虚拟寻址方式简化了编译器链接器的工作。同样也因为虚拟内存每个进程才能有很大的一致的私有的的地址空间。这方便了内存管理保护了每个进程的地址空间不被其他进程破坏。同时也方便了共享库。虚拟内存也是一种缓存思想虚拟内存将主存看成是一个磁盘的高速缓存主存中只保存活动区域并根据需要在磁盘和主存之间来回传送数据。从概念上来说虚拟内存被组织成为一个由存放在磁盘上的 N 个连续的字节大小的单元组成的数组也就是字节数组。每个字节都有一个唯一的虚拟地址作为数组的索引。虚拟内存的地址和磁盘的地址之间建立影射关系。磁盘上活动的数组内容被缓存在主存中。在存储器层次结构中磁盘(较低层L5参见我们上篇文章图4)的数据被分割成块(block)这些块作为和主存(较高层,L4)之间的传输单元。主存作为虚拟内存(或者说磁盘)的缓存。虚拟内存VM系统将虚拟内存分割成称为大小固定的虚拟页Virtual Page,VP每个虚拟页的大小为固定字节。同样的物理内存被分割为物理页Physical Page,PP,大小也为固定字节物理页也称作页帧page frame。在任意时刻虚拟页面都分为三个不相交的部分未分配的(Unallocated)VM 系统还未分配或者创建的页未分配的页没有任何数据和它们关联因此不占用任何内存/磁盘空间。缓存的(Cached)当前已缓存在物理内存中的已分配页。未缓存的(UnCached)该页已经映射到磁盘上了但是还没缓存在物理内存中。其中未分配的VP不占用任何的实际物理空间这点要理解。32位程序地址空间就有4G至于64G的程序它的地址空间是一个非常大的天文数字(貌似是16777216T)而目前我们的电脑高配的也就2T磁盘16G内存。如果64位程序每个VP都映射着实际的PP。无论如何也对应不上的。并且也完全没必要一一映射,图12:进程地址空间中可以看到地址空间内有大量的空白。毕竟程序不可能实际使用那么大的地址空间。图13VM使用主存来作为缓存上图展示了在一个有 8 个页面的虚拟内存中虚拟页 0 和 3 还没有被分配所以在磁盘上不存在。虚拟页 146 被缓存在物理内存中。虚拟页 257 已经被映射分配了但是还没有缓存在主存中。当然那个图上标注的不对,VP 部分n-p和N-1应该分别标注为3和7,不过我们找不到更合适的图了(这种图自己画压力太大了)。所以大家知道我们假设共有8个VP就好了。页表(page table)系统必须得有办法判定某个虚拟页是否缓存在主存的某个地方。这具体可分为两种情况。已经在主存中就需要判断出该虚拟页存在于哪个物理页中。不在主存中那么系统必须判断虚拟页存放在磁盘的哪个位置并且在物理主存中选择一个牺牲页并将该虚拟页从磁盘复制到 主存替换这个牺牲页。这些功能由软硬件联合提供包括操作系统CPU中的内存管理单元Memory Management Unit,MMU和一个存放在物理内存中叫页表page table的数据结构页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换成物理地址时都会读取页表。图14页表