赞
踩
目录
https://yq.aliyun.com/articles/485336
vhost 是 virtio 的一种后端实现方案,在 virtio 简介中,我们已经提到 virtio 是一种半虚拟化的实现方案,需要虚拟机端和主机端都提供驱动才能完成通信,通常,virtio 主机端的驱动是实现在用户空间的 qemu 中,而 vhost 是实现在内核中,是内核的一个模块 vhost-net.ko。为什么要实现在内核中,有什么好处呢,请接着往下看。
在 virtio 的机制中,guest 与 用户空间的 Hypervisor 通信,会造成多次的数据拷贝和 CPU 特权级的上下文切换。例如 guest 发包给外部网络,首先,guest 需要切换到 host kernel,然后 host kernel 会切换到 qemu 来处理 guest 的请求, Hypervisor 通过系统调用将数据包发送到外部网络后,会切换回 host kernel , 最后再切换回 guest。这样漫长的路径无疑会带来性能上的损失。
vhost 正是在这样的背景下提出的一种改善方案,它是位于 host kernel 的一个模块,用于和 guest 直接通信,数据交换直接在 guest 和 host kernel 之间通过 virtqueue 来进行,qemu 不参与通信,但也没有完全退出舞台,它还要负责一些控制层面的事情,比如和 KVM 之间的控制指令的下发等。
下图左半部分是 vhost 负责将数据发往外部网络的过程, 右半部分是 vhost 大概的数据交互流程图。其中,qemu 还是需要负责 virtio 设备的适配模拟,负责用户空间某些管理控制事件的处理,而 vhost 实现较为纯净,以一个独立的模块完成 guest 和 host kernel 的数据交换过程。
vhost 与 virtio 前端的通信主要采用一种事件驱动 eventfd 的机制来实现,guest 通知 vhost 的事件要借助 kvm.ko 模块来完成,vhost 初始化期间,会启动一个工作线程 work 来监听 eventfd,一旦 guest 发出对 vhost 的 kick event,kvm.ko 触发 ioeventfd 通知到 vhost,vhost 通过 virtqueue 的 avail ring 获取数据,并设置 used ring。同样,从 vhost 工作线程向 guest 通信时,也采用同样的机制,只不过这种情况发的是一个回调的 call envent,kvm.ko 触发 irqfd 通知 guest。
vhost 与 kvm 的事件通信通过 eventfd 机制来实现,主要包括两个方向的 event,
代码分析整个通信的流程:
http://royluo.org/2014/08/22/vhost/
在 vhost 的方案中,由于 vhost 实现在内核中,guest 与 vhost 的通信,相较于原生的 virtio 方式性能上有了一定程度的提升,从 guest 到 kvm.ko 的交互只有一次用户态的切换以及数据拷贝。这个方案对于不同 host 之间的通信,或者 guest 到 host nic 之间的通信是比较好的,但是对于某些用户态进程间的通信,比如数据面的通信方案,openvswitch 和与之类似的 SDN 的解决方案,guest 需要和 host 用户态的 vswitch 进行数据交换,如果采用 vhost 的方案,guest 和 host 之间又存在多次的上下文切换和数据拷贝,为了避免这种情况,业界就想出将 vhost 从内核态移到用户态。这就是 vhost-user 的实现。
vhost-user 和 vhost 的实现原理是一样,都是采用 vring 完成共享内存,eventfd 机制完成事件通知。不同在于 vhost 实现在内核中,而 vhost-user 实现在用户空间中,用于用户空间中两个进程之间的通信,其采用共享内存的通信方式。
vhost-user 基于 C/S 的模式,采用 UNIX 域套接字(UNIX domain socket)来完成进程间的事件通知和数据交互,相比 vhost 中采用 ioctl 的方式,vhost-user 采用 socket 的方式大大简化了操作。
vhost-user 基于 vring 这套通用的共享内存通信方案,只要 client 和 server 按照 vring 提供的接口实现所需功能即可,常见的实现方案是 client 实现在 guest OS 中,一般是集成在 virtio 驱动上,server 端实现在 qemu 中,也可以实现在各种数据面中,如 OVS,Snabbswitch 等虚拟交换机。
如果使用 qemu 作为 vhost-user 的 server 端实现,在启动 qemu 时,我们需要指定 -mem-path 和 -netdev 参数,如:
- $ qemu -m 1024 -mem-path /hugetlbfs,prealloc=on,share=on \
- -netdev type=vhost-user,id=net0,file=/path/to/socket \
- -device virtio-net-pci,netdev=net0
指定 -mem-path 意味着 qemu 会在 guest OS 的内存中创建一个文件,share=on 选项允许其他进程访问这个文件,也就意味着能访问 guest OS 内存,达到共享内存的目的。
-netdev type=vhost-user 指定通信方案,file=/path/to/socket 指定 socket 文件。
当 qemu 启动之后,首先会进行 vring 的初始化,并通过 socket 建立 C/S 的共享内存区域和事件机制,然后 client 通过 eventfd 将 virtio kick 事件通知到 server 端,server 端同样通过 eventfd 进行响应,完成整个数据交互。
开源社区中实现了一个项目 Vapp,主要是用来测试 vhost-user 的 C/S 模式的,github 地址如下:
https://github.com/virtualopensystems/vapp.git
使用:
- $ git clone https://github.com/virtualopensystems/vapp.git
- $ cd vapp
- $ make
- // 运行 server 端
- $ ./vhost -s ./vhost.sock
- // 运行 client 端
- $ ./vhost -q ./vhost.sock
通过以上步骤,就可以启动 vhost-user 的 C/S 模式。
另外还有例子就是集成在虚拟交换机 Snabbswitch 上的 vhost-user,通过以下方式获得 vhost-user 分支:
- $ git clone -b vhostuser --recursive https://github.com/SnabbCo/snabbswitch.git
- $ cd snabbswitch
- $ make
- 测试:
- $ sudo src/snabbswitch -t apps.vhost.vhost_user
还有例子就是 qemu 上的实现,这也是最原早的实现,同样通过以下方式来获得使用:
- $ git clone -b vhost-user-v5 https://github.com/virtualopensystems/qemu.git
- $ mkdir qemu/obj
- $ cd qemu/obj/
- $ ../configure --target-list=x86_64-softmmu
- $ make -j
除此之外,还有很多的实现,如 OVS 和 DPDK 上都有实现,这实际上是集成了 vhost-user 的通用 API。
https://www.jianshu.com/p/ae54cb57e608
在软件实现的网络I/O半虚拟化中,vhost-user在性能、灵活性和兼容性等方面达到了近乎完美的权衡。虽然它的提出已经过了四年多,也已经有了越来越多的新特性加入,但是万变不离其宗,那么今天就从整个vhost-user数据通路的建立过程,以及数据包传输流程等方面详细介绍下vhost-user架构,本文基于DPDK 17.11分析。
vhost-user的最好实现在DPDK的vhost库里,该库包含了完整的virtio后端逻辑,可以直接在虚拟交换机中抽象成一个端口使用。在最主流的软件虚拟交换机OVS(openvswitch)中,就可以使用DPDK库。
vhost-user最典型的应用场景如图所示,OVS为每个虚拟机创建一个vhost端口,实现virtio后端驱动逻辑,包括响应虚拟机收发数据包的请求,处理数据包拷贝等。每个VM实际上运行在一个独立的QEMU进程内,QEMU是负责对虚拟机设备模拟的,它已经整合了KVM通信模块,因此QEMU进程已经成为VM的主进程,其中包含vcpu等线程。QEMU启动的命令行参数可以选择网卡设备类型为virtio,它就会为每个VM虚拟出virtio设备,结合VM中使用的virtio驱动,构成了virtio的前端。
前面说到VM实际上是运行在QEMU进程内的,那么VM启动的时候要想和OVS进程的vhost端口建立连接,从而实现数据包通路,就需要先建立起来一套控制信道。这套控制信道是基于socket进程间通信,是发生在OVS进程与QEMU进程之间,而不是与VM,另外这套通信有自己的协议标准和message的格式。在DPDK的lib/librte_vhost/vhost_user.c中可以看到所有的消息类型:
- static const char *vhost_message_str[VHOST_USER_MAX] = {
- [VHOST_USER_NONE] = "VHOST_USER_NONE",
- [VHOST_USER_GET_FEATURES] = "VHOST_USER_GET_FEATURES",
- [VHOST_USER_SET_FEATURES] = "VHOST_USER_SET_FEATURES",
- [VHOST_USER_SET_OWNER] = "VHOST_USER_SET_OWNER",
- [VHOST_USER_RESET_OWNER] = "VHOST_USER_RESET_OWNER",
- [VHOST_USER_SET_MEM_TABLE] = "VHOST_USER_SET_MEM_TABLE",
- [VHOST_USER_SET_LOG_BASE] = "VHOST_USER_SET_LOG_BASE",
- [VHOST_USER_SET_LOG_FD] = "VHOST_USER_SET_LOG_FD",
- [VHOST_USER_SET_VRING_NUM] = "VHOST_USER_SET_VRING_NUM",
- [VHOST_USER_SET_VRING_ADDR] = "VHOST_USER_SET_VRING_ADDR",
- [VHOST_USER_SET_VRING_BASE] = "VHOST_USER_SET_VRING_BASE",
- [VHOST_USER_GET_VRING_BASE] = "VHOST_USER_GET_VRING_BASE",
- [VHOST_USER_SET_VRING_KICK] = "VHOST_USER_SET_VRING_KICK",
- [VHOST_USER_SET_VRING_CALL] = "VHOST_USER_SET_VRING_CALL",
- [VHOST_USER_SET_VRING_ERR] = "VHOST_USER_SET_VRING_ERR",
- [VHOST_USER_GET_PROTOCOL_FEATURES] = "VHOST_USER_GET_PROTOCOL_FEATURES",
- [VHOST_USER_SET_PROTOCOL_FEATURES] = "VHOST_USER_SET_PROTOCOL_FEATURES",
- [VHOST_USER_GET_QUEUE_NUM] = "VHOST_USER_GET_QUEUE_NUM",
- [VHOST_USER_SET_VRING_ENABLE] = "VHOST_USER_SET_VRING_ENABLE",
- [VHOST_USER_SEND_RARP] = "VHOST_USER_SEND_RARP",
- [VHOST_USER_NET_SET_MTU] = "VHOST_USER_NET_SET_MTU",
- [VHOST_USER_SET_SLAVE_REQ_FD] = "VHOST_USER_SET_SLAVE_REQ_FD",
- [VHOST_USER_IOTLB_MSG] = "VHOST_USER_IOTLB_MSG",
- };
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
随着版本迭代,越来越多的feature被加进来,消息类型也越来越多,但是控制信道最主要的功能就是:传递建立数据通路必须的数据结构;控制数据通路的开启和关闭以及重连功能;热迁移或关闭虚拟机时传递断开连接的消息。
从虚拟机启动到数据通路建立完毕,所传递的消息都会记录在OVS日志文件中,对这些消息整理过后,实际流程如下图所示:
左边一半为一些协商特性,尤其像后端驱动与前端驱动互相都不知道对方协议版本的时候,协商这些特性是必要的。特性协商完毕,接下来就要传递建立数据通路所必须的数据结构,主要包括传递共享内存的文件描述符和内存地址的转换关系,以及virtio中虚拟队列的状态信息。下面对这些最关键的部分一一详细解读。
在虚拟机中,内存是由QEMU进程提前分配好的。QEMU一旦选择了使用vhost-user的方式进行网络通信,就需要配置VM的内存访问方式为共享的,具体的命令行参数在DPDK的文档中也有说明:
-object memory-backend-file,share=on,...
这意味着虚拟机的内存必须是预先分配好的大页面且允许共享给其他进程,具体原因在前一篇文章讲过,因为OVS和QEMU都是用户态进程,而数据包拷贝过程中,需要OVS进程里拥有访问QEMU进程里的虚拟机buffer的能力,所以VM内存必须被共享给OVS进程。
一些节约内存的方案,像virtio_balloon这样动态改变VM内存的方法不再可用,原因也很简单,OVS不可能一直跟着虚拟机不断改变共享内存啊,这样多费事。
而在vhost库中,后端驱动接收控制信道来的消息,主动映射VM内存的代码如下:
- static int vhost_user_set_mem_table(struct virtio_net *dev, struct VhostUserMsg *pmsg)
- {
- ...
- mmap_size = RTE_ALIGN_CEIL(mmap_size, alignment);
-
- mmap_addr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,
- MAP_SHARED | MAP_POPULATE, fd, 0);
- ...
- reg->mmap_addr = mmap_addr;
- reg->mmap_size = mmap_size;
- reg->host_user_addr = (uint64_t)(uintptr_t)mmap_addr +
- mmap_offset;
- ...
- }
它使用的linux库函数mmap()
来映射VM内存,详见linux编程手册http://man7.org/linux/man-pages/man2/mmap.2.html。注意到在映射内存之前它还干了一件事,设置内存对齐,这是因为mmap函数映射内存的基本单位是一个页,也就是说起始地址和大小都必须是页大小是整数倍,在大页面环境下,就是2MB或者1GB。只有对齐以后才能保证映射共享内存不出错,以及后续访存行为不会越界。
后面三行是保存地址转换关系的信息。这里涉及到几种地址转换,在vhost-user中最复杂的就是地址转换。从QEMU进程角度看虚拟机内存有两种地址:GPA(Guest Physical Address)和QVA(QEMU Virtual Address);从OVS进程看虚拟机内存也有两种地址GPA(Guest Physical Address)和VVA(vSwitch Virtual Address)
GPA是virtio最重要的地址,在virtio的标准中,用来存储数据包地址的虚拟队列virtqueue里面每项都是以GPA表示的。但是对于操作系统而言,我们在进程内访问实际使用的都是虚拟地址,物理地址已经被屏蔽,也就是说进程只有拿到了物理地址所对应的虚拟地址才能够去访存(我们编程使用的指针都是虚拟地址)。
QEMU进程容易实现,毕竟作为VM主进程给VM预分配内存时就建立了QVA到GPA的映射关系。
对于OVS进程,以上图为例,mmap()函数返回的也是虚拟地址,是VM内存映射到OVS地址空间的起始地址(就是说我把这一块内存映射在了以mmap_addr起始的大小为1GB的空间里)。这样给OVS进程一个GPA,OVS进程可以利用地址偏移算出对应的VVA,然后实施访存。
但是实际映射内存比图例复杂的多,可能VM内存被拆分成了几块,有些块起始的GPA不为0,这就需要在映射块中再加一个GPA偏移量,才能完整保留下来VVA与GPA之间的地址对应关系。这些对应关系是后续数据通路实现的基础。
还有一点技术细节值得注意的是,在socket中,一般是不可以直接传递文件描述符的,文件描述符在编程角度就是一个int型变量,直接传过去就是一个整数,这里也是做了一点技术上的trick,感兴趣的话也可以研究下
虚拟队列的结构由三部分构成:avail ring、used ring和desc ring。这里稍微详细说明一下这三个ring的作用与设计思想。
传统网卡设计中只有一个环表,但是有两个指针,分别为驱动和网卡设备管理的。这就是一个典型的生产者-消费者问题,生产数据包的一方移动指针,另一方追逐,直到全部消费完。但是这么做的缺点在于,只能顺序执行,前一个描述符处理完之前,后一个只能等待。
但在虚拟队列中,把生产者和消费者分离开来。desc ring中存放的是真实的数据包描述符(就是数据包或其缓冲区地址),avail ring和used ring中存放的指向desc ring中项的索引。前端驱动将生产出来的描述符放到avail ring中,后端驱动把已经消费的描述符放到used ring中(其实就是写desc ring中的索引,即序号)。这样前端驱动就可以根据used ring来回收已经使用的描述符,即使中间有描述符被占用,也不会影响被占用描述符之后的描述符的回收。另外DPDK还针对这种结构做了一种cache层面上预取的优化,使之更加高效。
在控制信道建立完共享内存以后,还需要在后端也建立与前端一样的虚拟队列数据结构。所需要的信息主要有:desc ring的项数(不同的前端驱动不同,比如DPDK virtio驱动和内核virtio驱动就不一样)、上次avail ring用到哪(这主要是针对重连或动态迁移的,第一次建立连接此项应为0)、虚拟队列三个ring的起始地址、设置通知信号。
这几项消息处理完以后,后端驱动利用接收到的起始地址创建了一个和前端驱动一模一样的虚拟队列数据结构,并已经准备好收发数据包。其中最后两项eventfd是用于需要通知的场景,例如:虚拟机使用内核virtio驱动,每次OVS的vhost端口往虚拟机发送数据包完成,都需要使用eventfd通知内核驱动去接收该数据包,在轮询驱动下,这些eventfd就没有意义了。
另外,VHOST_USER_GET_VRING_BASE
是一个非常奇特的信号,只在虚拟机关机或者断开时会由QEMU发送给OVS进程,意味着断开数据通路。
数据通路的实现在DPDK的lib/librte_vhost/virtio_net.c中,虽然代码看起来非常冗长,但是其中大部分都是处理各种特性以及硬件卸载功能的,主要逻辑却非常简单。
负责数据包的收发的主函数为:
- uint16_t rte_vhost_enqueue_burst(int vid, uint16_t queue_id, struct rte_mbuf **pkts, uint16_t count)
- //数据包流向 OVS 到 VM
- uint16_t rte_vhost_dequeue_burst(int vid, uint16_t queue_id,
- struct rte_mempool *mbuf_pool, struct rte_mbuf **pkts, uint16_t count)
- //数据包流向 VM 到 OVS
具体的发送过程概括来说就是,如果OVS往VM发送数据包,对应的vhost端口去avail ring中读取可用的buffer地址,转换成VVA后,进行数据包拷贝,拷贝完成后发送eventfd通知VM;如果VM往OVS发送,则相反,从VM内的数据包缓冲区拷贝到DPDK的mbuf数据结构。以下贴一段代码注释吧,不要管里面的iommu、iova,那些都是vhost-user的新特性,可用理解为iova就是GPA。
- uint16_t
- rte_vhost_dequeue_burst(int vid, uint16_t queue_id,
- struct rte_mempool *mbuf_pool, struct rte_mbuf **pkts, uint16_t count)
- {
- struct virtio_net *dev;
- struct rte_mbuf *rarp_mbuf = NULL;
- struct vhost_virtqueue *vq;
- uint32_t desc_indexes[MAX_PKT_BURST];
- uint32_t used_idx;
- uint32_t i = 0;
- uint16_t free_entries;
- uint16_t avail_idx;
-
- dev = get_device(vid); //根据vid获取dev实例
- if (!dev)
- return 0;
-
- if (unlikely(!is_valid_virt_queue_idx(queue_id, 1, dev->nr_vring))) {
- RTE_LOG(ERR, VHOST_DATA, "(%d) %s: invalid virtqueue idx %d.\n",
- dev->vid, __func__, queue_id);
- return 0;
- } //检查虚拟队列id是否合法
-
- vq = dev->virtqueue[queue_id]; //获取该虚拟队列
-
- if (unlikely(rte_spinlock_trylock(&vq->access_lock) == 0)) //对该虚拟队列加锁
- return 0;
-
- if (unlikely(vq->enabled == 0)) //如果vq不可访问,对虚拟队列解锁退出
- goto out_access_unlock;
-
- vq->batch_copy_nb_elems = 0; //批处理需要拷贝的数据包数目
-
- if (dev->features & (1ULL << VIRTIO_F_IOMMU_PLATFORM))
- vhost_user_iotlb_rd_lock(vq);
-
- if (unlikely(vq->access_ok == 0))
- if (unlikely(vring_translate(dev, vq) < 0)) //因为IOMMU导致的,要翻译iova_to_vva
- goto out;
-
- if (unlikely(dev->dequeue_zero_copy)) { //零拷贝dequeue
- struct zcopy_mbuf *zmbuf, *next;
- int nr_updated = 0;
-
- for (zmbuf = TAILQ_FIRST(&vq->zmbuf_list);
- zmbuf != NULL; zmbuf = next) {
- next = TAILQ_NEXT(zmbuf, next);
-
- if (mbuf_is_consumed(zmbuf->mbuf)) {
- used_idx = vq->last_used_idx++ & (vq->size - 1);
- update_used_ring(dev, vq, used_idx,
- zmbuf->desc_idx);
- nr_updated += 1;
-
- TAILQ_REMOVE(&vq->zmbuf_list, zmbuf, next);
- restore_mbuf(zmbuf->mbuf);
- rte_pktmbuf_free(zmbuf->mbuf);
- put_zmbuf(zmbuf);
- vq->nr_zmbuf -= 1;
- }
- }
-
- update_used_idx(dev, vq, nr_updated);
- }
-
- /*
- * Construct a RARP broadcast packet, and inject it to the "pkts"
- * array, to looks like that guest actually send such packet.
- *
- * Check user_send_rarp() for more information.
- *
- * broadcast_rarp shares a cacheline in the virtio_net structure
- * with some fields that are accessed during enqueue and
- * rte_atomic16_cmpset() causes a write if using cmpxchg. This could
- * result in false sharing between enqueue and dequeue.
- *
- * Prevent unnecessary false sharing by reading broadcast_rarp first
- * and only performing cmpset if the read indicates it is likely to
- * be set.
- */
- //构造arp包注入到mbuf(pkt数组),看起来像是虚拟机发送的
- if (unlikely(rte_atomic16_read(&dev->broadcast_rarp) &&
- rte_atomic16_cmpset((volatile uint16_t *)
- &dev->broadcast_rarp.cnt, 1, 0))) {
-
- rarp_mbuf = rte_pktmbuf_alloc(mbuf_pool); //从mempool中分配一个mbuf给arp包
- if (rarp_mbuf == NULL) {
- RTE_LOG(ERR, VHOST_DATA,
- "Failed to allocate memory for mbuf.\n");
- return 0;
- }
- //构造arp报文,构造成功返回0
- if (make_rarp_packet(rarp_mbuf, &dev->mac)) {
- rte_pktmbuf_free(rarp_mbuf);
- rarp_mbuf = NULL;
- } else {
- count -= 1;
- }
- }
- //计算有多少数据包,现在的avail的索引减去上次停止时的索引值,若没有直接释放vq锁退出
- free_entries = *((volatile uint16_t *)&vq->avail->idx) -
- vq->last_avail_idx;
- if (free_entries == 0)
- goto out;
-
- LOG_DEBUG(VHOST_DATA, "(%d) %s\n", dev->vid, __func__);
-
- /* Prefetch available and used ring */
- //预取前一次used和avail ring停止位置索引
- avail_idx = vq->last_avail_idx & (vq->size - 1);
- used_idx = vq->last_used_idx & (vq->size - 1);
- rte_prefetch0(&vq->avail->ring[avail_idx]);
- rte_prefetch0(&vq->used->ring[used_idx]);
-
- //此次接收过程的数据包数目,为待处理数据包总数、批处理数目最小值
- count = RTE_MIN(count, MAX_PKT_BURST);
- count = RTE_MIN(count, free_entries);
- LOG_DEBUG(VHOST_DATA, "(%d) about to dequeue %u buffers\n",
- dev->vid, count);
-
- /* Retrieve all of the head indexes first to avoid caching issues. */
- //从avail ring中取得所有数据包在desc中的索引,存在局部变量desc_indexes数组中
- for (i = 0; i < count; i++) {
- avail_idx = (vq->last_avail_idx + i) & (vq->size - 1);
- used_idx = (vq->last_used_idx + i) & (vq->size - 1);
- desc_indexes[i] = vq->avail->ring[avail_idx];
-
- if (likely(dev->dequeue_zero_copy == 0)) //若不支持dequeue零拷贝的话,直接将索引写入used ring中
- update_used_ring(dev, vq, used_idx, desc_indexes[i]);
- }
-
- /* Prefetch descriptor index. */
- rte_prefetch0(&vq->desc[desc_indexes[0]]); //从desc中预取第一个要发的数据包描述符
- for (i = 0; i < count; i++) {
- struct vring_desc *desc, *idesc = NULL;
- uint16_t sz, idx;
- uint64_t dlen;
- int err;
-
- if (likely(i + 1 < count))
- rte_prefetch0(&vq->desc[desc_indexes[i + 1]]); //预取后一项
-
- if (vq->desc[desc_indexes[i]].flags & VRING_DESC_F_INDIRECT) { //如果该项支持indirect desc,按照indirect处理
- dlen = vq->desc[desc_indexes[i]].len;
- desc = (struct vring_desc *)(uintptr_t) //地址转换成vva
- vhost_iova_to_vva(dev, vq,
- vq->desc[desc_indexes[i]].addr,
- &dlen,
- VHOST_ACCESS_RO);
- if (unlikely(!desc))
- break;
-
- if (unlikely(dlen < vq->desc[desc_indexes[i]].len)) {
- /*
- * The indirect desc table is not contiguous
- * in process VA space, we have to copy it.
- */
- idesc = alloc_copy_ind_table(dev, vq,
- &vq->desc[desc_indexes[i]]);
- if (unlikely(!idesc))
- break;
-
- desc = idesc;
- }
-
- rte_prefetch0(desc); //预取数据包
- sz = vq->desc[desc_indexes[i]].len / sizeof(*desc);
- idx = 0;
- }
- else {
- desc = vq->desc; //desc数组
- sz = vq->size; //size,个数
- idx = desc_indexes[i]; //desc索引项
- }
-
- pkts[i] = rte_pktmbuf_alloc(mbuf_pool); //给正在处理的这个数据包分配mbuf
- if (unlikely(pkts[i] == NULL)) { //分配mbuf失败,跳出包处理
- RTE_LOG(ERR, VHOST_DATA,
- "Failed to allocate memory for mbuf.\n");
- free_ind_table(idesc);
- break;
- }
-
- err = copy_desc_to_mbuf(dev, vq, desc, sz, pkts[i], idx,
- mbuf_pool);
- if (unlikely(err)) {
- rte_pktmbuf_free(pkts[i]);
- free_ind_table(idesc);
- break;
- }
-
- if (unlikely(dev->dequeue_zero_copy)) {
- struct zcopy_mbuf *zmbuf;
-
- zmbuf = get_zmbuf(vq);
- if (!zmbuf) {
- rte_pktmbuf_free(pkts[i]);
- free_ind_table(idesc);
- break;
- }
- zmbuf->mbuf = pkts[i];
- zmbuf->desc_idx = desc_indexes[i];
-
- /*
- * Pin lock the mbuf; we will check later to see
- * whether the mbuf is freed (when we are the last
- * user) or not. If that's the case, we then could
- * update the used ring safely.
- */
- rte_mbuf_refcnt_update(pkts[i], 1);
-
- vq->nr_zmbuf += 1;
- TAILQ_INSERT_TAIL(&vq->zmbuf_list, zmbuf, next);
- }
-
- if (unlikely(!!idesc))
- free_ind_table(idesc);
- }
- vq->last_avail_idx += i;
-
- if (likely(dev->dequeue_zero_copy == 0)) { //实际此次批处理的所有数据包内容拷贝
- do_data_copy_dequeue(vq);
- vq->last_used_idx += i;
- update_used_idx(dev, vq, i); //更新used ring的当前索引,并eventfd通知虚拟机接收完成
- }
-
- out:
- if (dev->features & (1ULL << VIRTIO_F_IOMMU_PLATFORM))
- vhost_user_iotlb_rd_unlock(vq);
-
- out_access_unlock:
- rte_spinlock_unlock(&vq->access_lock);
-
- if (unlikely(rarp_mbuf != NULL)) { //再次检查有arp报文需要发送的话,就加入到pkts数组首位,虚拟交换机的mac学习表就能第一时间更新
- /*
- * Inject it to the head of "pkts" array, so that switch's mac
- * learning table will get updated first.
- */
- memmove(&pkts[1], pkts, i * sizeof(struct rte_mbuf *));
- pkts[0] = rarp_mbuf;
- i += 1;
- }
-
- return i;
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
在软件实现的网络功能中大量使用批处理,而且一批的数目是可以自己设置的,但是一般约定俗成批处理最多32个数据包。
在OVS中有很多这样的vhost端口,DPDK加速的OVS已经实现绑核轮询这些端口了。因此一般会把众多的端口尽可能均匀地绑定到有限的CPU核上,一些厂商在实际的生产环境中如下图所示,甚至做到了以队列为单位来负载均衡。
另外针对物理端口和虚拟端口的绑核也有一些优化问题,主要是为了避免读写锁,比如:把物理网卡的端口全部绑定到一个核上,软件的虚拟端口放到另一个核上,这样收发很少在一个核上碰撞等。
OVS每个轮询核的工作也非常简单,对于每个轮询到的端口,查看它有多少数据包需要接收,接收完这些数据包后,对它们进行查表(flow table,主要是五元组匹配,找到目的端口),然后依次调用对应端口的发送函数发送出去(就像vhost端口的rte_vhost_enqueue_burst函数),全部完成后继续轮询下一个端口。而SLA、Qos机制通常都是在调用对应端口的发送函数之前起作用,比如:查看该端口的令牌桶是否有足够令牌发送这些数据包,如果需要限速会选择性丢掉一些数据包,然后执行发送函数。
其实不管是哪种软件实现的转发逻辑,都始终遵循在数据通路上尽可能简单,其他的机制、消息响应可以复杂多样的哲学。
virtio,vhost,vhost-user 是基于场景和性能而提出的三种 guest 和 host 之间的通信方案,三种方案,各有优劣。
vhost-user 用在很多数据面之上的进程间通信,效率高。
《Linux虚拟化KVM-Qemu分析(八)之virtio初探》
《Linux虚拟化KVM-Qemu分析(九)之virtio设备》
《使用DPDK打开Open vSwitch(OvS) *概述》
《《深入浅出DPDK》读书笔记(十五):DPDK应用篇(Open vSwitch(OVS)中的DPDK性能加速)》
《virtio 网络的演化:原始virtio > vhost-net(内核态) > vhost-user(DPDK) > vDPA》
站外参考文章
《关于英特尔® 以太网服务器适配器中 SR-IOV 的常见问题解答》
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。