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_addr
page->host_phys_addr = host_phys_addr
page->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