阿男的小窝

View the Project on GitHub

虚拟机的各种形态

(本文由阿男和小李同学的聊天记录整理而成,感谢小李同学进行编辑与整理)

Docker这样的容器和VMWareVirtualBox这样的虚拟机之间有什么区别?

网上的介绍一般都是:Docker是个container,而VMWare这些是hypervisor。

那么containerhypervisor的区别又在哪里呢?

下面是一张错误的图1

上面的图中,左边是对的:hypervisor是在host os上模拟一层硬件,类似于一台虚拟的电脑,然后在hypervisor之上,可以安装各种操作系统。

因为操作系统是认为自己运行在真实的一个硬件平台之上,所以在这个平台上可以安装各种操作系统。

所以说,hypervisor模拟的是硬件本身。

除了virtualboxvmware这些一站式的,商业化的虚拟机产品,现在还有基于操作系统,开源的一些hypervisor,比如kvmqemu这样的虚拟化工具2

可以看到想这样的hypervisor,本身和操作系统的内核绑定的更紧密。这样的hypervisor往往不像vmware那样大而全,而是针对特定操作系统。比如kvm就是基于linux操作系统的。然后在kvm+qemu之上可以安装windows或linux操作系统。

hypervisor所支持的平台,以及在它之上所能安装的操作系统,取决于hypervisor产品自身是如何实现的。要支持的操作系统越多,要模拟的硬件环境越全面,产品本身肯定就更复杂。

接下来说说对container的错误理解:

上面这个图的错误在于,container并不是在operation system和上层的各个apps之间的。正确的图是这样的:

可以看到,docker只是对主机操作系统的一个资源管理而已,它只负责划分主机操作系统的资源,而不存在一个虚拟的硬件层面(仅在Linux平台上是这样,MacOS和Windows平台的docker下面再讨论)。

上面这句话是什么意思呢?

我们可以登录进一个docker的container,然后查看它的kernel目录:

可以看到,这个linux的container里面,内核是没有的!

也就是说,运行在docker上面的,是一个没有kernel的linux操作系统。那么这个container里面对kernel的syscall都是跑去哪里了呢?

如果你的host os是linux,也就是说,你的docker跑在linux操作系统上,那么这个container的所有kernel的syscall都是直接由docker交给host os完成的。

因此,docker是一个linux对linux的container manager。它的container里运行的是linux系统(没有kernel),然后container里的程序,实际上是跑在真正的host的linux操作系统上面的。而docker本身其实只是管理一下各个container对host os的资源使用:

那么这就带来一些问题:

对于第一个问题,其实linux本身是靠kernel的版本来区分的,而不是linux的各个发行版本。而现在新的kernel版本往往兼容旧版本下编译的代码,所以不会存在container里面程序无法执行的情况。

但是,一些用到特定版本的内核功能的程序,就会失效,比如各种设备的驱动代码。因为docker本身是不存在硬件的虚拟层面的,所以这些驱动的代码是无法运行在docker的container里面的。

对于第二个问题,32bit的linux系统如何用行在64bit的host kernel上。因为linux的64bit kernel本身是支持32bit的程序运行的,所以也没什么问题。但是一些特定的不兼容的代码,实际上是会运行失败的。

因此,docker实现的是对linux的系统资源划分,而不是硬件层面的虚拟化,因此它在最初始也只支持linux的host上运行linux的container。而它对host os的资源划分以及对container的资源管理,是通过linux自身的cgroups这个特性来实现的3

在后续的docker版本当中,加入了对windows和macos的host支持,那么这个是怎么实现的呢?

因为macos和windows的内核不是linux内核了,因此,docker的containers在windows和macos上肯定不是直接运行在host os的kernel上了,这里面有一个虚拟层。要在macos和windows的内核之上,虚拟出linux kernel出来。

docker在这里,针对不同平台使用了不同的技术。在windows下,它使用了hyper-V4这个微软自己开发的虚拟机;在macos下,它使用了hyperkit5这个虚拟工具。

注意这两个工具都是hypervisor,也就是说,它们都提供虚拟的硬件层面。所以docker在macos和windows下和linux下运行的版本有本质不同。docker在linux下只是进行资源的划分,而在windows和macos下是提供一个hypervisor,然后它基于hypervisor再虚拟出linux kernel来。

在用户的使用角度来看,docker在各平台的使用方法,用户接口都是一样的,都是linux kernel。但在实际实现上,在linux平台和在其它平台是本质不同的。因此linux平台上运行docker是效率最高的,因为它没有硬件虚拟层面。

最后聊聊语言级别的虚拟机,比如Java的JVM虚拟机。

这种虚拟机其实也算是模拟一个硬件平台,但是它模拟的不是真实的硬件架构,而是它自己定义的一种主机架构。

比如JVM,这个虚拟主机并不不支持intel的汇编代码,也不模拟intel架构下的硬件平台。它自己定义一套指令集,叫做bytecode,然后虚拟机本身非常简化,自己提供内存管理,代码执行这些功能,不需要考虑去模拟什么真实的硬件。

不管用户使用scalajava还是clojure编写代码,所有运行在JVM虚拟机上的代码,都要被编译成bytecode:

public class Hello {
	public static void main() {
		System.out.println("Hello, world!");
	}
}
$ javac Hello.java
$ javap -c Hello.class
Compiled from "Hello.java"
public class Hello {
  public Hello();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main();
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello, world!
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

这些bytecode代码,就相当于是JVM这个虚拟机的汇编代码。

语言级别虚拟机的好处是,你可以不管你的代码运行在什么操作系统上,只要你在你的操作系统上安装了这个虚拟机,你的代码就可以编译,并运行在这个虚拟机之上。

而虚拟机本身的实现,则和各个平台相关,由虚拟机的实现者去负责在各个平台上去实现。

比如Java,针对不同操作系统,有不同的发行版本。你在MacOS,Windows或Linux上,要下载各个平台对应的Java发型版本。它们的实现是各不相同的,跟操作系统相关,但是它们的虚拟机实现是完全一致的。

现代的编程语言几乎都是这个架构,都有虚拟机这个层面。比如Ruby,从1.9版本以后,Ruby代码就要编译成它的虚拟机YARV的虚拟机代码。下面是例子:

$ irb
irb(main):001:0> code = <<END
irb(main):002:0" puts 2+2
irb(main):003:0" END
=> "puts 2+2\n"
irb(main):004:0> puts RubyVM::InstructionSequence.compile(code).disasm
== disasm: #<ISeq:<compiled>@<compiled>>================================
0000 trace            1                                               (   1)
0002 putself
0003 putobject        2
0005 putobject        2
0007 opt_plus         <callinfo!mid:+, argc:1, ARGS_SIMPLE>, <callcache>
0010 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0013 leave
=> nil

以上是关于各种虚拟机的一些讲解,最后再次感谢小李同学的归纳整理。

参考资料