vmware的原理和影子页表

vmware启动的时候同时会有一个vmware-vmx启动,二者通过pipe或者socket通信,实际上,vmware只是一个输入/显示客户端,类似X服务器,它一般在一个窗口中运行一个虚拟操作系统。真正工作的是vmware-vmx这个进程,它和内核中monitor通信完成虚拟操作系统的执行和数据向vmware的传导和接收,通信的接口是/dev/vmmon字符设备,使用最多的就是其ioctl例程。
执行32位保护模式虚拟操作系统的虚拟机实现中最复杂的不是别的,正是虚拟OS名字中所体现的,它便是内存的保护,在运行在真实硬件上的操作系统上,它是由MMU实现的,而在虚拟机中,guest os上的进程的线性地址必然要转化为真实机器上的物理地址才可以,而这个转化并不能直接进行,因为guest os并不知道自己在虚拟机中运行,所以guest进程必然还是首先使用它自己的guest os的mmu机制来转化,现在问题就是如何将这个guest os的mmu和真实机器的mmu联系起来。vmware使用了所谓的影子页表的方式实现,其实这个影子页表只是一个转化后的结果,转化分为两步,第一步是guest的线性地址转化为guest的物理地址,第二步是guest的物理地址转化为host的真实物理地址,这里有趣的事就来了。
我们的虚拟机建立好以后,需要配置一下物理内存的大小,然后启动它以后会在目录下创建一个扩展名为.vmem的文件,其大小和你配置的物理内存的大小是一样的,这个文件将要映射到真实机器的内存当中,大小正好是虚拟机的“物理内存”大小,它是一个文件,是平坦的,然而它在内核真正需要的时候,其映射的物理内存却不一定是连续的,而可能是分离的页面,正是这些分离的不连续的页面提供给虚拟机一个连续的“物理内存”的印象,虚拟机中的“物理内存”就是这些不连续真实主机操作系统的页面。然而这是怎么做到的呢?想想也不难,任何现代的操作系统,不管是运行于虚拟机还是运行于真实机器,其对内存的访问都是要通过mmu的,这些操作系统所访问的都是虚拟地址,就连内核空间也不例外,也就是只要mmu能为这些虚拟地址映射到一个真实的物理页面,就算物理页面不连续也无所谓。理解这一点的时候,千万不要为linux内核内存的一一线性映射给迷惑了,倒是linux高端内存的映射方式是一种理解虚拟机“物理内存”的一种媒介。
有了以上的理论,下面要做的就是影子页表的实现了,这也不难,就是虽然当前运行的是虚拟机上的操作系统,然而其MMU操作还是要在真实机器中完成,也就是说,真实的机器内核空间运行着一个VMM的monitor程序,由它来提供虚拟机操作系统的所有的页表映射,也就是提供虚拟机操作系统运行所需的cr3寄存器的物理地址,这个物理地址指向的就是影子页表。
那么,如果vmm运行在ring0,而guest os的内核也运行在ring0,怎么能保证不混乱呢?办法就是让guest os内核运行在ring1,但是这样的话,guest os的用户进程执行了一个int X指令,由于int是陷入到ring0的指令,guest os怎么能收到并且提供系统调用服务呢?ring 0的vmm即使收到,又如何转发给运行于ring1的guest os让其自由处理呢?由是,我们不妨先按照自己的想法想一下如何实现,然后将已有的虚拟机实现往我们自己的实现上套,这样就不至于一开始就迷失于各个虚拟机复杂的文档和源码了。
那么虚拟机上的进程是如何工作的呢?如果知道了这个,虚拟机的运行方式也就知道了,然而最关键的就是它体现的是最复杂部分的实现,这个最复杂的部分就是内存虚拟化。
理解软件或者和硬件配合的软件如操作系统这种东西的最佳方式就是设想一个场景,如果这个场景被你相通了,内中各个步骤关节都被你搞明白了,那么你也就掌握了最关键的大局,接下来你再去看什么文档源码之类的东西,总之这种情景分析十分有效,起码对我十分有效。下面就以guest os上的fork为例来看一下怎么执行,选用fork是因为它有递归的性质,任何的新的进程在linux中都是被fork出来的,相反,如果选用read/write之类的就会把事情搞复杂,因为后者会涉及到虚拟化的另一个大的主题-IO虚拟化。guset os运行了linux,假定guest os上当前的一个进程的影子页表已经存在了,它fork一个子进程的时候将会创建另一个影子页表-有点数学归纳法的意思哦,过程如下:
1.fork为系统调用,本该陷入guest os的运行与ring1的内核,可是int指令却陷入了ring0的vmm;
2.vmm和guest os共享一部分内存,它既是host os中的一个内核执行绪,又可以被guest os触碰,它截获int指令,将之路由给guest os;
3.guest os接管这个int,发现要创造一个新的进程,于是创建页表:
3.1.在普通的host上的linux,不考虑高端内存的话,物理内存和虚拟内存是一一映射的,内核操作的也是虚拟地址,它们对应的物理地址连续是因为linux的实现就是这样,如果换成windows,就不是这样了,内核中不也有分页内存吗?
3.2.在虚拟机上的guest linux,它的运行并不依赖物理内存(前面说过,现代操作系统看到的都是虚拟内存地址,物理内存仅仅是一种资源),因此它所关心的就是有个机制能将现在要操作的虚拟地址转成物理地址就可以了,这是由mmu来完成的,已经比操作系统低了一个层次了,在mmu看来,它才不知道你运行的是windows还是mac os呢。
3.3.mmu其实并不属于操作系统的范畴,然后操作系统却需要管理各个页表,而页表却是mmu操作的对象,由于mmu的工作完全在运行于ring0的vmm中完成,因此cr3的物理地址也是由vmm指定的,由于vmm可以看到所有的物理内存,因此它能完成这个工作。
3.4.guest os上的当前进程的页表由当前vmm中的cr3-也就是整个系统中的cr3指定,又由于guest os已经陷入了内核态,内核态的页表又都相同,因此vmm当然能找到guest os的当前调用fork的进程在创建子进程页表时需要访问的虚拟地址所对应的物理页面,这个页面肯定属于.vmem文件所映射进内存的页面,然而它只是映射进了内存,不一定被分配了物理页面啊
3.4.1.如果没有被分配物理页面,也即是页表本身缺页,那么缺页中断将被触发,那么分配一个页面,映射进该地址,也就是为guest os补全了一个页面的物理地址,毕竟虚拟内存需要的页面最终是需要在物理内存中分配的。
3.4.1.1.在guest os的alloc_page中操作的都是虚拟地址,然而内核却需要管理整个物理内存,这个物理内存本身的管理需要的也是虚拟地址,这个只要在vmm管理的影子页表中有映射即可。
3.4.1.2.比如guest os内核中的前三个物理页面的起始物理地址肯定为0,4096,8192,起始虚拟地址则是(3G+page数组+0),(3G+page数组+4096),(3G+page数组+8192),如果运行在host os中这三个地址的虚拟地址就是这三个数,而对应的物理地址则是前三个数,然而在guest os中,前三个页面的虚拟地址对应的仍然是那三个数,只是物理地址对应的则不一定是那前三个数了,而可能会是任意的数,但是这个有关系吗?没有关系,因为我们运行的是现代操作系统。
3.4.1.3.guest os在初始化的时候,已经将自己内核的虚拟地址按照操作系统特定的方式全部映射到.vmem文件中了,比如对于linux就是一一线性映射,同时vmm也构建好了影子页表,内存中有一个.vmem文件大小的连续虚拟地址空间,然而可能还没有被分配物理页面,vmm需要做的就是当guest os需要访问这个虚拟的.vmem文件映射空间的时候为其提交物理页面,并且用这个物理页面的物理地址来构造影子页表而不是用guest os在.vmem文件中的它看到的物理页面的地址来构造页表。
3.4.1.4.再次重申,现代操作系统看到的都是虚拟地址。仅在它管理mmu的时候需要页面的物理地址。
3.4.2.如果已经有了物理页面,则guest os继续。
4.5.如是,子进程的页表被构建完毕。
4.6.guest os的进程切换,完全按照guest os的方式进行,最后切换cr3,然而就在这时,事情发生了
4.6.1.由于切换cr3的时候需要的参数是页目录的物理地址,但是guest os看不到物理地址,mmu是在host os中的vmm中被管理的,这怎么办?
4.6.2.vmware使用了BT技术,也就是二进制翻译技术,在vmm打算让一个guest os运行前,将要运行的guest os的二进制代码就被重写了,此时它会产生一个fault,然后vmm截取到以后,帮助guest os切换cr3,因为vmm知道guest os所谓的cr3的物理地址(其实在host上它就是一个文件映射进连续虚拟内存空间的一个虚拟地址)在哪里。
4.7.子进程运行。
以上就是vmware虚拟化实现中最复杂的技术之一,内存的虚拟化,特别针对现代的32位保护模式硬件上的操作系统,它真的很复杂,其中涉及了BT技术,影子页表技术,ring1技术,其中在运行于ring0的vmm中实现mmu,然后提交给ring1的操作系统使用,这体现了设计者对内存访问流程是多么的理解。技术细节上需要说明的是,guest os的“内核页表”保持不变,它实现了第一层的映射,而vmm中的影子页表直接实现了第一层和第二层映射的组合映射,因此它必然需要时刻和真实的guest os页表进行同步操作,另外还有,由于这些guest os的物理内存实际上是vmm管理的.vmem文件映射的内存,因此这些“物理内存”是可以回写到磁盘.vmem文件的。
理解了内存的虚拟化,io虚拟化就简单了,它被vmm捕获之后,就是模拟执行,在vmnet中,vmware-vmx在用户态模拟了虚拟机里面的网卡,使用的就是这个原理。甚至系统调用的过程也简单了,不过这涉及到x86的架构,和这里讨论的无关。
虚拟一套x86机器的硬件系统,包括所有寄存器,中断等,然而中断几乎都是由host os来处理的,原因是这样的,因为guest os运行在ring1,而你的vmm运行在ring0,因此只要有硬件中断,cpu在执行guest os的下一条指令之前一看有if标志,因此它陷入ring0,也就是vmm,此时vmm切换回host os,由于重新载入了所有的idt等寄存器,所有host os完全可以处理这个中断,比如鼠标中断,vmware在用户态的vmware-vmx进程取到这个鼠标中断之后,发现当前的鼠标在虚拟机中,那么就会把这个事件发给vmm,然后vmm模拟一个中断交给guest os,切换到guest os由它的中断处理程序来处理之。
也就是说,必然需要一个ring0的东西在运行,否则很多指令都没有办法执行的,将guest os放到ring1,这也就必然导致中断,io指令等都要由vmm来接管,vmm对于io来说,交给用户态的vmx,而对于中断则直接切回host os由之处理,对于guest os的用户态的int之类的指令,依然陷入vmm,然后vmm再转给ring1的guest os内核。

你可能感兴趣的:(vmware)