1,virtqueue

图一

每个queue实际上是由tx/rx两个virtqueue组成的也就是说tx和rx的virtqueue是分开的,并没有共享。一个virtio net设备最多有多少个queue由后端vhost决定,但前端可以通过ethtool –L eth0 combined 16命令动态修改当前队列数,每个queue有多少个描述符(即队列深度)是由前端决定。一个queue里有3个关键的数据实体:

descriptor table:描述符表,每个描述符指明了缓冲区的位置,长度,以及与后面描述符的串联情况。描述符在描述符表中的位置就是它的ID,available/used ring用ID来引用描述符。

used ring:已用描述符环,在tx queue (vm tx and vhost rx) 中,被vhost用来归还已发送报文的描述符给vm,在rx queue (vhost tx and vm rx) 中,被vhost用来填充发送给vm的报文,由上可知,used ring始终是vhost在写,而vm在读。

available ring:可用描述符环,在tx queue (vm tx and vhost rx) 中,被vm用来填充发送给vhost的报文,在rx queue (vhost tx and vm rx) 中,被vm用来填充空的缓冲区给vm以接收新报文,由上可知,available ring始终是vm在写,而vhost在读。

vm和vhost共享的能同时看到的内存区就是上述3个数据实体:descriptor table,used ring,available ring,而struct virtqueue中其他数据结构都只是vhost本地的数据,vm看不到。考虑到数据的一致性,vhost操作available ring和used ring的原则是:

  • 最先读取available ring的idx,把它保存为局部变量,之后在单轮burst收发过程中不再读取avail ring的idx。
  • 最后写入used ring的idx:无论是tx (vm tx and vhost rx)时归还已发送报文的描述符还是在rx (vm rx and vhost tx)时填充发送给vm的报文,都应在所有操作完成后改变used ring的idx,因为一旦改变,理论上vm就可以看到。

无论是vhost还是vm,他们都可以工作在interrupt和polling两种模式之一:

  • interrupt意味着需要发消息给对方,对方才知道avail / used ring中的数据有变化,需要通知对方时,vq->callfd用于vhost通知vm,而vq->kickfd用于vm通知vhost。
  • polling意味着对端一直在轮询avail / used ring,所以无需发消息对端就能只能知道avail / used ring的变化。

dpdk vhost user显然是工作在polling模式,所以vq->used->flags上会有VRING_USED_F_NO_NOTIFY标识。 这也就是说vm操作available ring导致变化时并不需要发消息给vhost。传统的vm virtio net驱动是工作在interrupt模式,所以vq->avail->flags默认为0而没有VRING_AVAIL_F_NO_INTERRUPT标识。这也就是说vhost操作used ring导致变化时需要发消息通知vm。当然虚拟机里面也可以运行dpdk vhost user驱动,这时vm就是工作在polling模式了,这时vq->avail->flags就会带上VRING_AVAIL_F_NO_INTERRUPT标识。这也就是说vhost操作used ring导致变化时无需要发消息通知vm。

图二

tx (vm tx and vhost rx)时ring的使用

前后端共享区域:vq->avail.ring[x]存放vm发送给vhost的报文,vq->avail.idx指示available ring中vm发送给vhost的报文的头部位置,也就是vm放下一个filled buffer的位置。vq->used.ring[x]存放vhost发送完报文后归还给vm的描述符,vq->used.idx指示used ring中vhost归还给vm的描述符的头部位置,也就是vhost放下一个used buffer的位置。

vhost私有区域:vq->last_avail_idx记录vhost从available ring中取下一个vm发过来的报文的位置,vq->last_used_idx记录used ring中vhost归还下一个used buffer描述符的位置,这个值最终是会等于vq->used->idx。

rx (vm rx and vhost tx)时ring的使用

前后端共享区域:vq->avail.ring[x]存放vm发送给vhost的用以接收新报文的empty buffer,vq->avail.idx指示available ring中vm发送给vhost的empty buffer的头部位置,也就是vm放下一个empty buffer的位置。vq->used.ring[x]存放vhost发送给vm的报文filled buffer,vq->used.idx指示used ring中vhost发给vm的报文的头部位置,也就是vhost放下一个报文filled buffer的位置。

vhost私有区域:vq->last_avail_idx记录vhost从available ring中取下一个vm发过来的empty buffer的位置,vq->last_used_idx记录used ring中vhost填充下一个发给vm的报文filled buffer描述符的位置,这个值最终是会等于vq->used->idx。vq->shadow_used_idx表示在报文copy之前预取了多少个empty buffer,vq->shadow_used_ring在报文copy之前预取的empty buffer都放在这里包括描述符的索引及长度。

ring的idx索引

需要注意的是ring的大小就是queue的大小,即vq->size,这个值一般不大,典型值为256. 但是vq->used->idx, vq->avail->idx, vq->last_used_idx, vq->last_avail_idx这些索引并不会限于vq->size,它们是单方向增长的,也就是会一直增加直到超出uint16_t的范围溢出再从0开始。可看做是在一条无止境的射线上。只有拿这些index真正去索引vq->used->ring[x]和vq->avail->ring[x]时才需要将这些索引与(vq->size - 1)进行与操作:ring [index & (vq->size - 1)]。无论是tx还是rx,索引的大小关系是恒定的,即使是uint16_t溢出: vq->used->idx < vq->last_used_idx < vq->last_avail_idx < vq->avail->idx,所以我们可以用 vq->avail->idx - vq->last_used_idx 表示还可以发送多少个报文给vm而不需要考虑这2个数之间的大小和绕卷。

2,vhost user与qemu的消息交互

图三

2.1,vhost_user_msg_handler(vid, fd)

dev = get_device(vid)
read_vhost_message(fd, &msg)read_fd_message(sockfd, msg, msg->fds)recvmsg(sockfd, &msgh, 0)cmsg = CMSG_FIRSTHDR(&msgh)memcpy(fds, CMSG_DATA(cmsg), fdsize)read(sockfd, &msg->payload, msg->size)

