版本说明


qemu version:2.6.0
kernel version:3.10.102

简而言之


  • vhost模块需要提前加载,注册一个misc的设备,供虚拟机启动时候使用。
  • 虚拟机创建的时候,会初始化一个tap设备,然后启动一个vhost_$(qemu-kvm_pid)的线程,配置vring等承载数据的队列。
  • guest和host进行网络数据IO的时候,只负责数据IO的队列,中断消息等由kvm模块负责。
  • guest发包的时候,virtio模块负责发送将数据包加入队列,然后通知kvm模块,kvm模块通过ioeventfd通知vhost模块,此时会将有包的队列挂在work_list上,然后激活线程vhost进行收包操作,收到包之后传递给tap设备,再往内核协议栈中上传。
  • guest收包的时候,首先是vhost的往tap设备发包,然后将包加入到其中一个队列,然后将挂在work_list,激活线程vhost,vhost进行收包的操作,然后传递到队列,然后vhost通过irqfd通知kvm模块,kvm模块给guest发送中断,guest会通过中断,到NAPI,执行poll,收取数据包,然后上传到协议栈。

以上的流程如下:
请输入图片描述

创建网络设备


host qemu层

qemu-kvm启动虚拟机的时候,在命令行的-netdev tap,...中指定vhost=on选项,初始化设备的同时,会创建vhost的内核线程。
vl.c中main-->net_init_clients-->net_init_netdev-->net_client_init-->net_client_init1-->net_client_init_fun[]-->net_init_tap初始化网口。

net_init_tap-->net_init_tap_one

  • 调用open("/dev/vhost-net", O_RDWR),会调用到vhost_net_open
  • vhost_net_init-->vhost_dev_init-->vhost_set_backend_type设置为kernel模式。
  • vhost_net_init-->vhost_dev_init-->vhost_kernel_set_owner-->vhost_kernel_call最终调用ioctl设置owner,这个时候会启动vhost_$(qemu_pid)的进程。
  • vhost_net_init-->vhost_dev_init-->vhost_kernel_get_features最终调用到vhost的vhost_net_ioctl获取vhost的features存入vdev->features
  • vhost_net_init-->vhost_dev_init-->vhost_virtqueue_init-->vhost_kernel_set_vring_call调用ioctl。

device_set_realized-->virtio_device_realize-->virtio_bus_device_plugged-->virtio_net_get_features-->vhost_net_get_features会将vdev->features赋给vdev->host_features

host vhost模块

vhost_net_open

  • 创建vhost_net的数据结构。
  • 为vhost申请virtqueue,rx和tx两个队列,并且初始化。
  • 调用vhost_dev_init初始化虚拟设备。初始化数据结构,然后给设备挂上handle_tx_kickhandle_rx_kick两个worker。
  • vhost_net挂上handle_tx_nethandle_rx_net两个worker。
  • vhost_net记录在file数据结构的private_data指针。
  • vhost_net_ioctl(VHOST_SET_OWNER)
  • vhost_net_set_owner-->vhost_dev_set_owner调用kthread_create创建vhost_worker进程

配置通信方式


host qemu层

  • main-->configure_accelerator-->accel_init_machine-->kvm_init注册kvm_io_listener,其中包括kvm_io_ioeventfd_add接口
  • kvm_io_ioeventfd_add-->kvm_set_ioeventfd_pio-->kvm_vm_ioctl配置KVM_IOEVENTFD,这里会调用ioctl对vhost模块进行配置

host kvm模块

kvm_vm_fops-->kvm_vm_ioctl-->kvm_ioeventfd-->kvm_assign_ioeventfd

kvm_assign_ioeventfd

  • kvm_iodevice_init添加ioeventfd_ops->dev->opsioeventfd_ops中主要关注ioeventfd_write
  • kvm_io_bus_register_dev主要是讲_ioeventfd->dev记录到kvm->buses[id]-->dev中。
  • kvm->ioeventfds添加到_ioeventfd的链表中。

guest virtio模块

virtio_dev_probe-->vp_get_features调用到qemu的virtio_pci_config_read-->virtio_ioport_read获取到vdev->host_features
virtio_init注册总线,完成之后会在/sys/bus下创建virtio目录,内含devices和drivers两个目录,后续会记录总线支持的设备和驱动。
module_pci_driver为virtio-pci设备驱动加载,完成之后会分别在/sys/bus/pci/devices和/sys/devices/下创建virtio-pci的目录。
module_virtio_driver加载virtio-net设备驱动。

