文章目录

  • 背景
  • 原理
  • vhost protocol
  • vhost-net
  • 数据结构
  • VhostBackendType
  • ioctl cmd
  • VhostOps


背景

  • virtio协议基于虚机和主机可以共享内存这一基础,规范了虚机与主机之间高效数据传输需要遵守哪些原则,可以拆分为两部分:
  1. control plane:负责virtio设备初始化和终止时控制方面的工作,包括设备特性协商、状态记录、配置信息传输等。
  2. data plane:负责virtio设备运行时数据传输方面的工作,包括前端准备好数据后的通知、数据传输以及后端处理完数据后的通知。
  • virtio设备的控制面和数据面在实现上目标不一样。控制面实现对性能要求不高,因为其工作时间占比virtio设备整个生命周期少,但控制面需要尽量灵活,因为需要适配不同后端实现virtio协议。数据面在功能实现上比较简单,但注重性能,因为virtio设备大部分时间都在传输数据,数据传输速度决定了virtio设备的性能。
  • 如何让数据面的实现更加高效是基于virtio规范实现虚拟化网络始终要研究的课题。
  • 分析传统的virtio网络实现,前端工作在Guest内核态,后端工作在主机用户态。原理如下图所示:
  • 传统virtio网络的实现中,有以下几点会影响性能:
  1. 前端驱动准备好可用buffer后,会通知后端处理。在基于pci的virtio实现中,会写pci配置空间的一个特定地址,敏感指令导致vCPU暂停运行,陷入主机内核态,内核态获取通知后切换到用户态qemu。整个过程会经过两次CPU的上下文切换,开销大。
  2. qemu主线程通过ioeventfd poll内核态的通知,然后对virtio的buffer进行处理,这个过程会获取BQL这把大锁,对于其它需要拿BQL的线程,有同步的开销。
  3. qemu处理在收发网络包时,有系统调用和数据拷贝的开销。
  4. qemu处理完virtio队列上的buffer后,需要注入中断给vCPU,通知虚机,最后再通过系统调用进入内核态,让vCPU重新运行,整个过程也有开销。
  • 通过对传统的virtio网络实现的分析我们知道,因为qemu使用tun/tap设备实现网络包收发,而这个设备的包转发在内核实现,因此只要virtio的后端处理在qemu中实现,都会有上下文切换的开销。同样,前端通知首先到达的也是内核态,如果virtio后端处理在qemu中实现,也会有消息传递、上下文切换等开销。整个包的处理需要经过两次上下文的切换。
  • 如果virtio后端的处理可以在内核态实现,至少可以避免主机用户态与内核态的切换,可以减少一部分上下文切换开销。因此virtio性能的提升变成了如何将virtio后端的处理放到内核态。
  • 再进一步分析,因为qemu始终要负责virtio设备的模拟,基于当前的实现,virtio控制面的后端处理是没法放到内核态(也没有必要),所以我们能够做的就是将virtio数据面的后端处理放到内核态。即将dataplane卸载到内核态。

原理

vhost protocol

  • 我们定义两个角色,将发起卸载请求的称为master,实现卸载请求的称为slave,我们在master和slave之间定义一套通信协议,master将实现dataplane处理的必要信息通过该协议传输给slave,将实现dataplane卸载初始化配置的请求通过该协议发送给slave,从而实现dataplane卸载。这套通信协议我们称为vhost protocol,配置请求接口称为vhost api,如下图所示:
  • 分析virtio的工作原理,slave需要处理virtqueue上的数据,必须获得以下信息:
  1. virtqueue的地址,这是组成vring的三个关键元素的主机虚拟地址,包括描述符表基址、可用环虚拟地址、已用环虚拟地址,slave必须知道这些才能取出virtio队列上的数据。
  2. Guest内存布局,在qemu中由一组RAM MemoryRegionSection构成,实际上就是qemu分配给虚机的整个内存。因为virtqueue中存放的是数据的地址(只是元数据),真正的数据散布在虚机的物理地址空间,因此slave必须获得整个虚机的内存布局才能访问virtqueue上指向的数据。
  3. ioeventfd,前端数据可用,内核通过此描述符通知用户态的qemu virtqueue队列数据可用,slave拿到描述符,可以接收前端数据通知,从而对virtqueue上的数据进行处理。
  4. irqfd,后端数据处理完成后,内核通过此描述符向虚机vCPU注入中断,通知前端数据已处理,slave使用此描述符,可以通知到前端virtqueue上数据已处理。
  • 因此整个vhost api的设计,就围绕master如何将上述信息传递到slave,并控制slave完成初始化配置,使其可以实现dataplane的处理。
  • kernel中实现slave角色的模块,就是本文提到的vhost-net。