读取来自QEMU的vhost消息,然后根据msg.request进行对应处理 switch (msg.request):

2.2,VHOST_USER_GET_FEATURES

msg.payload.u64 = VHOST_FEATURES
send_vhost_message

返回vhost支持的virtio-net功能子集它来自全局变量VHOST_FEATURES。

2.3,VHOST_USER_SET_FEATURES

dev->features = features
dev->vhost_hlen = sizeof(struct virtio_net_hdr_mrg_rxbuf)

检查功能掩码,设置vhost和virtio前端共同支持的特性,需要两者同时支持才能生效,同时根据dev->features里是否包含VIRTIO_NET_F_MRG_RXBUF设置virtio net header的长度。

2.4,VHOST_USER_RESET_OWNER

dev->flags &= ~VIRTIO_DEV_RUNNING
notify_ops->destroy_device(dev->vid)
cleanup_device(dev, 0); reset_device(dev);

当前进程释放对设备的所有权,这一步也会同时destroy设备。

2.5,VHOST_USER_SET_MEM_TABLE

vhost_user_set_mem_table

dev->guest_pages = malloc(dev->max_guest_pages *sizeof(struct guest_page))
dev->mem = rte_zmalloc(sizeof(struct virtio_memory) + sizeof(struct virtio_memory_region) * nregions)
dev->mem->nregions = memory.nregions
Iterate every memory region in message: fd = pmsg->fds[i]; reg = &dev->mem->regions[i];
reg->guest_phys_addr = memory.regions[i].guest_phys_addr
reg->guest_user_addr = memory.regions[i].userspace_addr
reg->size = memory.regions[i].memory_size; reg->fd = fd
reg->mmap_addr = mmap(mmap_size, READ | WRITE, MAP_SHARED | MAP_POPULATE, fd)
reg->mmap_size = RTE_ALIGN_CEIL((reg->size + memory.regions[i].mmap_offset), alignment)
reg->host_user_addr = reg->mmap_addr + mmap_offset
add_guest_pages(dev, reg, alignment)

add_guest_pages(dev, reg, alignment)

host_phys_addr = rte_mem_virt2phy(reg->host_user_addr)
add_one_guest_page(dev, guest_phys_addr, host_phys_addr, size)page = &dev->guest_pages[dev->nr_guest_pages++]page->guest_phys_addr = guest_phys_addrpage->host_phys_addr = host_phys_addrpage->size = size

设置内存空间布局信息,用于报文收发时的地址转换。

虚拟机是以一个进程的形式运行的,虚拟机内部看到的物理地址空间(guest phys addr)实际上是虚拟机所在进程的虚拟地址空间。虚拟机所发出的报文描述符携带的是guest phys addr,这是虚拟机所在进程的虚拟地址空间。为了能在vhost user这个进程访问到虚拟机的guest phys addr,vhost user必须把虚拟机的guest phys addr空间映射到自己的虚拟地址空间来,vhost user自己的虚拟地址空间称作host user addr。这个映射以共享内存的方式进行:qemu把虚拟机的guest phys addr以文件描述符fd_vm的形式告诉vhost user,vhost user再用mmap把这个fd_vm映射到自己的虚拟地址空间host user addr。这样vhost user就可以像访问自己的数据一样去访问报文了。

guest phys addr到host user addr的转换主要靠dev->mem->regions[x]进行(参考图二里的virtio_memory):

  • .guest_phys_addr qemu通过消息告诉vhost user的虚拟机所看到的物理地址空间,也就是虚拟机所在进程的虚拟机地址空间。
  • .guest_user_addr 这是guest phys addr映射到qemu的虚拟地址空间中的地址。
  • .host_user_addr 这是把虚拟机所在进程的虚拟机地址空间通过mmap映射到vhost user进程后的地址。
  • .size 内存区块的大小。
  • .mmap_addr 这个地址即是mmap返回的地址,它与host_user_addr仅仅相差一个mmap_offset.fd 代表虚拟机所在进程的虚拟地址空间,是一个命名的共享内存块。

gpa_to_vva:vhost user把虚拟机发送的报文copy到mbuf时,首先需要把报文描述符内的guest phys addr转换 成vhost user能访问的host user addr。

当zero copy开启时,vhost user直接把虚拟机发过来的报文转发给host主机上网卡,而不进行copy。这个时候涉及到2层地址转换:将虚拟机的guest phys addr转换成vhost user所能直接访问的host user addr,这一步即通过上面所述的共享内存mmap形式进行;host主机上的网卡并不能直接访问host user addr,因为它是vhost user进程的虚拟地址空间,所以vhost user在发送报文时还需把host user addr转换为host phys addr。这一步是通过访问vhost user进程自己的页表/proc/vhost_user_pid/pagemap来进行翻译的。

guest phys addr到host phys addr的转换主要靠dev->guest_pages[x]进行(参考图二里的guest_page)

  • .guest_phys_addr qemu通过消息告诉vhost user的虚拟机所看到的物理地址空间,也就是虚拟机所在进程的虚拟机地址空间。
  • .host_phys_addr 这是网卡所能访问的主机物理地址空间,它通过读取vhost user进程的页表 /proc/vhost_user_pid/pagemap翻译得到。
  • .size 内存区块的大小。

gpa_to_hpa:vhost user进行zero copy直接把虚拟机发过来的报文转发给host上的物理网卡时需要把报文描述符内的guest phys addr转换成网卡能访问的host phys addr。

2.6,VHOST_USER_SET_VRING_NUM

vq = dev->virtqueue[state->index]
vq->size = state->num
if (dev->dequeue_zero_copy) vq->zmbufs = rte_zmalloc(vq->zmbuf_size * sizeof(struct zcopy_mbuf))
vq->shadow_used_ring = rte_malloc(vq->size * sizeof(struct vring_used_elem))

vhost记录某个虚拟队列的大小,即描述符的个数。同时为每个描述符分配一个struct zcopy_mbuf结构,以在zero copy时把mbuf和描述符关联起来。

2.7,VHOST_USER_SET_VRING_ADDR