virtnet_probe

  • 获取host是否支持多队列的virtio-net设备。
  • 生成一个网络设备并且进行默认设置。
  • 检测一些特性,比如硬件csum,GSO等等feature,设置mac。
  • init_vqs进行队列的初始化工作。
  • 设置dev真实rx、tx队列为1,注册设备。
  • 创建接收buffers,注册CPU通知,虚拟网卡启动加电。

init_vqs调用virtnet_alloc_queues函数申请receive_queue、send_queue,记录到virtnet_info中,并且给receive_queue添加NAPI的支持,挂上函数。
virtnet_poll调用virtnet_find_vqs-->vp_find_vqs给virtqueue配置中断。创建vring,并且初始化。
vp_find_vqs调用vp_try_to_find_vqs函数给virtqueue配置中断,最优为msi-x中断,每个队列一个,次之的是共享中断,最次的是int-x的普通中断。

vp_try_to_find_vqs

  • msi-x中断时,当队列和CPU一致时,配置CPU亲和性。
  • 调用setup_vq,给设置每个virtqueue。
  • 调用request_irq申请中断,三种方式如下

    • 使用msi-x中断,有n+1个vector,一个用来对应change中断,n个分别对应n个队列的vq中断。change中断处理函数vp_config_changed,vq中断处理函数:vring_interrupt
    • 使用msi-x中断,有2两个vector,一个用来对应change中断,一个对应所有队列的vq中断。change中断处理函数vp_config_changed,vq中断处理函数vring_interrupt
    • 普通int-x中断,change中断和所有vq的中断公用一个中断irq。中断处理函数vp_interrupt

setup_vq

  • 创建virtio_pci_vq_info,获取队列数量,申请空间给virtio_pci_vq_info->queue使用,并且激活队列通告qemu。
  • vring_new_virtqueue创建结构vring_virtqueue,并且对vring_virtqueue进行初始化。
  • 将创建的vring_virtqueue挂在virtio_pci_vq_info->vq上。

vring_new_virtqueue

  • 创建vring_virtqueue,并初始化其结构下的vring,初始化内容包括队列数量、page地址、vring对齐字节,这些数据会计算vring的avail和used两个变量,avail表示guset端可用,used表示host端已经占用。
  • vring_virtqueue的结构下的virtqueue挂上回调函数skb_recv_doneskb_xmit_done,分别是在接收、发送完成之后调用。
  • 在上面的结构上挂上通知函数vp_notify等信息。
  • 最后将virtqueue加入到vdev设备。

虚拟机发包


虚拟机中使用pktgen发包,pktgen直接调用netdev的ndo_start_xmit,如果是应用层发包,需要走socket和内核路径。

guest virtio模块

virtnet_netdev.ndo_start_xmit = start_xmit

  • 调用xmit_skb,该函数计算csum,gso标识记录,最后调用virtqueue_add_outbuf加入virtnet_info->send_queue对应的virtqueue。
  • virtqueue_kick-->virtqueue_notify-->vp_notify发一个VIRTIO_PCI_QUEUE_NOTIFY通知host kernel,并告知队列vq的index。

host kvm模块

guest virtio模块notify可能导致调用该接口vmx_handle_exit(vcpu_enter_guest)

vmx_handle_exit-->kvm_vmx_exit_handlers[exit_reason]-->handle_io-->kvm_fast_pio_out-->emulator_pio_out_emulated-->emulator_pio_in_out-->kernel_pio-->kvm_io_bus_write-->kvm_iodevice_write(dev->ops->write)-->ioeventfd_write-->eventfd_signal-->wake_up_locked_poll-->__wake_up_locked_key-->__wake_up_common-->vhost_poll_wakeup-->vhost_poll_queue-->vhost_work_queue-->wake_up_process

以上操作最终会唤醒vhost_worker。

host vhost模块

vhost_worker线程是一个死循环

  • 设置当前状态为TASK_INTERRUPTIBLE。
  • 加自旋锁,判断设备的work_list是为空,不为空则获取第一个worker,然后从worker_list中删除,解锁。
  • 如果worker获取到了,则设置线程状态为TASK_RUNNING,调用worker对应的功能模块。
  • 没有则调用schedule进行进程切换。

vhost_worker-->handle_tx_kick-->handle_tx(sock->ops->sendmsg)-->tun_sendmsg-->tun_get_user(内部的tun_alloc_skb?)-->netif_rx_ni(netif_rx没看到多占cpu)-->do_softirq-->call_softirq-->__do_softirq-->net_rx_action-->process_backlog-->__netif_receive_skb-->__netif_receive_skb_core-->netdev_frame_hook-->netdev_port_receive-->ovs_vport_receive-->ovs_dp_process_packet