vhost-net

pve网卡配置vlan_内核态

  • vhost-net工作原理如上图所示,可以简单描述其流程:
  1. vhost-net 驱动被加载后,会创建一个字符设备/dev/vhost-net,当虚机启动需要使用vhost加速的virtio网卡时,qemu打开该字符设备并做相关初始化配置。主要目的是启动一个vhost-net的实例与该虚机关联,为特性协商、配置信息共享做准备。
  2. virtio网卡初始化阶段,vhost-net会为该虚机启动一个以vhost-$pid命令的内核工作线程(pid是qemu的进程号),专用于处理虚机virtio网卡后端的数据处理。qemu依然会创建tap设备用于网络收发包,但tap设备的读写会交由vhost-$pid工作线程来处理。
  3. 传统的virtio网卡,通过ioeventfd接收来自前端的通知,通过irqfd注入中断来通知vCPU,引入vhost-net之后,qemu会将这两个描述符也传递给vhost-net,当虚机内部对ioeventfd关联的特定地址区间有读写操作时,vhost-net会收到通知并进行相应处理,不需要将qemu唤醒,没有vCPU与qemu主线程同步的开销。当vhost-net需要通知前端virtio数据处理完成时,可以通过irqfd注入中断到vCPU,整个过程没有qemu用户态与内核态切换的开销。

数据结构

VhostBackendType

  • 对于vhost protocol,其slave角色可以在不同模块中实现,即所谓的vhost后端,目前已经实现了三种vhost的后端,如下:
typedef enum VhostBackendType {
    VHOST_BACKEND_TYPE_NONE = 0,
    VHOST_BACKEND_TYPE_KERNEL = 1,		/* 将dataplane卸载到kernel,即vhost-net,vhost-net模块负责virtio后端数据的处理 */
    VHOST_BACKEND_TYPE_USER = 2,		/* 将dataplane卸载到用户态的程序,即vhost-user,通常就是dpdk进程 */
    VHOST_BACKEND_TYPE_VDPA = 3,		/* 将dataplane卸载到vdpa设备,该设备可以实现dataplane的硬件卸载 */
    VHOST_BACKEND_TYPE_MAX = 4,
} VhostBackendType;
  • 注意,对于vhost后端为VHOST_BACKEND_TYPE_USER情况,master和slave之间的通信协议叫做vhost-user protocol。

ioctl cmd

  • 对于kernel作为vhost后端的情况,master和slave之间的通信就是用户态qemu和内核态vhost-net之间的通信,通过字符设备的ioctl可以实现。ioctl cmd可以代表消息类型,不同cmd可以表示不同的消息类型,在vhost-net中,其部分定义如下:
VHOST_GET_FEATURES 			/* 获取slave特性 */
VHOST_SET_FEATURES			/* 设置slave特性 */
VHOST_SET_OWNER				/* 设置fd的owner为当前进程 */
VHOST_RESET_OWNER			/* 重置fd的owner为默认值 */
VHOST_SET_MEM_TABLE			/* 设置guest的物理内存区域 */
VHOST_SET_LOG_BASE			/* TODO */
VHOST_SET_LOG_FD			/* TODO */
/* virtio队列相关设置 */
VHOST_SET_VRING_NUM			/* 设置virtio队列长度 */
VHOST_SET_VRING_ADDR		/* 设置virtio队列地址 */
VHOST_GET_VRING_BASE		/* 获取virtio队列地址 */
VHOST_SET_VRING_KICK 		/* 设置ioeventfd */
VHOST_SET_VRING_CALL		/* 设置irqfd */
VHOST_SET_VRING_ERR			/* 设置eventfd出错通知的fd */
......