vq = dev->virtqueue[addr->index]
vq->desc = qva_to_vva(dev,addr->desc_user_addr)
vq->avail = qva_to_vva(dev,addr->avail_user_addr)
vq->used = qva_to_vva(dev,addr->used_user_addr)

由qemu发送virtqueue结构的descriptor table, available ring, used ring虚拟地址,vhost user将该地址转换成vhost user的虚拟地址。qva_to_vva():

2.8,VHOST_USER_SET_VRING_BASE

dev->virtqueue[state->index]->last_used_idx = state->num
dev->virtqueue[state->index]->last_avail_idx = state->num

设置某个虚拟队列的last_used_idx和last_avail_idx的初始值,vhost通过该索引值找到初始描述符。

2.9,VHOST_USER_GET_VRING_BASE

vhost_user_get_vring_base(struct vhost_vring_state *state):

vq = dev->virtqueue[state->index]
dev->flags &= ~VIRTIO_DEV_RUNNING
notify_ops->destroy_device(dev->vid)
state->num = vq->last_used_idx
close(vq->kickfd)
if (dev->dequeue_zero_copy) free_zmbufs(vq)
rte_free(vq->shadow_used_ring)
send_vhost_message

将虚拟队列的当前last_used_idx值发送给qemu。这个消息只有在qemu将要关闭这个虚拟队列时才发送,所以vhost user要做一些关闭时的清理动作:

  • 1, 调用notify_ops->destroy_device(dev->vid)通知ovs即将关闭virtio net设备。
  • 2, close(vq->kickfd):打开vq->kickfd表示启用虚拟队列,关闭表示停止虚拟队列。
  • 3, 释放为虚拟队列中每个描述符分配的struct zcopy_mbuf结构。

2.10,VHOST_USER_SET_VRING_KICK

index = pmsg->payload.u64;
fd = pmsg->fds[0]
vq = dev->virtqueue[file.index];
vq->kickfd = file.fd
if (virtio_is_ready(dev)) notify_ops->new_device(dev->vid)

传递eventfd文件描述符kickfd。当guest有新的数据要发送时写PCI BAR空间中的notification capability,这是一个doorbell会引发vm-exit陷入host kvm,之后KVM根据发生page fault的MMIO地址找到关联的ioeventfd,这就是之前QEMU注册的kickfd。kickfd被QEMU传递给vhost-user后其一端是KVM另一端是vhost-user,所以KVM将kickfd变为ready后vhost-user就会被唤醒。通过该文件描述符通知vhsot接收数据并发送到目的地;vhost使用eventfd代理模块把这个文件描述符从qemu上下文切换到自己的进程上下文。kickfd用于虚拟机通知vhost user,但是由于当前dpdk采用polling模式,这个kickfd实际未使用。目前kickfd的唯一用途是:打开vq->kickfd表示启用虚拟队列,关闭表示停止虚拟队列。

2.11,VHOST_USER_SET_VRING_CALL

index = pmsg->payload.u64;
fd = pmsg->fds[0]
vq = dev->virtqueue[file.index];
vq->callfd = file.fd

传递eventfd文件描述符callfd。使vhost能够在完成对新的数据包接收或有新数据发送到虚拟机时,通过中断方式通知guest准备回收缓冲区或接收数据包。使用eventfd代理模块把这个文件描述符从qemu上下文切换到自己的进程上下文。callfd用于vhost user通知虚拟机,它的一端是vhost-user另一端是KVM,当vhost-user将callfd变为ready后KVM会在vm-entry时向VM注入一个MSIX中断。

2.12,VHOST_USER_GET_QUEUE_NUM

msg.payload.u64 = VHOST_MAX_QUEUE_PAIRS
send_vhost_message

返回virtio net设备的队列数,每个队列对应了2个struct vhost_virtqueue *vq,称之为queue pair,一个为rx queue,一个为tx queue。

2.13,VHOST_USER_SET_VRING_ENABLE

notify_ops->vring_state_changed(dev->vid, state->index, enable)
dev->virtqueue[state->index]->enabled = enable

当虚拟队列准备好启用时,qemu发送此消息enable此虚拟队列。

2.14,VHOST_USER_SEND_RARP

mac = (uint8_t *)&msg->payload.u64
memcpy(dev->mac.addr_bytes, mac, 6)
dev->broadcast_rarp = 1

当虚拟机发生迁移时需要构造一个rarp报文并广播到各个交换机以让虚拟机学习虚拟机的新地址。此处并不直接发送rarp报文,而仅在dev->broadcast_rarp做一下标记,之后在rte_vhost_dequeue_burst()从虚拟机收包时插入一个rarp报文,并随同虚拟机要发送的报文一起发送(广播)。

3,初始化

当使用vhost-user时,需要在系统中创建一个unix domain socket server,用来处理qemu发送给host的消息。 如果有新的socket连接,说明guest创建了新的virtio-net设备,vhost驱动会为之创建一个vhost设备,之后qemu就可以通过socket和vhost进行通信了;当socket关闭,vhost就会销毁对应的设备。

初始化首先是从struct netdev_class dpdk_vhost_class->init()开始,即netdev_dpdk_vhost_class_init()。

第一步首先是调用rte_vhost_driver_callback_register注册了一些事件回调函数:

new_device(int vid) :新的virtio net设备准备好时的回调函数。
destroy_device(int vid) :virtio net设备关闭时的回调函数。
vring_state_changed(int vid, uint16_t queue_id, int enable):virtio net设备的某个queue开启/关闭的回调函数。

第二步是调整了一下默认的virtio net feature。

第三步是起了一个线程轮询各个socket,这包括作为server 的socket,以及client连上来的socket这些fd是在调用rte_vhost_driver_register的过程中逐步添加到全局变量vhost_user的。

当virtio端口被添加后会调用struct netdev *netdev->class->construct()即netdev_dpdk_vhost_construct。

