vhost-net-原理-初始化流程-数据传输流程-vhost-net后端

1.vhost net

传统的virtio网卡是通过虚拟机内部的virtio驱动作为前端,负责将虚拟机内部的IO请求封装到vring descriptor中,然后通过写MMIO或PIO的方式通知QEMU中的virtio后端设备,QEMU将这些IO请求设备发送到tap设备,然后通过网桥发送到真实的网卡上
vhost方案也是通过虚拟机中的virtio驱动将IO请求封装在vring descriptor中,但vhsot是由宿主内核中的vhost模块作为virtio后端,vhost在收到虚拟机的通知后会直接在宿主机内核中与tap设备通信,从而完成数据的收发

vhost-net框架图
在这里插入图片描述
在这里插入图片描述
vhost-net 是一个内核驱动程序,它实现了 vhost 协议的处理程序端,以实现高效的数据平面,即数据包转发。
qemu 和 vhost-net 内核驱动程序(处理程序)使用 ioctl 来交换 vhost 消息,并使用几个名为 irqfd 和 ioeventfd 的类似 eventfd 的文件描述符来与guest交换通知。
当加载 vhost-net 内核驱动程序时,它会在/dev/vhost-net 上公开一个字符设备。
当 qemu 在 vhost-net 支持下启动时,它会打开它并使用多个 ioctl(2) 调用来初始化 vhost-net 实例。
这些是将虚拟机管理程序进程与 vhost-net 实例关联、准备 virtio 功能协商并将guest物理内存映射传递给 vhost-net 驱动程序所必需的。
在初始化期间,vhost-net 内核驱动程序创建一个名为 vhost-$pid 的内核线程,其中 p i d 是管理程序进程 p i d 。该线程称为“ v h o s t 工作线程”。 T a p 设备仍然用于 V M 与主机的通信,但现在工作线程处理 I / O 事件,即轮询驱动程序通知或 T a p 事件,并转发数据。 Q e m u 分配一个 e v e n t f d 并将其注册到 v h o s t 和 K V M 以实现通知绕过。 v h o s t − pid 是管理程序进程 pid。该线程称为“vhost 工作线程”。 Tap 设备仍然用于 VM 与主机的通信,但现在工作线程处理 I/O 事件,即轮询驱动程序通知或 Tap 事件,并转发数据。 Qemu分配一个eventfd并将其注册到vhost和KVM以实现通知绕过。vhost- pid是管理程序进程pid。该线程称为vhost工作线程Tap设备仍然用于VM与主机的通信,但现在工作线程处理I/O事件,即轮询驱动程序通知或Tap事件,并转发数据。Qemu分配一个eventfd并将其注册到vhostKVM以实现通知绕过。vhostpid 内核线程轮询它,当 guest 写入特定地址时,KVM 写入它。这个机制被命名为ioeventfd。
这样,对特定客户内存地址的简单读/写操作不需要经过昂贵的 QEMU 进程唤醒,可以直接路由到 vhost 工作线程。这还有一个优点是异步的,不需要 vCPU 停止(因此不需要立即进行上下文切换)。
另一方面,qemu分配另一个eventfd并再次将其注册到KVM和vhost,以进行直接vCPU中断注入。这种机制称为irqfd,它允许主机中的任何进程通过写入来向客户机注入 vCPU 中断,具有相同的优点(异步、不需要立即上下文切换等)。
请注意,virtio 数据包处理后端中的此类更改对于仍然使用标准 virtio 接口的guest来说是完全透明的。
vhost-net内核模块层次图
在这里插入图片描述

