如何使用xhyve

介绍

FreeBSD 下的虚拟技术 bhyve (The BSD Hypervisor) 是去年1月份正式发布的,包含在了 FreeBSD 10.0 发行版中. xhyve 是基于 bhyve 的 Mac OS X 移植版本. 也就是说我们想在 Mac 上运行 Linux 的话除了 VirtualBox, VMware Fusion 外,现在有了第三种选择.

安装xhyve

安装要求

  • OS X 10.10.3 Yosemite or later
  • a 2010 or later Mac (i.e. a CPU that supports EPT)

安装

If you have homebrew, then simply:

brew update
brew install --HEAD xhyve

The –HEAD in the brew command ensures that you always get the latest changes, even if the homebrew database is not yet updated. If for any reason you don’t want that simply do brew install xhyve .

if not then:
Building

git clone https://github.com/mist64/xhyve
cd xhyve
make

The resulting binary will be in build/xhyve

打印usage

xhyve -h

使用xhyve安装ubuntu

新建一个 ubuntu 目录用来存放所有和 ubuntu 虚拟机相关的东西,下载 ubuntu-14.04.2-server-amd64.iso,并把 iso 里面的两个系统启动需要的文件 vmlinuz 和 initrd.gz 拷贝出来:

mkdir ~/VMs/ubuntu16.04 -pv && mkdir ~/VMs/ISO -pv

cd ~/VMs/ISO && \
curl -O http://releases.ubuntu.com/16.04.3/ubuntu-16.04.3-server-amd64.iso

因为现在的 centos 镜像是hybrid file system的(可以直接dd 到u盘烧录的),而 OS X 的hdiutil 不支持直接挂载,所以我们需要一点小小的magic

dd if=/dev/zero bs=2k count=1 of=/tmp/ubuntu.iso && \
dd if=ubuntu-16.04.3-server-amd64.iso bs=2k skip=1 >> /tmp/ubuntu.iso

hdiutil attach /tmp/ubuntu.iso && \
cp /Volumes/Ubuntu-Server\ 16/install/vmlinuz . && \
cp /Volumes/Ubuntu-Server\ 16/install/initrd.gz .

创建一个 10GB 大小的硬盘文件当作 ubuntu 虚拟机的硬盘:

dd if=/dev/zero of=ubuntu16.04_root.vhd bs=1g count=10

创建虚拟机安装脚本

#!/bin/sh

KERNEL="${HOME}/VMs/ubuntu16.04/vmlinuz"
INITRD="${HOME}/VMs/ubuntu16.04/initrd.gz"
CMDLINE="earlyprintk=serial console=ttyS0 acpi=off"

MEM="-m 1G"
#SMP="-c 2"
NET="-s 2:0,virtio-net"
IMG_CD="-s 3,ahci-cd,${HOME}/VMs/ISO/ubuntu-16.04.3-server-amd64.iso"
IMG_HDD="-s 4,virtio-blk,${HOME}/VMs/ubuntu16.04/ubuntu16.04_root.vhd"
PCI_DEV="-s 0:0,hostbridge -s 31,lpc"
LPC_DEV="-l com1,stdio"

xhyve $MEM $SMP $PCI_DEV $LPC_DEV $NET $IMG_CD $IMG_HDD -f kexec,$KERNEL,$INITRD,"$CMDLINE"

启动这个文件需要 sudo 权限(添加桥接网卡需要root权限)

sudo sh ubuntu16.04_install.sh

这时候会看到 ubuntu 的标准文本格式的安装程序,安装过程中唯一要注意的是硬盘分区不要选择 LVM 分区.

本例中分区: /dev/vda1 /boot ; /dev/vda2 /

还有一个要注意的地方,安装完毕后,这时候选择 Go Back,因为我们要到 Execute a shell 命令行界面把里面的内核文件拷贝出来留作以后启动用

选择 Execute a shell 后转到目标目录,知道虚拟机的 IP 地址后用 nc 把虚拟机和外面的世界(Mac)连起来传输文件:

cd /target/
tar c boot | nc -l -p 9000

# if centos
dhclient eth0
cd /mnt/sysimage/boot/
python -m SimplyHTTPServer

在 Mac 上接受文件:

cd ${HOME}/VMs/ubuntu16.04
nc 192.168.64.3 9000 | tar x    #192.168.64.3为虚拟机IP

# if centos
curl -O http://192.168.64.2:8000/vmlinuz-3.10.0-229.el7.x86_64
curl -O http://192.168.64.2:8000/initramfs-3.10.0-229.el7.x86_64.img

虚拟机启动脚本

#!/bin/sh

KERNEL="${HOME}/VMs/ubuntu16.04/boot/vmlinuz-4.4.0-87-generic"
INITRD="${HOME}/VMs/ubuntu16.04/boot/initrd.img-4.4.0-87-generic"
#/dev/vdaX 和 /(根分区) 分区一致
CMDLINE="earlyprintk=serial console=ttyS0 acpi=offi root=/dev/vda2 ro"

MEM="-m 1G"
SMP="-c 2"
NET="-s 2:0,virtio-net"
#IMG_CD="-s 3,ahci-cd,${HOME}/VMs/ISO/ubuntu-16.04.3-server-amd64.iso"
IMG_HDD="-s 4,virtio-blk,${HOME}/VMs/ubuntu16.04/ubuntu16.04_root.vhd"
PCI_DEV="-s 0:0,hostbridge -s 31,lpc"
LPC_DEV="-l com1,stdio"

xhyve $MEM $SMP $PCI_DEV $LPC_DEV $NET $IMG_CD $IMG_HDD -f kexec,$KERNEL,$INITRD,"$CMDLINE"

参考

项目地址

虚拟机安装参考

附录

homebrew 使用

安装

cd ~

mkdir extapp && curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C extapp

now enjoy it

安装其他软件包

cd extapp

bin/brew install tmux

vi ~/.tmux.conf
set-option -g mouse on
通过proxy升级
ALL_PROXY=socks5://127.0.0.1:60000 bin/brew update
ALL_PROXY=socks5://127.0.0.1:60000 bin/brew upgrade

centos 安装脚本

#!/bin/sh

KERNEL="${HOME}/vms/centos7x64_1503_base/boot/vmlinuz"
INITRD="${HOME}/vms/centos7x64_1503_base/boot/initrd.img"
CMDLINE="earlyprintk=serial console=ttyS0 acpi=off"

MEM="-m 1G"
#SMP="-c 2"
NET="-s 2:0,virtio-net"
IMG_CD="-s 3,ahci-cd,${HOME}/vms/iso/CentOS-7-x86_64-DVD-1503-01.iso"
IMG_HDD="-s 4,virtio-blk,${HOME}/vms/centos7x64_1503_base/vhd/centos7x64_1503_rootpv.vhd"
PCI_DEV="-s 0:0,hostbridge -s 31,lpc"
LPC_DEV="-l com1,stdio"

${HOME}/extapp/bin/xhyve $MEM $SMP $PCI_DEV $LPC_DEV $NET $IMG_CD $IMG_HDD -f kexec,$KERNEL,$INITRD,"$CMDLINE"

centos 启动脚本

#!/bin/sh

KERNEL="${HOME}/vms/centos7x64_1503_base/boot/vmlinuz-3.10.0-229.el7.x86_64"
INITRD="${HOME}/vms/centos7x64_1503_base/boot/initramfs-3.10.0-229.el7.x86_64.img"
CMDLINE="earlyprintk=serial console=ttyS0 acpi=off root=/dev/vda3 ro"

MEM="-m 1G"
SMP="-c 2"
NET="-s 2:0,virtio-net"
IMG_CD="-s 3,ahci-cd,${HOME}/vms/iso/CentOS-7-x86_64-DVD-1503-01.iso"
IMG_HDD="-s 4,virtio-blk,${HOME}/vms/centos7x64_1503_base/vhd/centos7x64_1503_rootpv.vhd"
PCI_DEV="-s 0:0,hostbridge -s 31,lpc"
LPC_DEV="-l com1,stdio"

