VirtIO半虚拟化驱动的使用

virtio 是对半虚拟化 hypervisor 中的一组通用模拟设备的抽象。该设置还允许 hypervisor 导出一组通用的模拟设备,并通过一个通用的应用编程接口(API)让它们变得可用。下图展示了为什么这很重要。有了半虚拟化 hypervisor 之后,来宾操作系统能够实现一组通用的接口,在一组后端驱动程序之后采用特定的设备模拟。后端驱动程序不需要是通用的,因为它们只实现前端所需的行为。除了前端驱动程序(在来宾操作系统中实现)和后端驱动程序(在 hypervisor 中实现)之外,virtio 还定义了两个层来支持来宾操作系统到 hypervisor 的通信。在顶级(称为 virtio)的是虚拟队列接口,它在概念上将前 端驱动程序附加到后端驱动程序。驱动程序可以使用 0 个或多个队列,具体数量取决于需求。例如,virtio 网络驱动程序使用两个虚拟队列(一个用于接收,另一个用于发送),而 virtio 块驱动程序仅使用一个虚拟队列。虚拟队列实际上被实现为跨越来宾操作系统和 hypervisor 的衔接点。但这可以通过任意方式实现,前提是来宾操作系统和 hypervisor 以相同的方式实现它。
KVM是必须使用硬件虚拟化辅助技术(如Intel VT-x、AMD-V)的hypervisor,在CPU运行效率方面有硬件支持,其效率是比较高的;在有Intel EPT特性支持的平台上,内存虚拟化的效率也较高。QEMU/KVM提供了全虚拟化环境,可以让客户机不经过任何修改就能运行在KVM环境中。不过,KVM在I/O虚拟化方面,传统的方式是使用QEMU纯软件的方式来模拟I/O设备(如第4章中提到模拟的网卡、磁盘、显卡等等),其效率并不非常高。在KVM中,可以在客户机中使用半虚拟化驱动(Paravirtualized Drivers,PV Drivers)来提高客户机的性能(特别是I/O性能)。目前,KVM中实现半虚拟化驱动的方式是采用了virtio这个Linux上的设备驱动标准框架。
使用QEMU模拟I/O的情况下,当客户机中的设备驱动程序(device driver)发起I/O操作请求之时,KVM模块中的I/O操作捕获代码会拦截这次I/O请求,然后经过处理后将本次I/O请求的信息存放到I/O共享页,并通知用户控件的QEMU程序。QEMU模拟程序获得I/O操作的具体信息之后,交由硬件模拟代码来模拟出本次的I/O操作,完成之后,将结果放回到I/O共享页,并通知KVM模块中的I/O操作捕获代码。最后,由KVM模块中的捕获代码读取I/O共享页中的操作结果,并把结果返回到客户机中。当然,这个操作过程中客户机作为一个QEMU进程在等待I/O时也可能被阻塞。另外,当客户机通过DMA(Direct Memory Access)访问大块I/O之时,QEMU模拟程序将不会把操作结果放到I/O共享页中,而是通过内存映射的方式将结果直接写到客户机的内存中去,然后通过KVM模块告诉客户机DMA操作已经完成。
QEMU模拟I/O设备的方式,其优点是可以通过软件模拟出各种各样的硬件设备,包括一些不常用的或者很老很经典的设备(如RTL8139的网卡),而且它不用修改客户机操作系统,就可以实现模拟设备在客户机中正常工作。在KVM客户机中使用这种方式,对于解决手上没有足够设备的软件开发及调试有非常大的好处。而它的缺点是,每次I/O操作的路径比较长,有较多的VMEntry、VMExit发生,需要多次上下文切换(context switch),也需要多次数据复制,所以它的性能较差。
Virtio最初由澳大利亚的一个天才级程序员Rusty Russell编写,是一个在hypervisor之上的抽象API接口,让客户机知道自己运行在虚拟化环境中,从而与hypervisor根据 virtio 标准协作,从而在客户机中达到更好的性能(特别是I/O性能)。目前,有不少虚拟机都采用了virtio半虚拟化驱动来提高性能,如KVM和Lguest。
其中前端驱动(frondend,如virtio-blk、virtio-net等)是在客户机中存在的驱动程序模块,而后端处理程序(backend)是在QEMU中实现的。在这前后端驱动之间,还定义了两层来支持客户机与QEMU之间的通信。其中,“virtio”这一层是虚拟队列接口,它在概念上将前端驱动程序附加到后端处理程序。一个前端驱动程序可以使用0个或多个队列,具体数量取决于需求。例如,virtio-net网络驱动程序使用两个虚拟队列(一个用于接收,另一个用于发送),而virtio-blk块驱动程序仅使用一个虚拟队列。虚拟队列实际上被实现为跨越客户机操作系统和hypervisor的衔接点,但它可以通过任意方式实现,前提是客户机操作系统和virtio后端程序都遵循一定的标准,以相互匹配的方式实现它。而virtio-ring实现了环形缓冲区(ring buffer),用于保存前端驱动和后端处理程序执行的信息,并且它可以一次性保存前端驱动的多次I/O请求,并且交由后端去动去批量处理,最后实际调用宿主机中设备驱动实现物理上的I/O操作,这样做就可以根据约定实现批量处理而不是客户机中每次I/O请求都需要处理一次,从而提高客户机与hypervisor信息交换的效率。
Virtio半虚拟化驱动的方式,可以获得很好的I/O性能,其性能几乎可以达到和native(即:非虚拟化环境中的原生系统)差不多的I/O性能。所以,在使用KVM之时,如果宿主机内核和客户机都支持virtio的情况下,一般推荐使用virtio达到更好的性能。当然,virtio的也是有缺点的,它必须要客户机安装特定的Virtio驱动使其知道是运行在虚拟化环境中,且按照Virtio的规定格式进行数据传输,不过客户机中可能有一些老的Linux系统不支持virtio和主流的Windows系统需要安装特定的驱动才支持Virtio。不过,较新的一些Linux发行版(如RHEL 6.3、Fedora 17等)默认都将virtio相关驱动编译为模块,可直接作为客户机使用virtio,而且对于主流Windows系统都有对应的virtio驱动程序可供下载使用。
首先,搭建kvm+qemu基础环境。检查CPU是否支持虚拟化技术,命令如下:

cat /proc/cpuinfo | grep vmx

接下来安装kvm以及qemu组件等:

sudo apt-get install kvm qemu libvirt-bin virtinst virt-manager virt-viewer

效果如图所示:
virtio1
实验使用的系统为Ubuntu Linux 16.04.1 LTS系统,内核版本4.4.0支持virtio。
首先使用qemu-img命令创建镜像文件:

sudo qemu-img create -f qcow2 rhel-6.4.img 20G

此命令创建了文件名为rhel-6.4.img的qcow2格式的20GB大小的镜像文件。
接下来使用rhel-server-6.4-x86_64-dvd.iso文件安装操作系统。
若要执行系统安装步骤,需要使用qemu-system-x86_64指令启动虚拟机执行系统安装。命令如下:

sudo qemu-system-x86_64 -enable-kvm -m 1024 -smp 4 -boot order=cd -hda rhel-6.4.img -cdrom ~/rhel-server-6.4-x86_64-dvd.iso

效果如图所示:
由于系统安装并非重点,故此处略去安装过程截图。
virtio2
在上面的命令中,-enable-kvm代表使用Linux内核内置的KVM硬件加速虚拟化技术,而非使用QEMU效率低的纯软件虚拟化。-m 1024代表分配1GB内存给虚拟机。-smp 4代表分配4个vCPU给虚拟机。-boot order=cd代表变更虚拟机启动顺序为光驱优先。-hda rhel-6.4.img代表虚拟机使用的磁盘镜像文件。-cdrom代表虚拟机需要使用光盘驱动器,此选项可直接指定物理光驱也可指定ISO文件,此处选择ISO镜像。
安装系统完成后,变更启动命令行为:

sudo qemu-system-x86_64 -enable-kvm -m 1024 -smp 4 -hda rhel-6.4.img -balloon virtio -vga virtio -net nic,model=virtio -device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x05