struct vhost_net:用于描述Vhost-Net设备。它包含几个关键字段:
1)struct vhost_dev,通用的vhost设备,可以类比struct device结构体内嵌在其他特定设备的结构体中;
2)struct vhost_net_virtqueue,实际上对struct vhost_virtqueue进行了封装,用于网络包的数据传输;
3)struct vhost_poll,用于socket的poll,以便在数据包接收与发送时进行任务调度;
struct vhost_dev:描述通用的vhost设备,可内嵌在基于vhost机制的其他设备结构体中,比如struct vhost_net,struct vhost_scsi等。关键字段如下:
1)vqs指针,指向已经分配好的struct vhost_virtqueue,对应数据传输;
2)work_list,任务链表,用于放置需要在vhost_worker内核线程上执行的任务;
3)worker,用于指向创建的内核线程,执行任务列表中的任务;
struct vhost_virtqueue:用于描述设备对应的virtqueue,这部分内容可以参考之前virtqueue机制分析,本质上是将Qemu中virtqueue处理机制下沉到了Kernel中。关键字段下:
1)struct vhost_poll,用于poll eventfd对应的文件,当不满足处理请求时会添加到eventfd对应的等待队列中,
而一旦被唤醒,该结构体中的struct vhost_work(执行函数被初始化为handle_tx_kick,以发送为例)将被放置到内核线程中去执行; 结构体的核心围绕着数据和通知机制,其中数据在vhost_virtqueue中体现,而通知主要是通过vhost_poll来实现,具体的细节下文将进一步描述。

2.vhost-net的初始化流程

net_init_tap        //qemu-8.0.2\net\tap.c
    net_init_tap_one
        vhost_net_init    //qemu-8.0.2\net\vhost-net.c
            vhost_dev_init    //qemu-8.0.2\net\vhost.c

在这里插入图片描述

Qemu中tap设备初始化在net_init_tap中完成,其中net_init_tap_one打开vhost-net设备文件,通过ioctl与内核的vhost-net交互;
vhost_set_backend_type设置vhost的后端类型,以及vhost的操作函数集。目前有两种vhost后端,一种是在内核态实现的virtio后端,一种是在用户态中实现的virtio后端;
kernel_ops:vhost的内核操作函数集,都是一些回调函数的实现,
最终会通过vhost_kernel_call–>ioctl–>vhost-net.ko路径,进行配置;
ioctl系统调用,与驱动交互简单来说可以分为三大类,下边分别介绍几个关键的设置:
在这里插入图片描述

vhost net设置

VHOST_SET_OWNER:底层会为调用者创建一个内核线程,对应到前文中数据结构中的vhost_worker,同时在vhost_dev结构体中还会保存调用者线程的内存空间数据结构;
VHOST_NET_SET_BACKEND设置vhost-net的后端设备,比如Qemu往内核态传递的tap设备对应的fd,从而让vhost-net直接与tap设备进行通信;

vhost dev设置

从Guest OS中的虚拟地址到最终的Host上的物理地址映射关系如上图所示,如果在Guest OS中要将数据发送出去,实际上只需要将Qemu中关于Guest OS的物理地址布局信息传递下去,此外再结合VHOST_SET_OWNER时传递的内存空间信息,就可以根据映射关系找到Guest OS中的数据对应到Host之上的物理地址,完成最后搬运即可;
VHOST_SET_MEM_TABLE将Qemu中的虚拟机物理地址布局信息传递给内核:
在这里插入图片描述

vhost vring设置

VHOST_SET_VRING_KICK设置vhost-net模块前端virtio驱动发送通知时触发的eventfd,通知机制,最终触发handle_kick函数的执行;
VHOST_SET_VRING_CALL设置vhost-net后端到虚拟机virtio前端的中断通知,参考之前文章中的irqfd机制;
此外关于vring的设备还包括vring的大小,地址信息等;
上述的这些设置的流程路径如下,关键路径:
在这里插入图片描述

当Guest OS中的virtio-net驱动完成初始化后,会通过vp_set_status来设置状态,以通知后端驱动已经ready,此时会触发VM的退出并进入KVM进行异常处理,最终路由给Qemu;
Qemu中的vcpu线程监测异常,当检测到KVM_EXIT_MMIO时,去回调注册该IO区域的读写函数,比如virtio_pci_common_write函数,在该函数中逐级往下最终调用到vhost_net_start函数;
在vhost_net_start中最终去通过kernel_ops函数集去设置底层并交互;初始化完成后,接下来让我们看看数据的发送与接收,为了能将整个流程表达清楚,我会将完整的图拆分成几个步骤来讲述。

3.数据收发流程分析

3.1 数据发送

vhost-net发送缓冲区图流程
在这里插入图片描述

1)
发送前的框图如下:
在这里插入图片描述

