一、前言
KVM可以使用Intel VT-x或者AMD-V虚拟化辅助技术,提高CPU虚拟化的效率;使用支持Intel EPT的平台上,可以提高内存虚拟化的效率;但是对于IO虚拟化,传统的方式是KVM使用qemu来模拟IO设备(网卡、磁盘、显卡等),其效率比较低下。而通过paravirtualization技术,例如virtio,可以进一步提升IO虚拟化的性能。
转载自https://blog.csdn.net/cloudvtech
二、qemu全虚拟化工作模式
在qemu全虚拟化的模式下,当客户机的设备驱动发起IO请求时,KVM会捕获这次IO请求,进行初步处理之后放入将IO请求放入KVM和qemu的共享内存页,然后通知用户空间的qemu进程;用户空间qemu进程会从内核中读取这个IO请求,由硬件模拟模块模拟这个IO操作;qemu的硬件模拟模块会根据IO请求的不同,跟不同的真实物理设备驱动进行交互,完成真正的IO操作(比如通过物理网卡访问外部网络),并将结果放回共享内存页,通知KVM的IO处理模块;KVMIO处理模块读取处理结果并返回给客户机设备驱动。
qemu全虚拟化方式处理IO,每次IO操作都要发送多次VMExit/VMEnrty、上下文切换、数据复制,性能较差。
转载自https://blog.csdn.net/cloudvtech
三、virtio工作模式
virio的基本概念是让客户机感知自己正处于虚拟化环境中,这时候虚拟机可以基于virio标准与宿主机进行协作以提高IO虚拟化的效率。
virtio在部署的时候由前端、后端组成和virt-queue组成。
3.1 前端初始化
前端包括virio为了满足Linux设备模型(总线、设备、驱动)而模拟的virtio pci bus、virtio pci driver和virtio pci device(包括virtio-net、virtio-blk等如上图所示)。当使用qemu启动VM并在命令行传入使用virtio-net类型设备的时候,qemu会对virtio-net设备进行初始化并在qemu虚拟PCI中加入这个virtio设备的初始化信息。在虚拟机内操作系统启动的时候,会枚举到该virtio设备并分配相应资源。在/sys/bus/目录下创建了一个新的目录virtio,在该目录下同时创建了两个文件夹为devices和drivers。表示创建virtio总线,总线支持设备与驱动,在/sys/devices/virtio-pci/创建相应子设备{virtioxxx},同时在/sys/bus/virtio/devices下面创建符号连接文件{virtioxxx}。这样在qemu和虚拟机操作系统内核对virtio设备的支持下,完成了virtio net设备的初始化,其实该过程与真实物理网卡的初始化过程类似。
3.2 virt-queue的使用
Virtio使用virtqueue接口来进行IO,每个virtqueue就是一个存储和传输IO数据的queue,virio中virtqueue由vring来实现。通过对virtqueue的buffer的读写,可以实现IO数据的读写,在读写virtqueue数据完毕后,借助virtqueue_kick()函数可以向queue notify寄存器写入队列index来通知宿主机。每个virio设备可以关联一个或者多个读写队列。
3.3 前端驱动的运作方式
虚拟机中virtio驱动通过iowrite/ioread等操作与virtio设备进行交互,而该io操作会被KVM截获,从而转移至后端进行处理。在QEMU中为支持virtio设备的模拟提供了多个函数注册结构来响应虚拟机的不同读写请求。
在虚拟机将数据包从内核协议栈向下发送的时候,虚拟网卡设备的virtio-net驱动中注册的处理函数会被调用:
static netdev_tx_t start_xmit(struct sk_buff *skb, struct net_device *dev)
{
......
err = xmit_skb(sq, skb);
......
virtqueue_kick(sq->vq);
......
}
xmit_skb主要是向vring写入数据:
static int xmit_skb(struct send_queue *sq, struct sk_buff *skb)
{
struct skb_vnet_hdr *hdr;
hdr_len = sizeof hdr->hdr;
hdr = skb_vnet_hdr(skb);
......
sg_set_buf(sq->sg, hdr, hdr_len);
num_sg = skb_to_sgvec(skb, sq->sg + 1, 0, skb->len) + 1;
return virtqueue_add_outbuf(sq->vq, sq->sg, num_sg, skb, GFP_ATOMIC);
}
在将数据写入之后,还要将需要发送buffer信息填充到vring的desc描述符,包括buffer的长度、buffer占用的page的基址,相对基址的偏移量等。
信息写入完毕之后,可以将IO请求事件通过virtqueue_kick发送到后端:
bool virtqueue_kick(struct virtqueue *vq)
{
if (virtqueue_kick_prepare(vq))
return virtqueue_notify(vq);
return true;
}
bool virtqueue_notify(struct virtqueue *_vq)
{
struct vring_virtqueue *vq = to_vvq(_vq);
if (unlikely(vq->broken))
return false;
/* Prod other side to tell it about changes. */
if (!vq->notify(_vq)) {
vq->broken = true;
return false;
}
return true;
}
static bool vp_notify(struct virtqueue *vq)
{
struct virtio_pci_device *vp_dev = to_vp_device(vq->vdev);
/* we write the queue's selector into the notification register to
* signal the other end */
iowrite16(vq->index, vp_dev->ioaddr + VIRTIO_PCI_QUEUE_NOTIFY);
return true;
}
由于前端已经将数据的位置和长度信息发送给virtio驱动并且发送至后端,而且前端和后端是共享内存的,所以后端可以直接开始处理IO请求,并且不存在数据拷贝。
3.4 后端处理
虚拟机使用vp_notify模拟一次IO操作,之后虚拟机会退出guest模式,host内核会执行vmx_handle_eixt,最后运行到ioeventfd_write的时候,就会产生kick信号,如果该eventfd是由qemu侧来监听的,则会执行对应的qemu函数kvm_handle_io();如果是vhost来监听的,则直接在vhost内核模块执行vhost->handle_kick()。
eventfd是只存在于内存中的文件,通过系统调用sys_eventfd可以创建新的文件,它可以用于线程间、进程间的通信,无论是内核态或用户态。一般来说,称guest侧通知host侧的行为为kick,host侧通知guset侧的行为为call。
根据不同的前端,后端会有不同的机制去handle,比如如果是网络数据包,则由后端处理程序负责发送到宿主机为这个虚拟机建立的tap接口:
转载自https://blog.csdn.net/cloudvtech