转载于http://blog.csdn.net/u013471946/article/details/46890933 加了一些自己的理解
一、前言
虚拟存储器,感觉很难,至少说很复杂,里面涉及到的东西也比较枯燥。当然,如果能彻底搞清楚,对继续学习操作系统原理是百利无一害的。
玩C或C++的人,经常通过&a的方式获取变量地址,并将其赋值给指针变量,也通常用printf打印出地址的值,类似0x8048
034之类的地址值,但要从此刻开始要明确一点,你打印出的这个地址值,根本不是内存里的真实值,而是通过链接器最终生成的虚拟地址,这个虚拟地址帮助程序在操作系统进程中正常运行,而每个进程里的虚拟存储分布的值几乎都一样……所以才会出现,你各进程中打印出的地址为啥分布区间那么相似。
虚拟存储器是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的、私有的地址空间。它有三个重要的功能:
①它将物理内存看成磁盘的高速缓存,永远只保留活动区域,从而高效利用物理内存
②它为每个进程提供一致的线性地址空间,从而简化存储器管理
③它保护每个进程的地址空间不被其他进程破坏。
真实物理内存中,空闲空间并非连续,可能零散的分布在各个角落,而虚拟存储器,就是把各对象的地址值分布在连续线性的逻辑地址空间中。那么如何将线性地址和零散的物理地址对应起来呢?这就需要地址翻译。CPU在处理对象地址时,利用内部MMU,通过访问存放在物理内存中的查询表来动态翻译虚拟地址,该表内容是由操作系统管理的。
扯了这么多是不是觉得很乱? 这里涉及到一个重要的存储器层次结构的概念。回忆下我写的《程序性能优化探讨(3)》章中的第一节就讲到“存储器层次结构”。里面图清晰的把存储器层次结构描述了出来:存储速度由快到慢、由高到低一次是:CPU寄存器、L1\L2\L3高速缓存(SRAM)、主存(内存,DRAM)、磁盘……
当世界上还有没有虚拟存储器概念时,内存DRAM显然就是磁盘的上一级缓存,就像SRAM是内存DRAM的上一级缓存一样类似。but现在发展出了虚拟存储器的概念,在磁盘上开辟了连续的空间玩票,那么我们可以理解为:虚拟存储器VM事实上成为磁盘的缓存!而谁又是VM的上一级缓存呢?自然而然就应该想到是内存DRAM。事实上VM相当于从存储器体系中的内存和磁盘之间的位置强行插入,新增的一个层次罢了!为啥要强插?很简单。因为磁盘读取速度实在太慢,极端情况下可能比DRAM的速度慢10 0000倍,根据存储器层次结构的思想,为了调和速度差,增加一个叫做虚拟存储器的缓存结构也就理所当然了。
二、虚拟存储器一般性概念
虚拟存储器成简称为VM。
先来看下比较拗口概念解释:虚拟存储器被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组……这句话除了觉得很抽象外,我们至少能获得两个关键信息:
①虚拟存储器的载体是磁盘,他和其他存储器一样,存放数据,而且它在磁盘上是连续存储的;
②虚拟存储器的每个字节数据一定对应唯一的1bit虚拟地址。
首先,我们要分清楚虚拟存储器和虚拟地址的区别,一个是逻辑上存储数据的对象;一个是逻辑上标识这些数据的概念地址。根据以上的定义,我们可以YY一下虚拟存储器的大概应用:虚拟存储器似乎和物理存储器有某种强对应关系,它利用磁盘上连续的存储数据,可以实现线性连续地址空间访问。也许他能优化磁盘与物理内存器之间的通信关系。
接下来我们先恶补看下什么是线性地址空间。回忆下,比如你有n位(bit)的,数据位,你能表达最大的整数就是2^(n) - 1,你能表示的所有整数的个数就是N=2^(n),(注意哦,最大整数是从0起的,而数个数是从1起:)),也就是说,能表示的所有数为:(0,1,2,…,N-1)这N个数。如果每个数都来标识一个地址,而每个地址又代表8字节(byte)的大小的空间,那么N个整数就能标识N*8字节的线性地址空间。
举个例子,32位机,由于是32bit,而每个字节又是8bit,所以32位机就是由4byte组成地址值。比如0x12345678,就是典型的4字节,32位。注意这里的位(bit)可不是16进制0x里表示的某一个位哦!而是比如0x12,表示一个字节,对应8位,它的二进制就是00010010。
32位系统的n=32,那么N=2^(2+10+10+10)=4Gbit,它的线性地址空间就是(0,1,2,...,4G-1)。这里再废话一下,为啥说32位机可以标识最大4Gbyte的地址,而这里算出来最大是4Gbit?哼哼!傻瓜才去用1bit地址对应1bit数据,那样多浪费啊!肯定是1bit地址对应8bit的数据,也就是说一位地址对应一字节数据,这样才合理嘛!!!
我们设n和N表示虚拟地址空间,m和M表示实际物理地址空间。有了前面的铺垫,我们可以想象一下,一字节数据,比一定只对应1bit地址哦!可以有多个1bit地址号称标识这一字节数据哦!至少目前,我们有N和M两个地址空间,可以同时来标识某一字节的数据!
接下来描述一个事实:磁盘上的数据要想缓存到物理内存中,不可能一位一位缓存进内存,也不可能一字节一字节缓存进内存。那到底以多大的批量缓存呢?我们把这个批量称为块,磁盘上的数据被分割为块,将它作为磁盘与内存之间的传输单元。好了,虚拟内存如果要来凑热闹,也得有相应的机制,于是VM系统也将VM分割成固定大小的块,我们将它称为虚拟页(VP)。这虚拟页既然有某个固定大小,肯定是多少字节之类的单位,于是我们定义虚拟页大小为P=2^p字节。当然,物理内存肯定也有类似的页大小。
卡住!是不是又有点晕了?P=2^p字节是个什么东东?这里我们可以借助极限思维来想象一下:如果压根没有虚拟页这种东西,那么我们传数据通常一字节一字节来玩,于是此时p=0,P=1字节,对否?哦,也就是说,这里的p,很类似N=2^n,利用位的概念来标识虚拟页的字节大小。假设VM系统的源代码里要定义虚拟页大小,应该是类似unsigned char page_addr_len之类的变量,就可以标识虚拟页P大小是256 = 2^8。
开始的确有点难理解,其页实也就是定义页的大小,但为什么是以2为低其实也很好理解,你的内存大小不也是以2为底来计算多少个字节。
好了,我们知道由于虚拟页的存在,将连续的虚拟存储空间分割成若干个虚拟页,当虚拟页大小是P字节,而我们又晓得整个虚拟空间大小是N字节,于是能轻易得出,虚拟存储器被分割成N/P=2^(n-p)个页,也就是我们得到虚拟页空间(0,1,2,……,N/P-1)。
同理,物理内存也有类似的分页概念,也被分割成M/P=2^(m-p)个页,于是得出物理页空间(0,1,2,……,M/P-1)。
三、虚拟页和页表
好了,有了这么冗长的铺垫,再来看下面的图,应该没那么懵了吧:
左边的VP代表虚拟页面,右边的PP代表物理页面,我们可以看到,虚拟存储器中的页原封不动的对应上了物理存储器中的页。
虚拟页面的集合都分为三个不相交的子集:
未分配的:说明该页没对应任何数据,因此就不占用磁盘空间……哇塞,大家有没有发现——不占用磁盘空间!难道说,VM虽然在磁盘上申请了连续空间,但它具体使用时不一定按连续的方式占用空间?有待考证;
缓存的:当前缓存在物理存储器中的已分配页……首先它得是已分配的页(有数据和它关联),并且当前正临时放在物理存储器中玩的页;
未缓存的:没有缓存在物理存储器中已分配的页……首先它也是已分配的页(有数据和它关联),只是暂时被剔除出物理存储器了。
神马?有人问为啥图的右边比左边短?→_→介个……我想说的是,两边在某个极端情况下可能一样高,此时M=N,这是什么时候?当你玩32位机时,土豪般的插了4G的内存条, 有可能两边一样长O(∩_∩)O~一般来说,屌丝插的内存条肯定少于理论最大内存值的,而虚拟存储器是可以设置成理论最大值,不信你到windows设置中找到虚拟内存设置,我的32机允许设置的最大值就是4092MB,因此作者画这个图时也充分考虑了这种普遍性……作者真为屌丝考虑O(∩_∩)O~
既然DRAM缓存的虚拟页面,那VM系统总需要一个种方法来判定某个虚拟页是否存放在DRAM中,如果是,系统还必须确定这个虚拟页存放在(DRAM)中哪个物理页中;如果不是(不命中),VM系统还必须寻找这个虚拟页在磁盘的什么位置。这就有两种情况:
①该虚拟页已经被VM系统分配,只是该虚拟页还未被缓存到DRAM中——说明该虚拟页已经对应上了磁盘里具体的某块数据
②该虚拟页还未被VM系统分配,也就是说这个虚拟页编号在VM看来还不存在——说明在磁盘上的虚拟存储器中找不到该虚拟页号(这种强行访问可能会出段错误哦!!!)
问题就是,谁来标识虚拟页这些不同的状态呢?答案是页表!首先注意,页表是专门存放在内存DRAM中特定区域的,它全面记录了所有可能的虚拟页号的当前情况:
从上图中可以看出,右上方的表,描述了内存DRAM中PP0~PP3Z这4个物理页中存放的VP1~VP4这四个虚拟页表;右下方的表,描述了虚拟存储器中已分配的虚拟页号;
而左边的表,就是大名鼎鼎的页表。PTE是页表条目,有效位为1时说明该虚拟页缓存在DRAM中,否则便有是上面描述过的两种情况:
①VP3和VP6两个虚拟页已经被分配,只是暂未缓存到DRAM中;
②VP0和VP5两个虚拟页还未被分配,于是对应的PTE0和PTE5就是null——说明这两个页号还没映射磁盘上的其他数据(比如暂时没需求),因此页表自然也将其置空了。
好了,这么完美动人的页表,是谁来生成和维护呢?答案是操作系统,而且操作系统还要负责完成磁盘和DRAM之间具体的传送页工作。
那又是谁会需要去读取页表的内容呢?是CPU里MMU(存储器管理单元)中的地址翻译硬件,当CPU需要将一个虚拟地址转换成物理地址时,都会读取页表的内容。
说到这我们稍微总结一下:虚拟存储器部署在磁盘上;页表部署在内存中;OS维护页表内容;MMU读取页表内容。
一旦理解了页表,类似页命中和缺页就好理解了。比如上图中你引用VP1\2\7\4都是页命中,可你要想起引用下VP3,地址翻译硬件读取PTE3时发现有效位0,于是处罚缺页异常。
缺页也没什么大不了,此时内核会调用异常处理程序,通过某种策略决定要赶走PP3中的VP4,此时无论VP4是否被修改,内核都会将其写回磁盘,然后从磁盘拷贝VP3到存储器PP3中,并更新PTE3,随后返回并重启导致缺页的指令,地址翻译硬件会重新翻译VP3的虚拟地址到物理地址,如下图:
这里专门提一下,以上讨论都针对的虚拟存储器——页表——物理存储器之间的数据交换,那么虚拟存储器和磁盘上原始数据的关系如何呢?是否清楚?事实上,每个进程都有自己的虚拟存储器,相当于每个进程都在磁盘上开辟了属于自己的虚拟页空间,因此每个进程都有诸如VP1~VP7这些虚拟页存储在磁盘上,因此物理存储器中也为每个进程准备了独立的页表。但是,物理存储器却可能被不同进程的虚拟页共享到同一个物理页上!这有什么好处呢,比如标准库中的printf函数,每个进程都可以将这个相应的虚拟页映射到同一个物理页上。
还有一个更神奇的细节,既然磁盘上的可执行文件在变成进程时,又会在磁盘上的虚拟存储器中开辟属于自己的私有虚拟页区域,那么可执行文件的文本部分(.init、.text、rodata等)就会同时出现在磁盘的两个位置?!也就是说,我们所谓的linux进程的存储器映射(如下图),根本就不放在内存中,而是放在磁盘中?!是不是很毁三观?事实上,加载器从来不把任何可执行文件的数据拷贝到物理存储器中,而是在页面第一次被引用时,由虚拟存储器系统从磁盘调入物理存储器的!也就是说,下图这个伟大的进程映射的实体,事实上是放在磁盘中的虚拟存储器里,并且被VM系统分配成诸如VP1~VP7这些虚拟页面,然后按需依次调入物理存储器的!
四、地址翻译
地址翻译听起来很高大上,但如果看明白上面的讲解,尤其是理解了虚拟页物理页以及页表的概念,那理解起地址翻译来也是灰常容易的!
回忆下,既然虚拟存储器被分配成一个个虚拟页,每个虚拟页大小的是2^p字节,那肯定有一个方法去识别,某个字节或者某段数据具体属于哪个虚拟页,又在这个页的哪个位置,对否?既然虚拟页大小是2^p字节,说明我们用p位二进制地址就能标识这个页中的每一个字节了。而虚拟地址共n位,也就说还剩n-p位,干嘛呢?当然是标识虚拟页号了,有n-p位就能标识2^(n-p)个虚拟页。(注意哦,2^p的单位是字节,2^(n-p)的单位是“个”千万别搞混!)
我们把标识2^p字节虚拟页中具体字节位置的量称为虚拟页面偏移量,用VPO表示;把标识2^(n-p)个虚拟页的号称为虚拟页号,用VPN表示。于是就有下面的虚拟地址结构:
VPN (n-p)位 | VPO p位 |
而物理页也有相同的结构,其物理地址结构:
PPN (n-p)位 | PPO p位 |
再回忆下,页表这个东西,为什么能把虚拟页和物理页对应联系起来呢?事实上,页表条目的PTE数组才是对应关系的关键,PTE数组下标能对应相应的虚拟页号VPN,而数组中存的又是物理页号PPN,这样当VPN与PPN建立强联系后,剩下的偏移位,直接将VPO复制给PPO就完事,于是有了下面的过程图:
这里又会涉及到一个现象,既然物理内存有上一级缓存SRAM,比如说就是L1,CPU中MMU与物理内存之间的任何数据交换都会经过L1的查找和处理。比如,MMU需要和物理内存交换PTE、PTEA(PTE的标号)、PA(物理地址)这些信息,L1都会检查是否先前已有缓存。如果命中,直接由L1提供MMU所需的数据;如果不命中,则由L1向物理内存申请数据,于是就有了下图的过程:
那么MMU在访问L1时,是用物理地址还是虚拟地址呢?相信自己脑补都应该有结论,肯定是物理地址啦!至少大部分系统都是这么处理的。
五、利用TLB加速地址翻译
从目前的设计来看,MMU如果需要翻译CPU产生的虚拟地址,就必须获得PTE,恰巧L1命中的情况还好,若不命中,就得花费几十上百的周期到物理内存中调用。而贪婪的人类试图尽可能的消除这样的开销,于是在MMU中专门开辟了一个小的缓冲区,称为翻译后备缓冲器TLB(Translation Lookaside Buffer)。
事实上,TLB很类似对虚拟地址已有的划分策略,比如之前我们把虚拟地址划分为VPN和VPO,都是为了体现特定的层次结构。那么现在可以把VPN部分再进行划分:
TLB是由标记(TLBT)和索引(TLBI)组成,如果TLB有T=2^t个组,那么TLBI就刚好是t位,而VPN剩余位就刚好留给TLBT……留来干什么?其实我也还搞不清楚,待下节分解。