1. 背景
虚拟化场景下,设备的虚拟化有三板斧:
①全模拟:通常指由虚拟化层(通常是Qemu)完全模拟一个设备给虚拟机用。
②virtio驱动半虚拟化:将设备虚拟的工作一拆为二,一部分挪到虚拟机内核中作为前端驱动,一部分放到虚拟化层(通常是Qemu)作为后端,前后端共享Ring环协同完成任务。
③设备直通、SRIOV:借助硬件技术,如intel的VT-d技术实现PCI设备直接挂载给虚拟机。
本文主要聚焦全模拟。以Qemu使用TUN/TAP,虚拟内网卡E1000为例介绍。
2. 一句话总结
物理网卡收到发往虚拟机的数据包后,将其转发到对应的TAP设备。Qemu中TAP设备分为后端驱动和TAP设备关联,负责处理TAP设备的数据包;前端设备,负责将数据传送至虚拟机。具体是数据包到来,Qemu调用tap_send函数,将网络数据报通过e1000_receive函数写入网卡的缓存区,然后通过pci_dma_write将数据包拷贝至虚拟机对应的内存中。然后中断注入给虚拟机。虚拟机读取中断后引发VM-Exit,停止VM进程执行,进入root操作状态。KVM要根据KVM_EXIT_REASON判断原因。对于IO请求,其标志为KVM_EXIT_IO。因为kvm无法处理此操作,需要重新回到qemu的用户态,调用kvm_handle_io进行处理。
3. E1000
Qemu中设备模拟有一套框架QOM,在具体介绍E1000之前简要概述下。
3.1 QOM
type_init(e1000_register_types)-->module_init(function, MODULE_INIT_QOM)__attribute__((constructor))-->register_dso_module_init
gcc的__attribute__((constructor))模拟了构建函数,作用是在程序的main方法执行之前调用了type_init最终其实是构造了一个ModuleEntry对象放到dso_init_list链表中可以理解为用C实现了一套面向对象的体系。
module_call_init(MODULE_INIT_QOM)
这里e->init也就是调用上面准备好的e1000_register_types方法
也就是构建了一个TypeInfo放置到哈希表中,后续使用时可以通过类型检索出来
3.2 设备创建
main()-->net_init_clients-->net_init_netdev-->net_client_init-->net_client_init1这里跟进Qemu中参数的不同可以能调用net_init_netdev或者net_init_client假设这里Qemu参数是-netdev tap 则-->net_init_tap假设这里使用TUN/TAP虚拟网络设备,libvirt已打开与之关联的字符设备/dev/net/tun获取到对应FD。和内核空间的数据交换通过此FD进行。-->net_init_tap_one-->net_tap_fd_init-->qemu_new_net_client-->qemu_net_client_setup
将网卡添加到队列net_clients中,注册相关的回调函数如下:
main-->解析参数-->machine_class->init(current_machine)-->pc_init1-->pc_nic_init-->pci_nic_init_nofail
Qemu构建主板,然后初始化各个设备,pc_nic_init初始化网卡,会调用到上诉的QOM模型,并最终调用到realized方法也就是pci_e1000_realize
pci_e1000_realize-->qemu_new_nic-->qemu_net_client_setup构建网卡对象,设置对应的回调钩子
注意这里会设置peer对端网络信息,也就是上面的netdev
pci_e1000_realize--> e1000_mmio_setup--> memory_region_init_io设置mmio的读写的对应钩子函数
3.3 网卡收包
假设当虚拟网卡有数据发送时调用tap_receivetap_send-->qemu_send_packet_async-->qemu_send_packet_async_with_flags这里获取到对端网口的队列,如前文所述就是E1000设备的incoming_queue队列queue = sender->peer->incoming_queue;-->qemu_net_queue_send-->qemu_net_queue_deliver-->queue->deliver
这里的queue->deliver方法是在qemu_net_client_setup中申请队列的时候指定的钩子函数qemu_deliver_packet_iov-->receive_iov-->e1000_receive_iove1000_receive_iov通过receive_filter过滤数据包,然后根据数据包的不同进行不同处理然后不停的循环,把数据包拷贝至虚拟机所对应的内存中去最后通过set_ics注入中断给虚拟机
E1000的发包流程如下:虚拟机内核通过网口驱动将数据拷贝至缓存区域,然后配置网卡的发包寄存器进行发包,如前文所述已设置mmio的对应回调钩子e1000_mmio_write-->macreg_writeopsindex跟进写入位置推算出具体的操作,调用对应的宏macreg_writeopsset_tctl-->start_xmit-->pci_dma_read从客户机中dma方式读取数据-->process_tx_desc 组装包开始收发包-->xmit_seg-->e1000_send_packet-->nc->info->receive这里会调用到上文的e1000_receive方法e1000_receive-->e1000_receive_iov
接下来和上面的收包类似,就不赘述了。全虚拟化中进行了多次的数据包内存的拷贝,和频繁的虚拟机非根模式的退出,因此效率是非常低的。