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, );
return ;
}

实现很简单,就是判断地址是否在该段Io地址区间内,如果在就调用eventfd_signal,给该段IOeventfd绑定的eventfd一个信号,这样在该eventfd上等待的对象就会得到处理。

以马内利!

参考资料:

linux3.10.1源码

KVM源码

qemu源码

virtIO之VHOST工作原理简析的更多相关文章

  1. DHCP工作原理简析

    引言 DHCP是网络体系结构中应用层的一个重要协议,它可以帮助我们对要连接到互联网的计算机进行IP地址等信息的配置.本文从DHCP的原理出发,就DHCP的工作过程 进行详细的探讨. 主要报文 发现报文 ...

  2. tomcat 工作原理简析

    https://github.com/HappyTomas/another-tutorial-about-java-web/blob/master/00-08.md 在00-02.理解HTTP中给出了 ...

  3. Spring 核心组件工作原理简析

    Spring Framework 的核心组件有三个: Spring Core,Spring Context 和 Spring Beans,它们奠定了 Spring 的基础并撑起了 Spring 的框架 ...

  4. Java Android 注解(Annotation) 及几个常用开源项目注解原理简析

    不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...

  5. PHP的错误报错级别设置原理简析

    原理简析 摘录php.ini文件的默认配置(php5.4): ; Common Values: ; E_ALL (Show all errors, warnings and notices inclu ...

  6. Java Annotation 及几个常用开源项目注解原理简析

    PDF 版: Java Annotation.pdf, PPT 版:Java Annotation.pptx, Keynote 版:Java Annotation.key 一.Annotation 示 ...

  7. [转载] Thrift原理简析(JAVA)

    转载自http://shift-alt-ctrl.iteye.com/blog/1987416 Apache Thrift是一个跨语言的服务框架,本质上为RPC,同时具有序列化.发序列化机制:当我们开 ...

  8. Spring系列.@EnableRedisHttpSession原理简析

    在集群系统中,经常会需要将Session进行共享.不然会出现这样一个问题:用户在系统A上登陆以后,假如后续的一些操作被负载均衡到系统B上面,系统B发现本机上没有这个用户的Session,会强制让用户重 ...

  9. SIFT特征原理简析(HELU版)

    SIFT(Scale-Invariant Feature Transform)是一种具有尺度不变性和光照不变性的特征描述子,也同时是一套特征提取的理论,首次由D. G. Lowe于2004年以< ...

随机推荐

  1. 用IFrame作预览pdf,图片

    <iframe id="my_img" src="@ViewBag.path" width="100%" frameborder=&q ...

  2. MetaSploit Pro 下载地址

    Windows: https://downloads.metasploit.com/data/releases/metasploit-latest-windows-installer.exe Linu ...

  3. bootstrap+PHP表单验证

    来源:http://www.sucaihuo.com/php/1814.html demo http://www.sucaihuo.com/jquery/18/1814/demo/

  4. mui自定义事件带参返回mui.back()

    父页面添加自定义监听事件:(e.detail.xxx) window.addEventListener('doit', function(e){ //获取参数值 var imagePath = e.d ...

  5. c++ template<typename T>

    template <typename T> 网上查了半天不知所云,网上说的太多,俺只是要知道所需要的就可以了. 写了个程序试了一下,其实就是这个东西可以根据你所需要的类型就行匹配.其实就是 ...

  6. wamp下修改mysql root用户的登录密码方法

    wamp环境安装之后mysql的root密码为空的,我们希望给它设置一个密码; 1.安装好wamp后,运行WampServer程序,进入MYSQL控制台: 2.进入控制台后,提示输入密码(不用输入任何 ...

  7. mybatis由浅入深day01_1课程安排_2对原生态jdbc程序中问题总结

    mybatis 第一天 mybatis的基础知识 1 课程安排: mybatis和springmvc通过订单商品 案例驱动 第一天:基础知识(重点,内容量多) 对原生态jdbc程序(单独使用jdbc开 ...

  8. Swift - UITableView的用法

    因为倾向于纯代码编码,所以不太喜欢可视化编程,不过也略有研究,所以项目里面的所有界面效果,全部都是纯代码编写! 终于到了重中之重的tableview的学习了,自我学习ios编程以来,工作中用得最多的就 ...

  9. String.Join重载String.Join 方法 (String, String[], Int32, Int32)

    https://msdn.microsoft.com/zh-cn/library/tk0xe5h0 String.Join 方法 (String, String[], Int32, Int32) 官方 ...

  10. centos7 ubuntu14 添加sudo 权限 ,禁用每次sudo 需要输入密码

    安装完centos7后,默认没有启用sudo,首先应该是对sudo进行设置.sudo的作用就是使当前非root用户在使用没有权限的命令 时,直接在命令前加入sudo,在输入自己当前用户的密码就可以完成 ...