virtio-net原理(二)

Virtio概念

virtio 是 KVM 虚拟环境下针对 I/O 虚拟化的最主要的一个通用框架。它通常分为后端和前端。前端是指跑在虚拟机里面的virtio驱动,后端是指Qemu中的设备模拟。

设计Virtio的目的主要考虑到它的高性能与可扩展性。Virtio模拟的是一种现实中不存在的设备,所以在它设计的时候避免了像e1000网卡那些复杂的寄存器操作。

Virtio-net网卡收包流程简述

Virtio网卡收发包的流程非常简单,以下以Qemu的virtio-net的收包流程来说明。 Qemu在收到tap发送过来的数据包后(tap是操作系统虚拟的一个以太网设备),在virtio_net_receive中把数据拷贝到虚拟机的virtio网卡接收队列。然后向虚拟机注入一个中断,这样虚拟机便感知到有网络数据报文的到来。

tap_send   --->  qemu_send_packet_async  ----> qemu_net_queue_send   ---> qemu_net_queue_deliver   ---> nc_sendv_compat  --->  virtio_net_receive。

复杂的地方都在virtio_net_receive,所以我们主要研究这个函数。 那么Qemu怎么获知虚拟机内部的网卡队列的地址呢?

Virtio-net数据结构

先了解一下普通的环形队列,它经常用于生产者/消费者的模型。比如virtio-net网卡recv流程中,网卡设备只管往ring中添加报文,而网卡驱动只需要从ring里不断的读取报文。如下图所示,通常来说我们只需要两个指针便能知道环形队列中的有效数据位置。

 

再来了解一下virtio网卡驱动的数据结构(virtio-net前端,在内核驱动代码中,数据结构如下图所示,注意,这是内核中的数据结构)。为了简单起见,我们不讨论它的原理,只说明用途。

VirtQueue是虚拟队列,用于描述队列的使用情况。Virtio网卡有一个读队列和一个写队列。

Vring是环形队列结构体:他用于记录缓冲区描述符、可用缓冲区描述符、已用缓冲区描述符的情况。

Vring->desc是一个结构体数组的首地址,其中每一个数组元素是一个描述缓冲区的结构体,也称为描述符数组。每个元素中都有一个next变量执行下一个元素。 其实desc是一个数组方式实现的环形队列的首地址。

Vring->avail成员变量是一个用于描述  desc中可用的描述符的结构。 Vring->used是描述已经使用的描述符。 (其实很多驱动中avail和used都简单的设计成两个指针,用于指向当前可用的描述符起始位置和已用的描述符起始位置。而virtio中他们被设计成了指针数组,这使得整个流程看起来很复杂。 指针数组这样的实现可以避免因为其中某一个描述符的处理阻塞而导致整个生产线阻塞的情况)

       从上图看出如果要获取网卡队列缓冲区的地址,我们只需要知道虚拟机内核中的Vring结构体中的desc、avail这些值即可。在Qemu中也有VRing结构体,他们与内核中的Vring对应,实际上它是从内核中获取的。

通过sourceinsight references可以知道有两个函数可以设置QEMU中vring的desc、avail、used成员变量,分别是virtio_queue_set_rings和virtio_queue_set_addr。

virtio_queue_set_rings被virtio_pci_common_write调用,而virtio_pci_common_write是virtio_pci_modern_regions_init注册的IO内存区域的回调,它会在MMIO的处理流程address_space_rw中被调用到(MMIO简单说明:在CPU看来,所有的设备和内存都一样,都是一段地址空间。X86的物理地址空间和PCI地址空间是重叠的,他们通过PCI控制器隔离开来。CPU可以通过PIO和MMIO这两种方式来访问这些设备的寄存器。PIO是用IN、OUT这样的IO指令来访问这些寄存器;而MMIO则把PCI设备的寄存器地址DMA映射到一段物理内存中,这么一来CPU访问PCI寄存器就跟访问内存一样,而不需要IN、OUT这样的指令。PIO是使用IO、OUT这样的敏感指令,所以会从guest模式退出到root模式并被KVM模拟;但是MMIO是普通的内存访问指令,普通的内存存取是不会退出到root模式的。为了捕获并模拟MMIO,KVM不会为MMIO映射的内存建立页表,这样在MMIO的时候就会出现缺页异常而退出到KVM并被模拟)。