对于vhost server而言,它会起一个socket开始监听来自qemu的连接,有client连过来以后会创建一个struct virtio_net设备放在全局变量vhost_user里,同时开始监听来自client的消息。对于vhost client而言,它会主动去连接vhost server,同时也会创建一个struct virtio_net设备放在全局变量vhost_user里,同时开始监听来自server的消息。无论是server还是client,其消息的处理都是在函数vhost_user_msg_handler里处理的(见2.1)。

4,发包流程

这里的发包是指前端guest os driver收包,后端dpdkvhostuser发包。发包对上API是netdev_send(),它实际上调用了netdev_dpdk_vhost_send() -> rte_vhost_enqueue_burst(),当双方支持VIRTIO_NET_F_MRG_RXBUF 功能时,即guest os driver能接收保存在一个描述符数组内的包,则调用virtio_dev_merge_rx() 把包发driver。若不支持上述功能,则调用virtio_dev_rx() 把包发给driver。

4.1,virtio_dev_merge_rx

vq = dev->virtqueue[queue_id];for (pkt_idx = 0; pkt_idx < count; pkt_idx++) {uint32_t pkt_len = pkts[pkt_idx]->pkt_len + vq->vhost_hlen;reserve_avail_buf_mergeable(vq, pkt_len, &start, &end);nr_used = copy_mbuf_to_desc_mergeable(dev, vq, start, end, pkts[pkt_idx]);rte_smp_wmb();while (unlikely(vq->last_used_idx != start)) rte_pause();*(volatile uint16_t *)&vq->used->idx += nr_used;vhost_log_used_vring(dev, vq, offsetof(struct vring_used, idx), sizeof(vq->used->idx));vq->last_used_idx = end;
}rte_mb();if (!(vq->avail->flags & VRING_AVAIL_F_NO_INTERRUPT) && (vq->callfd >= 0))eventfd_write(vq->callfd, (eventfd_t)1);

这里整体的逻辑是遍历每一个mbuf,调用reserve_avail_buf_mergeable()为每一个mbuf预留分配足够的描述符,同时记录描述符预留区间 [start, end],再调用copy_mbuf_to_desc_mergeable()将包填充到上面分配到的描述符中。之后调用flush_shadow_used_ring(dev, vq)把vq->shadow_used_ring[x]数组中预取的描述符copy到vq->used->ring[x]中从vq->last_used_idx开始的位置,更新vq->last_used_idx += vq->shadow_used_idx, 调用rte_smp_wmb()写屏障之后 更新vq->used->idx += vq->shadow_used_idx,至此,guest os虚拟机就能看到新发送的报文了。如果driver没有禁止中断,即vq->avail->flags 上没有 VRING_AVAIL_F_NO_INTERRUPT 标识,则调用 eventfd_write(vq->callfd) 通知guest os driver收到了包。

学习地址: Dpdk/网络协议栈/vpp/OvS/DDos/NFV/虚拟化/高性能专家-学习视频教程-腾讯课堂
更多DPDK相关学习资料有需要的可以自行报名学习,免费订阅,久学习,或点击这里加qun免费
领取,关注我持续更新哦! !

copy_mbuf_to_desc_mergeable

desc_addr = gpa_to_vva(dev, vq->buf_vec[vec_idx].buf_addr);
rte_prefetch0((void *)(uintptr_t)desc_addr);virtio_hdr.num_buffers = res_end_idx - res_start_idx;
virtio_enqueue_offload(m, &virtio_hdr.hdr);
copy_virtio_net_hdr(vq, desc_addr, virtio_hdr);
vhost_log_write(dev, vq->buf_vec[vec_idx].buf_addr, vq->vhost_hlen);

所有分配到的描述符已经放在了vq->buf_vec[ ]中,所以这里直接从vq->buf_vec[ ]取描述符。见reserve_avail_buf_mergeable()。desc->addr是guest os driver填充的地址,它是guest os的物理地址即GPA。但是当前的代码运行在ovs中,所以需要将其转换为vhost地址空间虚拟地址即VVA。desc_addr = gpa_to_vva(dev, vq->buf_vec[vec_idx].buf_addr)。

更新virtio_net_hdr.num_buffers记录当前包所占用的描述符的个数。

virtio_enqueue_offload()用于初始化virtio_net_hdr中的offload相关域,如果m_buf->ol_flags上有PKT_TX_TCP/UDP/SCTP_CKSUM等标识位则更新virtio_net_hdr->flags/csum_start/csum_offset。如果m_buf->ol_flags上有PKT_TX_TCP_SEG标识位则更新virtio_net_hdr->gso_type/gso_size/hdr_len。

copy_virtio_net_hdr()将virtio_net_hdr写入描述符。

desc_avail  = vq->buf_vec[vec_idx].buf_len - vq->vhost_hlen;
desc_offset = vq->vhost_hlen;mbuf_avail  = rte_pktmbuf_data_len(m);
mbuf_offset = 0;
while (mbuf_avail != 0 || m->next != NULL) {/* done with current desc buf, get the next one */if (desc_avail == 0) {desc_idx = vq->buf_vec[vec_idx].desc_idx;if (!(vq->desc[desc_idx].flags & VRING_DESC_F_NEXT)) {/* Update used ring with desc information */used_idx = cur_idx++ & (vq->size - 1);vq->used->ring[used_idx].id  = desc_idx;vq->used->ring[used_idx].len = desc_offset;vhost_log_used_vring(dev, vq, offsetof(struct vring_used, ring[used_idx]), sizeof(vq->used->ring[used_idx]));}vec_idx++;desc_addr = gpa_to_vva(dev, vq->buf_vec[vec_idx].buf_addr);/* Prefetch buffer address. */rte_prefetch0((void *)(uintptr_t)desc_addr);desc_offset = 0;desc_avail  = vq->buf_vec[vec_idx].buf_len;}/* done with current mbuf, get the next one */if (mbuf_avail == 0) {m = m->next;mbuf_offset = 0;mbuf_avail  = rte_pktmbuf_data_len(m);}cpy_len = RTE_MIN(desc_avail, mbuf_avail);rte_memcpy((void *)((uintptr_t)(desc_addr + desc_offset)), rte_pktmbuf_mtod_offset(m, void *, mbuf_offset), cpy_len);vhost_log_write(dev, vq->buf_vec[vec_idx].buf_addr + desc_offset, cpy_len);mbuf_avail  -= cpy_len;mbuf_offset += cpy_len;desc_avail  -= cpy_len;desc_offset += cpy_len;
}used_idx = cur_idx & (vq->size - 1);
vq->used->ring[used_idx].id = vq->buf_vec[vec_idx].desc_idx;
vq->used->ring[used_idx].len = desc_offset;
vhost_log_used_vring(dev, vq, offsetof(struct vring_used, ring[used_idx]), sizeof(vq->used->ring[used_idx]));

