virtIO之VHOST工作原理简析

时间:2021-08-18 23:25:08

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源码