传给virtio_queue_set_rings的desc是proxy结构中的成员变量,他们是在virtio_pci_common_write函数中的VIRTIO_PCI_COMMON_Q_DESCLO、VIRTIO_PCI_COMMON_Q_DESCHI中被赋值的, 很显然他们也都是虚拟机中MMIO的时候被KVM截获到的。查看内核virtio-net驱动代码便明白了,desc是在virtio_pci_modern.c的setup_vq函数中写入的:

vp_iowrite64_twopart(virt_to_phys(info->queue), &cfg->queue_desc_lo, &cfg->queue_desc_hi);

setup_vq函数往queue_desc_lo和queue_desc_hi这两个寄存器映射的内存中写入了网卡缓冲区描述符结构体数组的地址info->queue,setup_vq函数中还用同样的方式写入了avail和used这两个寄存器(modern模式)。

简单的说就是虚拟机驱动中往virtio网卡的寄存器中写入了网卡缓冲区描述符的首地址,然后这个写入动作被KVM捕获并传给QEMU,这样QEMU就可以找到网卡缓冲区的地址(这个地址是虚拟机的物理地址,QEMU中使用它之前还需要进行转换)。

Virtio-net网卡收包流程

网卡收包是在virtio_net_receive流程中实现的,它的调用栈如下:

 

这一整套的逻辑描述起来很简单,首先找出可用的缓冲区地址,然后把地址值赋给elem的vector。下一步组装成一个elem,把报文拷贝到elem的vector中,然后把elem变成uelem写入到虚拟机内核VRing->vring_used中,最后通知虚拟机。

以下是上图流程中的解释:

(1):在上面的数据结构中我们已经知道了Qemu怎么获取到desc的。这个desc只是一个vring结构描述的地址。通过address_space_read函数可以获取desc这个虚拟机的物理地址中保存的VRingDesc结构体,这个结构体中addr才是真正的网卡缓冲区地址(这里的网卡缓冲区也是虚拟机的物理地址)。

(2):在(1)的流程中获取了网卡缓冲区的地址,但它只是虚拟机的内核内存空间地址,我们还需要把它转换成Qemu进程的地址空间来。 这是通过调用cpu_physical_memory_map函数来完成的。 然后我们把信息组装成一个elem,返回给virtio_net_receive使用。(我理解的地址转换原理大概是这样的:设备在QEMU中初始化的时候(包括RAM)都会去设置设备的物理地址空间MemoryRegion,这个结构体里面的addr表示该设备的起始物理地址,所以只需要比较这个网卡缓冲区的物理地址是否落在addr到addr + len的空间中就能找出相应的MemoryRegion(RAM)。在Qemu中注册RAM的MemoryRegion的时候是有分配内存的,而内核中的内存逻辑地址与物理内存地址是线性映射的。所以RAM中分配的这个虚拟内存与虚拟机的内核内存地址应该也是简单的线性关系。因此也就能根据虚拟机的物理内存地址找出对应的Qemu进程虚拟内存地址了)。

(3):需要把elem转换成uelem才能写入到虚拟机的内存中。这是因为它是虚拟机内核空间中used描述符的结构体数组中的元素,uelem是used elem的意思。 uelem->id是pop elem流程中获取的可用desc的head。虚拟机中的Virtio网卡驱动中根据这个值便能找到接收报文的描述符的head。

(4):virtio_notify这个其实最终调用的是中断注入流程,往虚拟机中注入一个中断,通知虚拟机有报文到来。

 

Virtio-net网卡发包的原理差不多。虚拟机中virtio网卡驱动往网卡缓冲区中填好报文,然后写queue notify寄存器。这样虚拟机便会退出到root模式,然后在QEMU的vcpu线程virtio_mmio_write函数中对其处理(不过virtio-blk则是在主线程中处理IO的,因为它使用的是eventfd方式)。

posted @ 2018-03-03 14:19  你的KPI完成了吗  阅读(4661)  评论(0编辑  收藏  举报