Guest OS中的virtio-net驱动中维护两个virtqueue,分别用于发送和接收;
图中的datagram表示的是需要发送的数据;
KVM模块提供了ioeventfd和irqfd用于通知机制;
vhost-net模块中创建好了vhost_worker内核线程,用于处理任务;
2)

在这里插入图片描述

当数据包准备好之后,通过往kick fd上触发信号,从而唤醒vhost_worker内核线程来调用handle_tx_kick进行数据的发送;
当Tap/Tun不具备发送条件时,vhost_worker会poll在socket上,等待Tap/Tun的唤醒,一旦被唤醒后可以调用handle_tx_net发送;
最终的handle_tx完成具体的发送;
3)
在这里插入图片描述

vhost_get_vq_desc函数在vritqueue中查找可用的buffer,并将信息存储到iov中,以便更好的访问;
sock->ops->sendmsg()函数,实际调用的是tun_sendmsg函数,在该函数中分配了skb结构体,并将iov[]中的信息传递过来,最终如图中所示完成数据的拷贝和发送,通过NIC发送出去;
4)
在这里插入图片描述

数据发送完毕后,通过irqfd机制通知vcpu;

3.2 数据接收

数据的接收是发送的逆过程,流程一致:
1)
在这里插入图片描述

初始化部分与发送过程一致;
Tap/Tun驱动从NIC接收到数据包,准备发送给vhost-net;
在这里插入图片描述

vhost-net中的vhost_worker线程也poll在两个fd之上,与发送端类似;
kick fd上触发信号时最终调用handle_rx_kick函数,Tap/Tun对应的socket上触发信号时,调用handle_rx_net函数;
最终通过handle_rx来完成实际的接收;
3)
在这里插入图片描述

接收过程中,vhost_get_vq_desc获取virtqueue中的可用buffer,并将信息存储到iov[]中;
sock->ops->recvmsg()函数实际指向tun_recvmsg函数,在该函数中最终完成数据的传递;
4)
在这里插入图片描述

数据接收完成后,通过irqfd机制通过vcpu,从而在Guest OS中进行处理;

4ioventfd和irqfd的通知机制

在这里插入图片描述

irqfd:提供一种机制,可以通过文件描述fd来向Guest注入中断,路径为紫色线条所示;
ioeventfd:提供一种机制,可以通过文件描述符fd来接收Guest的信号,路径为红色线条所示;
eventfd和irqfd这两种机制,都是基于eventfd来实现的;
irqfd是kvm\qemu通知虚拟机内部系统的一种快捷通道
ioeventfd是虚拟机内部操作系统通知kvm\qemu的一种快捷通道

4.1ioeventfd

