版本说明
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_kick
和handle_rx_kick
两个worker。 - 给
vhost_net
挂上handle_tx_net
和handle_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->ops
,ioeventfd_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
。
- 使用msi-x中断,有n+1个vector,一个用来对应change中断,n个分别对应n个队列的vq中断。change中断处理函数
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_done
和skb_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的调用路径。