mbuf 可能是一个链表,第一个mbuf -> pkt_len 是总的包长,每个mbuf->data_len表示这个mbuf内的数据长度,描述符也 可能是一个链表,每个desc->len表示这个描述符可存储的数据长度。因为所有描述符都已被记录在vq->buf_vec[ ]中,所以取描述符时只需依次从vq->buf_vec[ ]中取。

填充数据包时遍历每一个mbuf,将其mbuf->data_len指示的数据长度复制到描述符中。一个mbuf被写完时遍历到下一个mbuf->next。当一个描述符被用完时遍历到下一个描述符vq->buf_vec[++vec_idx]。当前描述符有VRING_DESC_F_NEXT标识时下一个描述符来自desc->next,当前描述符没有VRING_DESC_F_NEXT标识时下一个描述符来自available ring 中预留的下一个描述符。

一个由desc->next链接起来的描述符链表在available ring和used ring中只占据一个数组索引,被当做单一逻辑描述符,它是由guest os driver分配的。与之不同的是,virtio device用virtio_net_hdr->num_buffers串联起几个描述符(包括上面guest os driver分配的描述符链表)。前一种串联方式在guest os driver和virtio device两边都是默认支持的。而后一种串联方式只有在两边协商了VIRTIO_NET_F_MRG_RXBUF功能时才支持。

4.2,virtio_dev_rx

vq = dev->virtqueue[queue_id];count = reserve_avail_buf(vq, count, &res_start_idx, &res_end_idx);
rte_prefetch0(&vq->avail->ring[res_start_idx & (vq->size - 1)]);
for (i = 0; i < count; i++) {desc_indexes[i] = vq->avail->ring[(res_start_idx + i) & (vq->size - 1)];
}rte_prefetch0(&vq->desc[desc_indexes[0]]);
for (i = 0; i < count; i++) {err = copy_mbuf_to_desc(dev, vq, pkts[i], desc_idx, &copied);vq->used->ring[used_idx].id = desc_idx;if (unlikely(err)) { vq->used->ring[used_idx].len = vq->vhost_hlen; }else { vq->used->ring[used_idx].len = copied + vq->vhost_hlen; }vhost_log_used_vring(dev, vq, offsetof(struct vring_used, ring[used_idx]), sizeof(vq->used->ring[used_idx]));if (i + 1 < count) rte_prefetch0(&vq->desc[desc_indexes[i+1]]);
}rte_smp_wmb();while (unlikely(vq->last_used_idx != res_start_idx))rte_pause();*(volatile uint16_t *)&vq->used->idx += count;
vq->last_used_idx = res_end_idx;
vhost_log_used_vring(dev, vq, offsetof(struct vring_used, idx), sizeof(vq->used->idx));/* flush used->idx update before we read avail->flags. */
rte_mb();/* Kick the guest if necessary. */
if (!(vq->avail->flags & VRING_AVAIL_F_NO_INTERRUPT) && (vq->callfd >= 0))eventfd_write(vq->callfd, (eventfd_t)1);

这里整体的逻辑是:首先调用reserve_avail_buf()从available ring中预留empty buffer,同时记录预留区间 [res_start_idx, res_end_idx]。接着cache预取第一个描述符 rte_prefetch0()。调用copy_mbuf_to_desc()填充上面分到的描述符。将填充后的描述符放到used->ring[ ]中,注意这里放在used ring中的位置跟empty buffer的预留区间是一样的,即used->ring [res_start_idx, res_end_idx]。这也就是说一个描述符从available ring中移到used ring时其index索引位置是不变的。调用rte_smp_wmb()写屏障确保前面的乱序写已完成,再rte_pause() 等待vq->last_used_idx进入自己的预留区间,即等待vq->last_used_idx == res_start_idx,更新 vq->used->idx 和 vq->last_used_idx,调用rte_mb()内存屏障 在读取vq->avail->flags之前确保vq->used->idx已被更新到内存。最后如果driver没有禁止中断,即vq->avail->flags 上没有F_NO_INTERRUPT 标识,则调用 eventfd_write(vq->callfd) 通知guest os driver收到了包。

copy_mbuf_to_desc

desc_offset = vq->vhost_hlen;
desc_avail  = desc->len - vq->vhost_hlen;*copied = rte_pktmbuf_pkt_len(m);
mbuf_avail  = rte_pktmbuf_data_len(m);
mbuf_offset = 0;
while (mbuf_avail != 0 || m->next != NULL) {/* done with current mbuf, fetch next */if (mbuf_avail == 0) {m = m->next;mbuf_offset = 0;mbuf_avail  = rte_pktmbuf_data_len(m);}/* done with current desc buf, fetch next */if (desc_avail == 0) {if ((desc->flags & VRING_DESC_F_NEXT) == 0) { return -1; }if (unlikely(desc->next >= vq->size)) return -1;desc = &vq->desc[desc->next];desc_addr   = gpa_to_vva(dev, desc->addr);desc_offset = 0;desc_avail  = desc->len;}cpy_len = RTE_MIN(desc_avail, mbuf_avail);rte_memcpy((void *)((uintptr_t)(desc_addr + desc_offset)), rte_pktmbuf_mtod_offset(m, void *, mbuf_offset), cpy_len);vhost_log_write(dev, desc->addr + desc_offset, cpy_len);mbuf_avail  -= cpy_len;mbuf_offset += cpy_len;desc_avail  -= cpy_len;desc_offset += cpy_len;
}