#UUID="-U cac5fd90-e6aa-4277-b8bd-26bed71fcb09"

${HOME}/extapp/bin/xhyve $MEM $SMP $PCI_DEV $LPC_DEV $NET $IMG_CD $IMG_HDD $UUID -f kexec,$KERNEL,$INITRD,"$CMDLINE"

C中enum的使用

  为了说明这个手法具体该咋用,咱举一个简单的例子来说事儿.比方说要开发一个网络程序,其中需要统计各种网络协议的数据包数量.

版本1

  假设一开始只需要处理HTTP和FTP两种协议.有些同学不假思索,立即会声明如下两个整数用于统计:

    int nCntHttp = 0;
    int nCntFtp = 0;

  猛一看,似乎没啥问题.但是,如果需求发生变更,又要增加两种协议:SMTP和SSH.然后,该同学会继续扩展上述代码,变为如下:

    int nCntHttp = 0;
    int nCntFtp = 0;
    int nCntSmtp = 0;
    int nCntSsh = 0;

  这时候,问题开始显露出来了.比方说要打印上述4统计值,就得写4个printf;再假如要用断言确保所有统计值大于零,也得写4个assert.这都是挺烦人的事儿.(当然啦,有些同学会把4个变量的打印写在一个printf中,但还是一样烦人)

版本2

  这可咋办捏?某些同学就灵机一动,把上述代码修改为数组形式,上述的4个统计值依次放入数组中.具体如下:

    int nCntProto[4]; /* 第0个是HTTP,第1个是FTP,第2个是SMTP,第3个是SSH */

  这样,无论是打印还是断言,都可以用for循环搞定,貌似挺方便的.但这么一来,引入了另一个问题.假设我在程序中要用到SMTP的统计数字,就得这么写代码:nCntProto[2].这就造成了很不雅观的“Magic Number”!要知道,Magic Number可是代码的臭味之一.万一将来,数组中的存放顺序发生变化,那就完蛋了:好多用到Magic Number的代码都得跟着改.一旦漏改某处,引出Bug无数!

版本3

  为了消除Magic Number,增加代码可读性和可维护性,有些同学开始打起enum的主意.在代码中增加了一组enum,具体如下:

  enum PROTO
  {
    PROTO_HTTP,
    PROTO_FTP,
    PROTO_SMTP,
    PROTO_SSH
  };
  int nCntProto[4];

这样,如果我需要用到SMTP的统计数字,我就不用写nCntProto[2],而是写nCntProto[PROTO_SMTP].这样,可读性明显好多了.即使将来数组中的存放顺序发生变化,也没关系:只需稍微调整enum中常量的顺序即可,其它代码不用动.

版本4

但是,还是有一个不爽的地方.定义数组的语句用到了“4”这个Magic Number.万一将来需求继续变更,继续增加协议,那这个数字还得不断调整.不爽!   这时候,终极版本隆重登场.请看如下代码:

  enum PROTO
  {
    PROTO_HTTP,
    PROTO_FTP,
    PROTO_SMTP,
    PROTO_SSH,

    PROTO_NUM /* 表示协议数量 */
  };
  int nCntProto[PROTO_NUM];

这种写法的好处在于,没有任何一个Magic Number.不管是引用某个统计值还是循环遍历数组,都使用的是定义好的常量.   当需求变更,需要增加新的协议,只要往enum中增加相应的enum常量即可(但要记得保证PROTO_NUM位于enum定义的末尾).由于PROTO_NUM会自动跟着增长,所以其它的代码几乎不会受到影响.

C++的补充说明   上述代码同时适用于C和C++.不过,对于某些C++程序员,或许看不惯原始数组,觉得STL的容器类看起来比较顺眼.那也没啥大关系:只要把上述代码的数组声明修改为如下,其它的代码基本照旧.

   std::vector<int> vctCntProto(PROTO_NUM);