![](https://www.fa1c0n.cn/img/posts/virtio/3.png)  

启动系统后效果如图所示。进入虚拟机后,输入命令以查看virtio分配设备:

sudo lsmod | grep virtio

效果如图所示:

从图片中以及命令行中可以看到,virtio_balloon,virtio_net,virtio_pci,virtio_ring,virtio均已开启。实现了通过QEMU命令行参数启动带有Virtio网卡驱动、Virtio磁盘驱动、Virtio Balloon设备驱动的虚拟机。
在上面执行的命令中,由于命令行中加入了-net nic,model=virtio命令行配置网络,则必须配置网络启动脚本/etc/qemu_ifup。在虚拟机启动时创建和打开指定的tap接口,并将接口添加到虚拟网桥中。qemu_ifup的配置代码见主要算法和程序清单部分。
至此Linux下的VirtIO半虚拟化驱动的使用实验完成。
对于Windows操作系统非开源,微软没有开发相应的virtio驱动,所以需要安装virtio驱动程序才能支持virtio。virtio驱动下载地址:https://launchpad.net/kvm-guest-drivers-windows/+download
此次实验选用Windows XP系统为实验对象。首先制作WinXP的镜像文件,命令如下:

sudo qemu-img create -f qcow2 winxp.img 50G

接下来启动镜像文件开始安装操作系统,安装过程截图不是重点同样略过。
安装系统启动虚拟机使用命令:

sudo qemu-system-x86_64 -smp 4 -m 1024 -cdrom zh-hans_windows_xp_professional_with_service_pack_3_x86_cd_vl_x14-74070.iso -drive file=winxp.img,cache=writeback,if=virtio,boot=on -boot order=cd -fda virtio-win-0.1.96_x86.vfd -usbdevice tablet -device virtio-balloon-pci,id=balloon0 -enable-kvm -net nic,model=virtio

![](https://www.fa1c0n.cn/img/posts/virtio/5.png)  

安装过程中,直接加载硬盘驱动器的VirtIO驱动(Red Hat VirtIO Block Disk Device WinXP/32bit)安装后的操作系统直接支持VirtIO磁盘驱动。

如图所示,在安装过程中,会提示安装Red Hat VirtIO SCSI Controller驱动程序,代表此虚拟机已加载virtio驱动程序。
系统安装完毕后,加载virtio-win-0.1.96.iso文件到光驱安装PCI设备驱动(VirtIO Balloon Driver)和网卡驱动(Red Hat VirtIO Ethernet Adapter),如图所示:

安装完成所有驱动后,打开任务管理器,效果如图所示:

如上图所示,任务管理器中成功安装了SCSI驱动、网卡驱动、Balloon驱动。实现了在Windows上通过QEMU命令行参数启动带有Virtio网卡驱动、Virtio磁盘驱动、Virtio Balloon设备驱动的虚拟机。

注意:使用qemu-system-x86_64命令启动虚拟机时肯能会出现以下错误:
qemu-system-x86_64: drive with bus=0, unit=0 (index=0) exists
解决方案如下:
1)大部分的原因是参数不对,可能少了“-”符号之类的;
2)参数是正确的,但是还是会出现这种情况,可以把参数重新用手动输入的方式敲,不要直接复制,因为复制过程中可能复制了一些我们没注意到的字符导致程序不识别;
3)可以切换至root权限下操作。

网络启动脚本/etc/qemu_ifup源码

#! /bin/sh
# Script to bring a network (tap) device for qemu up.
# The idea is to add the tap device to the same bridge
# as we have default routing to.

# in order to be able to find brctl
PATH=$PATH:/sbin:/usr/sbin
ip=$(which ip)

if [ -n "$ip" ]; then
    ip link set "$1" up
else
brctl=$(which brctl)
if [ ! "$ip" -o ! "$brctl" ]; then
    echo "W: $0: not doing any bridge processing: neither ip nor brctl utility not found" >&2
    exit 0
fi
ifconfig "$1" 0.0.0.0 up
fi

switch=$(ip route ls | \
awk '/^default / {
      for(i=0;i<NF;i++) { if ($i == "dev") { print $(i+1); next; } }
     }'
    )

# only add the interface to default-route bridge if we
# have such interface (with default route) and if that
# interface is actually a bridge.
# It is possible to have several default routes too
for br in $switch; do
    if [ -d /sys/class/net/$br/bridge/. ]; then
        if [ -n "$ip" ]; then
        ip link set "$1" master "$br"
        else
         brctl addif $br "$1"
        fi
        exit    # exit with status of the previous command
    fi
done