虚拟机收包


pktgen直接从虚拟机外部的tap设备发包,直接调用tap的ndo_start_xmit,即tun_net_xmit

host pktgen线程

tun_net_xmit-->vhost_poll_wakeup-->vhost_poll_queue-->vhost_work_queue-->wake_up_process会唤醒vhost_worker

tun_net_xmit

  • 调用skb_queue_tail,将skb添加到socket的sk_receive_queue
  • wake_up_interruptible_poll应该会唤醒vhost_poll_wakeup但是不是特别确定,可以参考这个路径vhost_net_set_backend-->vhost_net_enable_vq-->vhost_poll_start(file-->f_op-->poll)-->tun_chr_poll-->poll_wait

host vhost模块:

vhost_worker-->handle_rx_kick-->handle_rx-->tun_recvmsg&vhost_add_used_and_signal_n-->vhost_signal-->eventfd_signal-->wake_up_locked_poll-->irqfd_wakeup-->kvm_set_msi-->kvm_irq_delivery_to_apic-->kvm_irq_delivery_to_apic_fast-->kvm_apic_set_irq-->__apic_accept_irq-->kvm_vcpu_kick这个函数的功能就是,判断vcpu是否正在物理cpu上运行,如果是,则让vcpu退出,以便进行中断注入。

host kvm模块:

  • vcpu_enter_guest-->inject_pending_event-->vmx_inject_irq调用vmx_write32()写VMCS的IRQ寄存器。
  • vcpu_enter_guest-->vmx_vcpu_run在执行前,会读取VMCS中的寄存器信息,由于前面写了IRQ,所以vcpu运行就会知道有中断请求到来,再调用GuestOS的中断处理函数处理中断请求。

guest virtio模块:

vp_interrupt-->vp_config_changed & vp_vring_interrupt-->vring_interrupt-->__napi_schedule-->virtnet_poll-->receive_buf-->netif_receive_skb-->__netif_receive_skb-->__netif_receive_skb_core(rx_handler-->br_handle_frame)-->deliver_skb-->ip_rcv-->ip_rcv_finish-->ip_local_deliver-->ip_local_deliver_finish-->udp_rcv-->__udp4_lib_rcv

UDP

  • __udp4_lib_rcv找到匹配的sock,根据skb的net、四元组和入端口信息,sock会记录连接的状态。
  • __udp4_lib_rcv-->udp_queue_rcv_skb-->__udp_queue_rcv_skb-->sock_queue_rcv_skb添加到sk_receive_queue队列中。

TCP

  • tcp_v4_rcv找到匹配的sock,根据skb的net、四元组和入端口信息,sock会记录连接的状态。
  • tcp_v4_do_rcv-->tcp_rcv_established-->tcp_queue_rcv添加到sk_receive_queue队列中。

VXLAN封装


kernel模块

netdev_frame_hook-->netdev_port_receive-->ovs_vport_receive-->ovs_dp_process_received_packet-->ovs_execute_actions-->do_execute_actions-->do_output-->ovs_vport_send-->vxlan_tnl_send

vxlan_tnl_send

  • 根据vxlan tunnel的ip查找路由。
  • 调用vxlan_xmit_skb封装发送报文。

vxlan_xmit_skb

  • 计算封装vxlan需要的最小空间,并且扩展头部空间。
  • 添加vxlan头。
  • 如果有BGP的头,也添加。
  • udp_tunnel_xmit_skb添加协议头发送。

udp_tunnel_xmit_skb

  • 添加UDP协议头。
  • iptunnel_xmit继续添加协议头,并且发送。

iptunnel_xmit

  • 添加ip协议头。
  • ip_local_out_sk-->__ip_local_out-->__ip_local_out_sk继续添加协议头,并且发送。

__ip_local_out_sk

  • 过netfilter的LOCAL_OUT。
  • 调用dst_output_sk-->ip_output

ip_output

  • 过netfilter的POST_ROUTING。
  • 调用ip_finish_output

ip_finish_output

  • 如果报文支持gso,调用ip_finish_output_gso进行分片。
  • 如果报文大于mtu,调用ip_fragment进行分片。
  • 调用ip_finish_output2进行报文发送。

ip_finish_output2

  • __ipv4_neigh_lookup_noref查找邻居子系统。
  • 调用dst_neigh_output-->neigh_hh_output进行报文发送。

