转自:点击打开链接
qemu version:2.6.0
kernel version:3.10.102
以上的流程如下:
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
。
vhost_net_open
vhost_net
的数据结构。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
进程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模块进行配置 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
的链表中。 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
init_vqs
进行队列的初始化工作。 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
setup_vq
,给设置每个virtqueue。request_irq
申请中断,三种方式如下
vp_config_changed
,vq中断处理函数:vring_interrupt
。vp_config_changed
,vq中断处理函数vring_interrupt
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_done
和skb_xmit_done
,分别是在接收、发送完成之后调用。vp_notify
等信息。 虚拟机中使用pktgen发包,pktgen直接调用netdev的ndo_start_xmit
,如果是应用层发包,需要走socket和内核路径。
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。 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。
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
。
tun_net_xmit-->vhost_poll_wakeup-->vhost_poll_queue-->vhost_work_queue-->wake_up_process
会唤醒vhost_worker
。
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
。 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退出,以便进行中断注入。
vcpu_enter_guest-->inject_pending_event-->vmx_inject_irq
调用vmx_write32()
写VMCS的IRQ寄存器。vcpu_enter_guest-->vmx_vcpu_run
在执行前,会读取VMCS中的寄存器信息,由于前面写了IRQ,所以vcpu运行就会知道有中断请求到来,再调用GuestOS的中断处理函数处理中断请求。 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
__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_v4_rcv
找到匹配的sock,根据skb的net、四元组和入端口信息,sock会记录连接的状态。tcp_v4_do_rcv-->tcp_rcv_established-->tcp_queue_rcv
添加到sk_receive_queue
队列中。 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
udp_tunnel_xmit_skb
添加协议头发送。udp_tunnel_xmit_skb
iptunnel_xmit
继续添加协议头,并且发送。iptunnel_xmit
ip_local_out_sk-->__ip_local_out-->__ip_local_out_sk
继续添加协议头,并且发送。__ip_local_out_sk
dst_output_sk-->ip_output
。ip_output
ip_finish_output
。ip_finish_output
ip_finish_output_gso
进行分片。ip_fragment
进行分片。ip_finish_output2
进行报文发送。ip_finish_output2
__ipv4_neigh_lookup_noref
查找邻居子系统。dst_neigh_output-->neigh_hh_output
进行报文发送。neigh_hh_output
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
。 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。vxlan_xmit_skb
封包发送。vxlan_xmit_skb
udp_tunnel_xmit_skb
添加协议头发送。udp_tunnel_xmit_skb
iptunnel_xmit
继续添加协议头,并且发送。iptunnel_xmit
ip_local_out
继续添加协议头,并且发送.rpl_ip_local_out
output_ip
继续发送。tnl_skb_gso_segment
调用kernel的__skb_gso_segment
将报文进行分片,kernel中分片tcp的话会进行比较好的分片,udp的话,其实就是ip分片。而这里也会拷贝ip的协议头,这块没看懂,怪异。output_ip->ip_local_out
调用协议栈ip_local_out
此处是kernel中的调用。
dst_output-->ip_output
,剩下的参照前面kernel的调用路径。