mbuf 可能是一个链表,第一个mbuf -> pkt_len 是总的包长,每个mbuf->data_len表示这个mbuf内的数据长度,描述符也 可能是一个链表,每个desc->len表示这个描述符可存储的数据长度,填充数据包时遍历每一个mbuf,将其mbuf->data_len指示的数据长度复制到描述符中。一个mbuf被写完时遍历到下一个mbuf->next。当一个描述符被用完时遍历到下一个描述符desc->next。注意只有desc->flags 上有 VRING_DESC_F_NEXT标识时才能使用下一个描述符。

5,收包流程

这里的收包是指前端guest os driver发包,后端dpdkvhostuser收包。发包对上API是netdev_rxq_recv(rxq, batch),它实际上调用了netdev_dpdk_vhost_rxq_recv() -> rte_vhost_dequeue_burst():

if (unlikely(rte_atomic16_cmpset((volatile uint16_t *) & dev->broadcast_rarp.cnt, 1, 0))) {rarp_mbuf = rte_pktmbuf_alloc(mbuf_pool);make_rarp_packet(rarp_mbuf, &dev->mac);
}count = vq->avail->idx - vq->last_used_idx;/* Retrieve all of the head indexes first to avoid caching issues. */
for (i = 0; i < count; i++) { desc_indexes[i] = vq->avail->ring[(vq->last_used_idx + i) & (vq->size - 1)]; }/* Prefetch descriptor index. */
rte_prefetch0(&vq->desc[desc_indexes[0]]);
rte_prefetch0(&vq->used->ring[vq->last_used_idx & (vq->size - 1)]);for (i = 0; i < count; i++) {pkts[i] = rte_pktmbuf_alloc(mbuf_pool);copy_desc_to_mbuf(dev, vq, pkts[i], desc_indexes[i], mbuf_pool);used_idx = vq->last_used_idx++ & (vq->size - 1);vq->used->ring[used_idx].id  = desc_indexes[i];vq->used->ring[used_idx].len = 0;vhost_log_used_vring(dev, vq, offsetof(struct vring_used, ring[used_idx]), sizeof(vq->used->ring[used_idx]));
}rte_smp_wmb();
rte_smp_rmb();
vq->used->idx += i;
vhost_log_used_vring(dev, vq, offsetof(struct vring_used, idx), sizeof(vq->used->idx));/* Kick guest if required. */
if (!(vq->avail->flags & VRING_AVAIL_F_NO_INTERRUPT) && (vq->callfd >= 0)) eventfd_write(vq->callfd, (eventfd_t)1);

如果该报文是虚机迁移以来的第一个报文则需要构造一个RARP广播包放在pkts[0],用以刷新switch的mac learning table。vq->avail->idx指向了guest os driver发送的包的末尾,vq->last_avail_idx指向了上次virtio device已读取包的末尾,这两者之间的描述符就是guest os driver已发送但virtio device还未处理的包。而vq->last_used_idx指向了下一次归还用过的报文的位置。每次最多发送MAX_PKT_BURST个报文。把所有准备发送的报文的描述符copy到局部变量desc_indexes[x]数组中,同时把这些描述符copy到vq->used->ring[x]中vq->last_used_idx指向的开始位置。对于每一个需要接收的描述符,分配一个mbuf,调用copy_desc_to_mbuf()将描述符内的包内容copy到mbuf中。vq->last_used_idx是在used ring里还描述符开始的位置,现已还完发送的报文,所以将其移至下一次还描述符开始的位置。调用读写屏障后更新vq->used->idx,加上所还掉的描述符个数。如果driver没有禁止中断,即vq->avail->flags 上没有 VRING_AVAIL_F_NO_INTERRUPT 标识,则调用 eventfd_write(vq->callfd) 通知guest os driver返还了已用过的包缓冲区。

copy_desc_to_mbuf

desc = &vq->desc[desc_idx];
desc_addr = gpa_to_vva(dev, desc->addr);
rte_prefetch0((void *)(uintptr_t)desc_addr);/* Retrieve virtio net header */
hdr = (struct virtio_net_hdr *)((uintptr_t)desc_addr);
desc_avail  = desc->len - vq->vhost_hlen;
desc_offset = vq->vhost_hlen;mbuf_offset = 0;
mbuf_avail  = m->buf_len - RTE_PKTMBUF_HEADROOM;

desc->addr是guest os driver填充的地址,它是guest os的物理地址即GPA。但是当前的代码运行在ovs中,所以需要将其转换为vhost地址空间虚拟地址即VVA。desc_addr = gpa_to_vva(dev, desc->addr)。guest os driver发过来的包前面是一个virtio_net_hdr,之后再紧跟包的具体内容进行报文copy之前需要先跳过virtio_net_hdr。

while (desc_avail != 0 || (desc->flags & VRING_DESC_F_NEXT) != 0) {/* This desc reaches to its end, get the next one */if (desc_avail == 0) {desc = &vq->desc[desc->next];desc_addr = gpa_to_vva(dev, desc->addr);rte_prefetch0((void *)(uintptr_t)desc_addr);desc_offset = 0;desc_avail  = desc->len;}if (mbuf_avail == 0) {cur = rte_pktmbuf_alloc(mbuf_pool);prev->next = cur;prev->data_len = mbuf_offset;m->nb_segs += 1;m->pkt_len += mbuf_offset;prev = cur;mbuf_offset = 0;mbuf_avail  = cur->buf_len - RTE_PKTMBUF_HEADROOM;}cpy_len = RTE_MIN(desc_avail, mbuf_avail);rte_memcpy(mbuf_offset(cur, mbuf_offset), (desc_addr + desc_offset)), cpy_len);mbuf_avail  -= cpy_len;mbuf_offset += cpy_len;desc_avail  -= cpy_len;desc_offset += cpy_len;
}