qemu可以为虚拟机特定的地址关联一个eventfd,并对该eventfd进行事件监听,然后调用ioctl(KVM_IOEVENTFD)向KVM注册这段地址,当虚拟机内部因为IO发生VMExit时,KVM可以判断其地址是否有对应的eventfd,如果有就直接调用eventfd_signal发送信号到对应的fd,这样QEMU就能够从其事件监听循环返回,进而进行处理`

qemu侧

ioeventfd工作流程如下:
virtio_ioport_write
  virtio_pci_start_ioeventfd
    virtio_bus_start_ioeventfd              //qemu-8.0.2\hw\virtio\virtio-bus.c
      memory_region_add_eventfd            //qemu-8.0.2\softmmu\memory.c
        memory_region_transaction_commit
                    address_space_set_flatview
                        address_space_update_topology_pass
           address_space_update_ioeventfds
              address_space_add_del_ioeventfds
                kvm_init->  eventfd_add=kvm_mem_ioeventfd_add //qemu-8.0.2\accel\kvm\kvm-all.c
                  kvm_set_ioeventfd_mmio
                    kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &iofd);//向KVM注册ioeventfd

在这里插入图片描述

内存区域MemoryRegion中的ioeventfds成员按照地址从小到大排序,memory_region_add_eventfd函数会选择合适的位置将ioeventfds插入,并提交更新;
提交更新过程中最终触发回调函数kvm_mem_ioeventfd_add的执行,这个函数指针的初始化是在Qemu进行kvm_init时,针对不同类型的内存区域注册了对应的memory_listener用于监听变化;
kvm_vm_ioctl:向KVM注册ioeventfd;Qemu中完成了初始化后,任务就转移到了KVM中。

kvm侧

在这里插入图片描述

KVM中注册ioeventfd的核心函数为kvm_assign_ioeventfd_idx,该函数中主要工作包括:
1)根据用户空间传递过来的fd获取到内核中对应的struct eventfd_ctx结构体上下文;
2)使用ioeventfd_ops操作函数集来初始化IO设备操作;
3)向KVM注册IO总线,比如KVM_MMIO_BUS,注册了一段IO地址区域,当操作这段区域的时候出发对应的操作函数回调;
当Guest OS中进行IO操作时,触发VM异常退出,KVM进行捕获处理,最终调用注册的ioevnetfd_write
在该函数中调用eventfd_signal唤醒阻塞在eventfd上的任务,Qemu和KVM完成了闭环;

总体效果

在这里插入图片描述

4.2irqfd

qemu侧

virtio_pci_set_guest_notifiers   //qemu-8.0.2\hw\virtio\virtio-pci.c
    kvm_virtio_pci_vector_vq_use
        kvm_virtio_pci_vector_use_one
            kvm_virtio_pci_irqfd_use
                kvm_irqchip_add_irqfd_notifier_gsi
                    kvm_irqchip_assign_irqfd    //qemu-8.0.2accel\kvm\kvm-all.c
                        kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);  //向kvm发起ioctl请求

kvm侧

linux-5.16\virt\kvm\eventfd.c
kvm_irqfd
    kvm_irqfd_assign

在这里插入图片描述

Qemu中通过kvm_irqchip_assign_irqfd向KVM申请注册irqfd;
在KVM中,内核通过维护struct kvm_kernel_irqfd结构体来管理整个irqfd的流程;
kvm_irqfd_assign:
1)分配struct kvm_kernel_irqfd结构体,并进行各个字段的初始化;
2)初始化工作队列任务,设置成irqfd_inject,用于向Guest OS注入虚拟中断;
3)初始化等待队列的唤醒函数,设置成irqfd_wakeup,当任务被唤醒时执行,在该函数中会去调度工作任务irqfd_inject;
4)初始化pll_table pt字段的处理函数,设置成irqfd_ptable_queue_proc,该函数实际是调用add_wait_queue将任务添加至eventfd的等待队列中,这个过程是在vfs_poll中完成的;
当Qemu通过irqfd机制发送信号时,将唤醒睡眠在eventfd上的任务,唤醒后执行irqfd_wakeup函数,在该函数中调度任务,调用irqfd_inject来注入中断;

总体效果

在这里插入图片描述

qemu进行tap设备初始化
qemu8.0.2/net/tap.c

net_init_tap-->
    net_init_tap_one-->
        vhost_net_init(&options)-->
            vhost_net_get_fd(options->net_backend)
            vhost_dev_init-->
                vhost_set_backend_type-->
                    dev->vhost_ops = &kernel_ops;#在dev/host-net所在fd上调用ioctl,从而实现在host中的vhost-net模块进行交互
            vhost_net_ack_features

linux-5.16\drivers\vhost\net.c

vhost_net_init-->
    misc_register(&vhost_net_misc)-->
        .fops = &vhost_net_fops-->
            .open = vhost_net_open-->#用户态程序每次打开/dev/vhost-net都会调用该函数
            vhost_net_open-->        #初始化一个vhost_net结构体及其成员vhost_dev和收发包队列vhost_virtqueue;
                                     #同时将vhost_net设置为open打开的file结构体的private_data;f->private_data = n;
                vhost_dev_init-->
                        vhost_poll_init-->#对vhost_virqueue中的vhost_poll成员进行初始化
                            init_waitqueue_func_entry(vhost_poll_wakeup)-->
                                vhost_poll_wakeup-->                                
                vhost_poll_init(handle_tx_net)-->  
                    handle_tx_net  

参考:

https://www.redhat.com/en/blog/deep-dive-virtio-networking-and-vhost-net
https://www.redhat.com/en/blog/introduction-virtio-networking-and-vhost-net
https://www.cnblogs.com/LoyenWang/p/14399642.html
https://cloud.tencent.com/developer/article/1075600
https://blog.csdn.net/huang987246510/article/details/121046398
https://blog.csdn.net/qq_41596356/article/details/128538073

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yengi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值