VhostOps

  • VhostOps将ioctl cmd及其它配置操作封装成vhost api,软件模块只要实现了vhost api的最小子集,就可以作为vhost后端实现dataplane卸载。vhost api部分定义如下:
typedef struct VhostOps {
    /* 后端类型 */
    VhostBackendType backend_type;
    /* 后端初始化 */
    vhost_backend_init vhost_backend_init;			
	/* 将虚机内存地址传递到vhost-net */
    vhost_set_mem_table_op vhost_set_mem_table;
    /* 将virtqueue地址传递到vhost-net */
    vhost_set_vring_addr_op vhost_set_vring_addr;
    /* 将virtqueue队列长度传递到vhost-net */
    vhost_set_vring_num_op vhost_set_vring_num;
    /* 将virtqueue队列的可用偏移传递到vhost-net */
    vhost_set_vring_base_op vhost_set_vring_base;
    vhost_get_vring_base_op vhost_get_vring_base;
    /* 将ioeventfd的描述符传递到vhost-net */
    vhost_set_vring_kick_op vhost_set_vring_kick;
    /* 将irqfd的描述符传递到vhost-net */
    vhost_set_vring_call_op vhost_set_vring_call;
    vhost_set_features_op vhost_set_features;
    vhost_get_features_op vhost_get_features;
    vhost_set_backend_cap_op vhost_set_backend_cap;
    /* 设置vhost-net的owner为本qemu进程
     * vhost-net实例在工作时会启动一个vhost内核线程
     * 该接口将vhost内核线程与qemu进程关联,两者为多对一的关系
     * */
    vhost_set_owner_op vhost_set_owner;
	......
} VhostOps;
  • 在kernel作为vhost后端的场景下,kernel_ops是qemu定义的与vhost-net交互的vhost api最小集,如下:
const VhostOps kernel_ops = {
        .backend_type = VHOST_BACKEND_TYPE_KERNEL,
        .vhost_backend_init = vhost_kernel_init,
        .vhost_backend_cleanup = vhost_kernel_cleanup,
        .vhost_backend_memslots_limit = vhost_kernel_memslots_limit,
        .vhost_net_set_backend = vhost_kernel_net_set_backend,
        .vhost_scsi_set_endpoint = vhost_kernel_scsi_set_endpoint,
        .vhost_scsi_clear_endpoint = vhost_kernel_scsi_clear_endpoint,
        .vhost_scsi_get_abi_version = vhost_kernel_scsi_get_abi_version,
        .vhost_set_log_base = vhost_kernel_set_log_base,
        .vhost_set_mem_table = vhost_kernel_set_mem_table,
        .vhost_set_vring_addr = vhost_kernel_set_vring_addr,
        .vhost_set_vring_endian = vhost_kernel_set_vring_endian,
        .vhost_set_vring_num = vhost_kernel_set_vring_num,
        .vhost_set_vring_base = vhost_kernel_set_vring_base,
        .vhost_get_vring_base = vhost_kernel_get_vring_base,
        .vhost_set_vring_kick = vhost_kernel_set_vring_kick,
        .vhost_set_vring_call = vhost_kernel_set_vring_call,
        .vhost_set_vring_busyloop_timeout = vhost_kernel_set_vring_busyloop_timeout,       
        .vhost_set_features = vhost_kernel_set_features,
        .vhost_get_features = vhost_kernel_get_features,
        .vhost_set_backend_cap = vhost_kernel_set_backend_cap,
        .vhost_set_owner = vhost_kernel_set_owner,
        .vhost_reset_device = vhost_kernel_reset_device,
        .vhost_get_vq_index = vhost_kernel_get_vq_index,
		......
};