遍历所有描述符,将描述符中的内容copy到mbuf中。当前描述符copy完时遍历到下一个描述符desc->next (前提是desc->flags 上有 VRING_DESC_F_NEXT标识)。当mbuf用完时再分配:rte_pktmbuf_alloc(mbuf_pool)。注意mbuf->data_len记录了当前段的长度,而mbuf->pkt_len记录了mbuf链表所有段的长度,mbuf->nb_segs记录了mbuf链表的段数。copy时每个mbuf前面预留RTE_PKTMBUF_HEADROOM的空间用于填充各种报文头。同属一个描述符的mbuf用mbuf->next串起来形成一个逻辑包。每新加一个mbuf需更新第一个mbuf的m->nb_segs和m->pkt_len。

mbuf->buf_addr是mbuf中数据开始的位置,而mbuf->buf_len是mbuf分配的内存空间的大小。注意区分: mbuf->pkt_len : 总的包长,包含mbuf链表内所有段的长度。mbuf->data_len : 当前mbuf段的长度,mbuf->buf_len : 当前mbuf段的实际分配内存长度。每个mbuf头部都保留了一个RTE_PKTMBUF_HEADROOM的长度

prev->data_len = mbuf_offset;
m->pkt_len    += mbuf_offset;if (hdr->flags != 0 || hdr->gso_type != VIRTIO_NET_HDR_GSO_NONE)vhost_dequeue_offload(hdr, m);

最后调用vhost_dequeue_offload()用描述符中的virtio_net_hdr初始化mbuf的offload相关域。如果需要csum卸载即virtio_net_hdr->flags上有VIRTIO_NET_HDR_F_NEEDS_CSUM标识则在m_buf->ol_flags上设置PKT_TX_TCP/UDP/SCTP_CKSUM等标识位。如果需要GSO卸载即virtio_net_hdr->gso_type上有VIRTIO_NET_HDR_GSO_TCPV4/6等标识,则在m_buf->ol_flags上设置PKT_TX_TCP_SEG标识位,同时更新 m->tso_segsz = hdr->gso_size,和m->l4_len。

6,zero copy

zero copy的功能由dev->dequeue_zero_copy控制开关,它是ovs调用rte_vhost_driver_register(path, flags)时传入的flags参数决定的。正常情况下vhost从vm收包时需要先分配一个mbuf,再把vm发送的报文从描述符指向的缓冲区copy到mbuf的缓冲区。这一步最多要copy 64K字节的报文,所以有很大的overhead,这是进行zero copy原始动力。zero copy则是在vhost从vm收包时直接将mbuf的缓冲区指针指向描述符里的的缓冲区,而不是进行报文copy。除了mbuf指向描述符内的缓冲区外,为了后续归还描述符,我们需要建立mbuf与描述符的关联关系,这是用一个struct zcopy_mbuf结构来表示的。这个结构体构造好之后被插入vq->zmbuf_list关联关系链表。每次vhost从vm收包时先遍历vq->zmbuf_list关联关系链表,检查链表里每个mbuf是否已被物理网卡发送完毕,若是则可以把与mbuf关联的描述符归还给vm了。

vq->zmbufs是在收到VHOST_USER_SET_VRING_NUM消息时分配的,总共vq->size个struct zcopy_mbuf。vq->zmbuf_size等于vq->size。struct zcopy_mbuf表示了一个mbuf与描述符的关联关系:mbuf:报文所在的mbuf,它在vhost从vm收包后承载报文直到发送到物理网卡。desc_idx:报文从vm发给vhost时所关联的描述符,这是vm分配的所以也必须还给vm。in_use:为1表示这个mbuf与描述符的关联关系结构体正在使用中,这是它会被插入vq->zmbuf_list中。为0表示这是一个空闲的结构体且不在vq->zmbuf_list中。vq->zmbuf_list是一个struct zcopy_mbuf的链表,只有正在使用的mbuf与描述符的关联关系结构体才会放在这个链表中。vq->nr_zmbuf表示了这个链表的长度。vq->zmbuf_list是一个struct zcopy_mbuf的链表,只有正在使用的mbuf与描述符的关联 关系结构体才会放在这个链表中。vq->nr_zmbuf表示了这个链表的长度。

6.1,关联mbuf与描述符

将mbuf内的缓冲区指针指向描述符内的缓冲区,这一步是vhost从vm收报文rte_vhost_dequeue_burst()并准备将报文从描述符copy到mbuf时进行的copy_desc_to_mbuf():

如果dev->dequeue_zero_copy开启则不调用rte_memcpy()进行报文copy,我们只是直接将mbuf内的缓冲区指针指向描述符内的缓冲区,这里需要注意的是描述符内的缓冲区由desc->addr表示,它是一个guest phys addr,我们把它放到mbuf->buf_addr时需要转换为vhost能访问的host user addr,因为这个报文最终还要发送到物理网卡,所以还要将其转换为物理网卡能访问的host phys addr并存入mbuf->buf_physaddr。与有copy时一个mbuf可以承载多个描述符的数据内容不同,zero copy时一个mbuf只能关联一个描述符(因为只有一个mbuf->addr),所以在mbuf->buf_addr指向desc->addr之后,把mbuf_avail设为cpy_len,这样在下一轮就需要重新分配一个mbuf了。

为mbuf与描述符的关联构建一个struct zcopy_mbuf结构并插入vq->zmbuf_list链表中,这一步是在mbuf指向描述符内的缓冲区之后进行的:

首先从vq->zmbufs数组中取一个zmbuf->in_use为0元素,zmbuf->mbuf指向表示报文的mbuf,zmbuf->desc_idx设置为表示报文的描述符然后将zmbuf插入有效关联关系链表vq->zmbuf_list,并递增vq->nr_zmbuf。这里因为有对mbuf的引用,所以需要调用rte_mbuf_refcnt_update()增加mbuf的引用计数。

6.2,回收描述符

当mbuf里的报文被物理网卡发送完毕后,与mbuf关联的描述符就可以归还给vm了。没有zero copy时,归还描述符的时间节点就是vhost从available ring取出报文copy到mbuf之后。有zero copy时,归还描述符的时间节点是在vhost从available ring取出报文之前(参考rte_vhost_dequeue_burst)。

