virtIO之VHOST工作原理简析
2017-07-19
一、前言
之前有分析过虚拟化环境下virtIO的实现,virtIO相关于传统的虚拟IO在性能方面的确提高了不少,但是按照virtIO虚拟网卡为例,每次虚拟机接收数据包的时候,数据包从linux bridge经过tap设备发送到用户空间,这是一层数据的复制并且伴有内核到用户层的切换,而在用户空间virtIO后端驱动把数据写入到虚拟机内存后还需要退到KVM中,从KVM进入虚拟机,又增加了一次模式的切换。在IO比较频繁的情况下,会造成模式切换次数过多从而降低性能。而vhost便解决了这个问题。把后端驱动从qemu中迁移到内核中,作为一个独立的内核模块存在,这样在数据到来的时候,该模块直接监听tap设备,在内核中直接把数据写入到虚拟机内存中,然后通知虚拟机即可,这样就和qemu解耦和,减少了模式切换次数和数据复制次数,提高了性能。下面介绍下vhost的初始化流程。
二、 整体框架
介绍VHOST主要从三个部分入手:vHOST内核模块,qemu部分、KVM部分。而qemu部分主要是virtIO部分。本节不打算分析具体的工作代码,因为基本原理和VIRTIO类似,且要线性的描述vhost也并非易事。
1、vHOST 内核模块
vhost内核模块主要是把virtiO后端驱动的数据平面迁移到了内核中,而控制平面还在qemu中,所以就需要一些列的注册把相关信息记录在内核中,如虚拟机内存布局,设备关联的eventfd等。虽然KVM中有虚拟机的内存布局,但是由于vhost并非在KVM中,而是单独的一个内核模块,所以需要qemu单独处理。且目前vhost只支持网络部分,块设备等其他部分尚不支持。内核中两个文件比较重要:vhost.c和vhost-net.c。其中前者实现的是脱离具体功能的vhost核心实现,后者实现网络方面的功能。内核模块加载主要是初始化vhost-net,起始于vhost_net_init(vhost/net.c)
static const struct file_operations vhost_net_fops = { .owner = THIS_MODULE, .release = vhost_net_release, .unlocked_ioctl = vhost_net_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = vhost_net_compat_ioctl, #endif .open = vhost_net_open, .llseek = noop_llseek, };
函数表中vhost_net_open和vhost_net_ioctl两个函数需要注意,简单来讲,前者初始化,后者控制,当然是qemu通过ioctl进行控制。那么初始化主要是初始化啥呢?
主要有vhost_net(抽象代表vhost net部分)、vhost_dev(抽象的vhost设备),vhost_virtqueue。基本初始化的流程我们就不介绍,感兴趣可以参考代码,一个VM即一个qemu进程只有一个vhost-net和一个vhost-dev,而一个vhost-dev可以关联多个vhost_virtqueue。一般而言vhost_virtqueue作为一个结构嵌入到具体实现的驱动中,就网络而言vhost_virtqueue嵌入到了vhost_net_virtqueue。初始化最重要的任务就是初始化vhost_poll。在vhost_net_open的尾部,有如下两行代码
vhost_poll_init(n->poll + VHOST_NET_VQ_TX, handle_tx_net, POLLOUT, dev);
vhost_poll_init(n->poll + VHOST_NET_VQ_RX, handle_rx_net, POLLIN, dev);
在分析函数代码之前,先看下vhost_poll结构
struct vhost_poll {
poll_table table;
wait_queue_head_t *wqh;
wait_queue_t wait;
struct vhost_work work;
unsigned long mask;
struct vhost_dev *dev;
};
结合上篇poll机制的文章,这些字段就不难理解,table是包含一个函数指针,在驱动的poll函数中被调用,主要用于把当前进程加入到等待队列。wqh是一个等待队列头。wait是一个等待实体,其包含一个函数作为唤醒函数,vhost_work是poll机制处理的核心任务,参考上面就是处理网络数据包,其中有函数指针指向用户设置的处理函数,这里就是handle_tx_net和handle_rx_net,mask指定什么情况下进行处理,主要是POLL_IN和POLL_OUT,dev就指向依附的vhost-dev设备。结合这些介绍分析vhost_poll_init就无压力了。
看下vhost_poll_init函数的代码
void vhost_poll_init(struct vhost_poll *poll, vhost_work_fn_t fn, unsigned long mask, struct vhost_dev *dev) { init_waitqueue_func_entry(&poll->wait, vhost_poll_wakeup); init_poll_funcptr(&poll->table, vhost_poll_func); poll->mask = mask; poll->dev = dev; poll->wqh = NULL; /*设置处理函数*/ vhost_work_init(&poll->work, fn); }
代码来看很简单,意义需要解释下,每个vhost_net_virtqueue都有自己的vhost_poll,该poll是监控数据的核心机制,而现阶段仅仅是初始化。vhost_poll_wakeup是自定义的等待队列唤醒函数,在对某个描述符poll的时候会把vhost_poll加入到对应描述符的等待队列中,而该函数就是描述符有信号时的唤醒函数,唤醒函数中会验证当前信号是否满足vhost_poll对应的请求掩码,如果满足调用vhost_poll_queue->vhost_work_queue,该函数如下
void vhost_work_queue(struct vhost_dev *dev, struct vhost_work *work) { unsigned long flags; spin_lock_irqsave(&dev->work_lock, flags); if (list_empty(&work->node)) { /*把vhost_work加入到设备的工作链表,该链表会在后台线程中遍历处理*/ list_add_tail(&work->node, &dev->work_list); work->queue_seq++; /*唤醒工作线程*/ wake_up_process(dev->worker); } spin_unlock_irqrestore(&dev->work_lock, flags); }
该函数会把vhost_work加入到设备的工作队列,然后唤醒vhost后台线程vhost_worker,vhost_worker会遍历设备的工作队列,调用work->fn即之前我们注册的处理函数handle_tx_net和handle_rx_net,这样数据包就得到了处理。
vhost_net_ioctl控制信息
vhost控制接口通过一系列的API指定相应的操作,下面列举一部分
VHOST_GET_FEATURES
VHOST_SET_FEATURES
这两个用于获取设置vhost一些特性
VHOST_SET_OWNER //设置vhost后台线程,主要是创建一个线程绑定到vhost_dev,而线程的处理函数就是vhost_worker
VHOST_RESET_OWNER //重置OWNER
VHOST_SET_MEM_TABLE //设置guest内存布局信息
VHOST_NET_SET_BACKEND //
VHOST_SET_VRING_KICK //设置guest notify guest->host
VHOST_SET_VRING_CALL //设置host notify host->guest
2、qemu部分
前面介绍的都是内核的任务,而内核是为用户提供服务的,除了vhost内核模块加载时候主动执行一些初始化函数,后续的都是由qemu中发起请求,内核才去响应。这也正是qemu维持控制平面的表现之一。qemu中相关代码的介绍不介绍太多,只给出相关主线,感兴趣可以自行参考。这里我们主要通过qemu讨论下host和guest的通知机制,即irqfd和IOeventfd的初始化。先介绍下irqfd和IOeventfd的任务。
irqfd是KVM为host通知guest提供的中断注入机制,vhost使用此机制通知客户机某些任务已经完成,需要客户机着手处理。而IOevnetfd是guest通知host的方式,qemu会注册一段IO地址区间,PIO或者MMIO,这段地址区间的读写操作都会发生VM-exit,继而在KVM中处理。详细内容下面介绍
irqfd的初始化流程如下:
virtio_net_class_init
virtio_net_device_init virtio-net.c
virtio_init virtio.c
virtio_vmstate_change
virtio_set_status
virtio_net_set_status
virtio_net_vhost_status
vhost_net_start
virtio_pci_set_guest_notifiers //为guest_notify设置eventfd
kvm_virtio_pci_vector_use
kvm_virtio_pci_irqfd_use
kvm_irqchip_add_irqfd_notifier
kvm_irqchip_assign_irqfd
kvm_vm_ioctl(s, KVM_IRQFD, &irqfd); //向kvm发起ioctl请求
IOeventfd工作流程如下:
virtio_ioport_write
virtio_pci_start_ioeventfd
virtio_pci_set_host_notifier_internal //
memory_region_add_eventfd
memory_region_transaction_commit
address_space_update_topology
address_space_update_ioeventfds
address_space_add_del_ioeventfds
eventfd_add=kvm_mem_ioeventfd_add kvm_all.c
kvm_set_ioeventfd_mmio
kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &iofd);
3、KVM部分
KVM部分实现对上面ioctl的响应,在kvm_main.c的kvm_vm_ioctl里面,先看KVM_IRQFD的处理
kvm_irqfd->kvm_irqfd_assign,kvm_irqfd_assign函数比较长,我们主要介绍下核心功能
函数在内核生成一个_irqfd结构,首先介绍下_irqfd的工作机制
struct _irqfd { /* Used for MSI fast-path */ struct kvm *kvm; wait_queue_t wait; /* Update side is protected by irqfds.lock */ struct kvm_kernel_irq_routing_entry __rcu *irq_entry; /* Used for level IRQ fast-path */ int gsi; struct work_struct inject; /* The resampler used by this irqfd (resampler-only) */ struct _irqfd_resampler *resampler; /* Eventfd notified on resample (resampler-only) */ struct eventfd_ctx *resamplefd; /* Entry in list of irqfds for a resampler (resampler-only) */ struct list_head resampler_link; /* Used for setup/shutdown */ struct eventfd_ctx *eventfd; struct list_head list; poll_table pt; struct work_struct shutdown; };
kvm是关联的虚拟机,wait是一个等待队列对象,允许irqfd等待某个信号,irq_entry是中断路由表,属于中断虚拟化部分,本节不作介绍。gsi是全局的中断号,很重要。inject是一个工作对象,resampler是确认中断处理的,不做考虑。eventfd是其关联的evnetfd,这里就是guestnotifier.在kvm_irqfd_assign函数中,给上面inject和shutdown都关联了函数
INIT_WORK(&irqfd->inject, irqfd_inject);
INIT_WORK(&irqfd->shutdown, irqfd_shutdown);
这些函数实现了irqfd的简单功能,前者实现了中断的注入,后者禁用irqfd。irqfd初始化好后,对于irqfd关联用户空间传递的eventfd,之后忽略中间的resampler之类的处理,初始化了irqfd等待队列的唤醒函数irqfd_wakeup和核心poll函数irqfd_ptable_queue_proc,接着就调用irqfd_update更新中断路由项目,中断虚拟化的代码单独开一篇文章讲解,下面就该调用具体的poll函数了,这里是file->f_op->poll(file, &irqfd->pt);,实际对应的就是eventfd_poll函数,里面会调用poll_table->_qproc,即irqfd_ptable_queue_proc把irqfd加入到描述符的等待队列中,可以看到这里吧前面关联的eventfd加入到了poll列表,当该eventfd有状态时,唤醒函数irqfd_wakeup就得到调用,其中通过工作队列调度irqfd->inject,这样irqfd_inject得到执行,中断就被注入,具体可以参考vhost_add_used_and_signal_n函数,在从guest接收数据完毕就会调用该函数通知guest。
IOEVENTFD
内核里面起始于kvm_ioeventfd->kvm_assign_ioeventfd,这里相对于上面就比较简单了,主要是注册一个IO设备,绑定一段IO地址区间,给设备分配操作函数表,其实就两个函数
static const struct kvm_io_device_ops ioeventfd_ops = { .write = ioeventfd_write, .destructor = ioeventfd_destructor, };
而当guest内部完成某个操作,如填充好了skbuffer后,就需要通知host,此时在guest内部最终就归结于对设备的写操作,写操作会造成VM-exit继而陷入到VMM中进行处理,PIO直接走的IO陷入,而MMIO需要走EPT violation的处理流程,最终就调用到设备的写函数,这里就是ioeventfd_write,看下该函数的实现
static int ioeventfd_write(struct kvm_io_device *this, gpa_t addr, int len, const void *val) { struct _ioeventfd *p = to_ioeventfd(this); if (!ioeventfd_in_range(p, addr, len, val)) return -EOPNOTSUPP; eventfd_signal(p->eventfd, 1); return 0; }
实现很简单,就是判断地址是否在该段Io地址区间内,如果在就调用eventfd_signal,给该段IOeventfd绑定的eventfd一个信号,这样在该eventfd上等待的对象就会得到处理。
以马内利!
参考资料:
linux3.10.1源码
KVM源码
qemu源码