neigh_hh_output

  • 封装2层协议头。
  • 调用dev_queue_xmit进行报文发送。

vhost_worker-->handle_tx_kick-->handle_tx(sock->ops->sendmsg)-->tun_sendmsg-->tun_get_user(内部的tun_alloc_skb?)-->netif_rx_ni(netif_rx没看到多占cpu)-->do_softirq-->call_softirq-->__do_softirq-->net_rx_action-->process_backlog-->__netif_receive_skb-->__netif_receive_skb_core-->netdev_frame_hook-->netdev_port_receive-->ovs_vport_receive-->ovs_dp_process_packet-->ovs_execute_actions-->do_execute_actions-->do_output-->ovs_vport_send-->vxlan_tnl_send-->vxlan_xmit_skb-->udp_tunnel_xmit_skb-->iptunnel_xmit-->ip_local_out_sk-->__ip_local_out-->__ip_local_out_sk-->dst_output_sk-->ip_output-->ip_finish_output-->ip_finish_output2-->neigh_hh_output-->dev_queue_xmit-->__dev_xmit_skb-->sch_direct_xmit-->dev_hard_start_xmit-->xmit_one-->netdev_start_xmit-->__netdev_start_xmit-->bond_start_xmit-->__bond_start_xmit-->bond_3ad_xor_xmit-->bond_dev_queue_xmit-->dev_queue_xmit-->__dev_xmit_skb-->sch_direct_xmit-->dev_hard_start_xmit-->xmit_one-->netdev_start_xmit-->__netdev_start_xmit-->ixgbe_xmit_frame

以上是host kernel的整个调用路径,如果需要排查瓶颈,可以定义以下时间记录点

  • netif_rx_ni第一个记录点,vhost的获取到skb之后,第一个接口。
  • __netif_receive_skb第二个记录点,这里是过完中断之后,这个点函数没有接口。
  • ovs_vport_receive第三个记录点,查表之前。
  • ovs_dp_process_packet第四个记录点,查表之后。
  • ovs_vport_send第五个记录点,action执行时间。
  • vxlan_tnl_send第六个记录点,vxlan开始时间。
  • ip_local_out_sk第七个记录点,vxlan封装完毕时间。
  • ip_output第八个记录点,过完LOCAL_OUT的时间,这个挂不上jprobe,所以记录netfilter POSTROUTING第一个点的时间。
  • ip_finish_output第九个记录点,过完POSTROUTING的时间,这个也挂不上jprobe,所以记录netfilter POSTROUTING的最后一个点的时间。
  • __netdev_start_xmit第十个记录点,接下来调用网卡驱动发送报文。这个点挂不上,所以挂在dev_queue_xmit

ovs datapath

netdev_frame_hook-->netdev_port_receive-->ovs_vport_receive-->ovs_dp_process_packet-->ovs_execute_actions-->do_execute_actions-->do_output-->ovs_vport_send-->vxlan_xmit-->rpl_vxlan_xmit-->vxlan_xmit_one

vxlan_xmit_one

  • skb_tunnel_info从skb获取info。
  • 从info获取目的端口和vni,源、目的ip。
  • 路由查找,判断出口和入口不同,防止环,报错目的ip是本机的。
  • 获取ecn信息。
  • vxlan_xmit_skb封包发送。

vxlan_xmit_skb

  • 计算封装vxlan需要的最小空间,并且扩展头部空间。
  • 添加vxlan头。
  • 如果有BGP的头,也添加。
  • udp_tunnel_xmit_skb添加协议头发送。

udp_tunnel_xmit_skb

  • 添加UDP协议头。
  • iptunnel_xmit继续添加协议头,并且发送。

iptunnel_xmit

  • 添加ip协议头。
  • ip_local_out继续添加协议头,并且发送.

rpl_ip_local_out

  • 如果没有gso,则直接调用output_ip继续发送。
  • tnl_skb_gso_segment调用kernel的__skb_gso_segment将报文进行分片,kernel中分片tcp的话会进行比较好的分片,udp的话,其实就是ip分片。而这里也会拷贝ip的协议头,这块没看懂,怪异。
  • 将所有的skb发送出去,调用output_ip->ip_local_out调用协议栈

ip_local_out

此处是kernel中的调用。

  • 过netfilter的LOCAL_OUT
  • 调用dst_output-->ip_output,剩下的参照前面kernel的调用路径。
最后修改:2021 年 08 月 20 日
如果觉得我的文章对你有用,请随意赞赏