虚拟机是个用软件实现的CPU,而CPU的权限控制分为系统级和用户级。
例如,Linux内核就运行在CPU的最高优先级(ring0),而普通应用程序则运行在最低优先级(ring3)。
虽然英特尔把CPU的权限分了4个优先级,但实际只用到了2个。
对于虚拟机来说,要想模拟操作系统的运行,也必须进行权限分级。
1,CPU的权限分级,主要是指内存的访问权限。
intel的CPU分为实模式和保护模式,保护模式最主要的作用就是保护内存的访问权限。
内核代码可以访问所有的内存,但是用户代码只能访问进程的用户空间(内存)。
用户空间的内存是通过进程的页表来管理的,而进程的页表只能通过系统内核来修改。
当使用malloc()分配内存的时候,实际上并不是分配一块物理内存,而只是把用户空间的某一个内存范围设置为可用。
只有当进程代码真去读写这个内存范围的时候,操作系统才会给它分配物理内存,即Linux的写时复制和需求加载机制。
所以虚拟机要想“模拟”操作系统的运行,首先要模拟CPU的保护模式。
2,CPU保护模式的实现,靠的就是几个控制寄存器。
对于intel CPU来说,跟保护模式下相关的寄存器是cr0, cr1, cr2, cr3。
其中cr0用于控制分段和分页机制,一旦开启内存的分段机制就进入了保护模式。
一旦开启了内存的分页机制,操作系统可以支持的进程个数就是无限的了。
开启了分页之后,操作系统就可以4096字节的一个页为单位,为进程分配“必需的”内存空间,非常的灵活。
什么时候必需?
当然是写时复制和需求加载的时候必需,所以进程刚创建时除了它的task_struct结构之外,只需要给它分配4096字节做为页目录即可,其他的都可以跟父进程共享。
对于多进程多任务的操作系统来说,内存的分页机制是必需的,因为分段机制太死板了。
cr3就是页目录基地址寄存器,哪个进程运行时它就指向哪个进程的页表,内核运行时它就指向内核页表。
cr2在缺页中断时用于保存进程用户空间的内存地址。在哪个位置出错了,就保存哪个地址,然后操作系统就会为那个位置(所在的内存页)分配内存。
获取一个位置addr所在的内存页非常的简单,把它的最低12位清零就行,addr & ~0xfff
3,虚拟机要想模拟操作系统的运行,必须自己实现MMU的功能。
操作系统的运行,首先要依赖这几个控制寄存器。
这几个控制寄存器的主要作用,其实就是内存管理。
在真实的硬件上,内存管理是通过MMU实现的。MMU可以根据进程的页表实现用户空间的内存地址(线性地址)到物理内存的映射。
如果在虚拟机上,这部分功能就只能通过代码去实现了。
虚拟机要实现三层内存地址的映射:虚拟进程的用户内存地址 --> 虚拟物理内存的物理地址 --> 虚拟机所在的真实进程的用户内存地址。
所以像qemu这种能够直接运行Linux系统的大型虚拟机,是必须要实现CPU的控制寄存器和系统级指令的。
系统级指令,指的是只能在内核代码(或引导扇区)里运行的指令,例如:
pushfl 把标志寄存器压栈,
mov cr2, eax 把导致缺页的内存地址读到eax寄存器,
mov ax, cs 加载段选择符,等等。
4,脚本语言的虚拟机
脚本语言因为是运行在用户进程中,运行的代码也是用户态代码,所以实现起来比qemu这类虚拟机要简单的多。
它只需要解释一些常用指令就行了,不需要处理系统级的指令,也不需要管理复杂的内存映射。
它只需要把编译之后的字节码文件根据程序头的信息加载起来,并且处理动态库函数的调用(动态链接),就可以实现脚本语言的运行了。
最主要的是,脚本语言的字节码和编译器都是脚本语言的作者设计的,作者可以实现字节码和虚拟机的精确匹配,而不需要去实现CPU的整个指令集。
系统级的虚拟机就不得不实现CPU的整个指令集,因为OS内核被编译之后有可能用到CPU的所有指令,其中任何一条指令没被支持都可能导致内核运行失败。
脚本语言的虚拟机怎么写,之前已经说过了,不再细说了。