遍历有效的mbuf描述符关联关系链表vq->zmbuf_list,检查其中的每个mbuf,如果其已被物理网卡发送完毕则其关联的描述符就可以归还给vm了。一个mbuf被物理网卡发送完毕的标识就是其引用计数为1。如果是一个mbuf chain则需chain内各个mbuf引用计数均为1。要归还的描述符即mbuf关联的描述符zmbuf->desc_idx,跟正常情况下一样,这个描述符也是要归还到vq->used->ring[x]中,归还的位置也是由vq->last_used_idx指定。 update_used_ring()就是将要归还的描述符写入used ring。描述符归还之后就可以撤销mbuf与描述符的关联关系了,这一步就是将struct zcopy_mbuf结构体从vq->zmbuf_list中移除。同时调用put_zmbuf(zmbuf)将zmbuf->in_use 设为0,并递减vq->nr_zmbuf。遍历完关联关系链表vq->zmbuf_list归还完所有描述符后就可以正式更新vq->used->idx以让vm看到归还的描述符了,这一步正是update_used_idx(dev, vq, nr_updated)所做的事。

7,参考

本文的源码分析基于DPDK 16.04

原文链接:

https://zhuanlan.zhihu.com/p/540997335

vhost-user相关推荐

  1. 解决Wamp 开启vhost localhost 提示 403 Forbbiden 的问题!

    非常奇怪的一个问题.我曾经从来都没有这样过!訪问 http://localhost/ 提示  403 Forbbiden. 我之前的设置一直都是这种: httpd.conf <Directory ...

  2. C#Project不生成.vhost.exe和.pdb文件的方法

    编译C#工程时,在C#的Project的属性界面的Build选项卡中当Configuration : Relese 时,依然会生成扩展名为.vhost.exe和.pdb文件. 其中.pdb是debug ...

  3. php配置默认index.php,Apache的vhost中配置默认访问入口index-test.php的方法(Yii)

    最近的参与的Yii项目有多个分支,所以在入口文件里面有区分(index.php index-test.php index-beta.php)等.不同的入口文件对应不同的环境和配置. 这个时候在本地建立 ...

  4. DPDK vhost库(十一)

    Vhost库实现了一个用户空间virtio网络服务器,允许用户直接操作virtio. 换句话说,它允许用户通过VM virtio网络设备获取/发送数据包. 为了达到这个功能,一个vhost库需要实现: ...

  5. KVM中virtio、vhost 和vhost-user比较(十一)

    virtio 在虚拟机中,可以通过qemu模拟e1000网卡,这样的经典网卡一般各种客户操作系统都会提供inbox驱动,所以从兼容性上来看,使用类似e1000的模拟网卡是非常一个不错的选择. 但是,e ...

  6. 【Apache】 配置 (http协议的) vhost

    前言 Apache 2.4.39 phpStudy 8.1.1.2 tomcat 9.0 的项目 准备 启用代理模块. 在 httpd.conf 配置文件中加载 Http 反向代理用到的模块 Load ...

  7. nginx: [emerg] duplicate “log_format“ name “main“ in /usr/local/phpstudy/vhost/sys/nginx/sys.conf:11

    前言 CentOS Linux release 8.2.2004 (Core) phpstdy X1.26 nginx1.15 配置nginx日志出错 nginx: [emerg] duplicate ...

  8. RTMP的URL/Vhost规则

    RTMP的url其实很简单,vhost其实也没有什么新的概念,但是对于没有使用过的同学来讲,还是很容易混淆.几乎每个新人都必问的问题:RTMP那个URL推流时应该填什么,什么是vhost,什么是app ...

  9. Rabbitmq~对Vhost的配置

    rabbitmq里有一些概念我们要清楚,如vhost,channel,exchange,queue等,而前段时间在部署rabbitmq环境时启用了虚拟主机vhost,感觉他主要是起到了消息隔离的作用, ...

  10. rabbitmq报错:PRECONDITION_FAILED - parameters for queue ‘test-1‘ in vhost ‘/‘ not equivalent

    错误如下: [root@master2 rabbitmq-python]# python send-1.py Traceback (most recent call last):File " ...

最新文章

  1. HTML5 Audio标签方法和函数API介绍
  2. 决策树 python 结果画图_scikit-learn决策树的python实现以及作图
  3. 【收集】Web开发工具
  4. 有勇气的牛排---微信小程序
  5. 32位oracle和64位的区别,区分你的oracle是64位还是32位
  6. 5分钟,带你领略项目经理十年的功力
  7. 【原】Unity3D 窗口裁剪
  8. 26个提升java性能需要注意的地方
  9. C++Primer学习之一引用和指针
  10. 蓝色大巴汽车网站404页面源码
  11. createbitmap 旋转90度_解决某些机型调用系统相机照片旋转的问题
  12. python在哪里写代码-在哪里编写python代码
  13. 【CCCC】L2-002 链表去重 (25分),,把一个链表拆成两个
  14. Python数据结构1-----基本数据结构和collections系列
  15. jQuery 样式操作
  16. 未来15年,还有一波“增量”机会
  17. SpringCloudAlibaba之Nacos
  18. strstr函数用法小结
  19. java的基本数据类型有哪些
  20. 人工智能学习梳理和总结

热门文章

  1. 红米k40开启呼叫转移方法介绍
  2. 手机进水开机android,手机进水后开机只有启动画面进不了桌面怎么处理?
  3. Tomcat-Connector(连接器)
  4. 每个程序员都应该了解的 CPU 高速缓存
  5. 进程间通信 [3] —— 信号SIGNAL、信号量SEMAPHORE
  6. 企业微信hook接口,朋友圈功能开发教程,逆向开发,企业微信营销开发
  7. eclipse IDE
  8. 给SwipeRefreshLayout添加上拉加载更多功能
  9. JAVA编程习题及答案_完美版
  10. 软考高级 真题 2012年上半年 信息系统项目管理师 论文