通常我们使用qemu创建虚拟机时,会使用下面的选项指定虚拟网卡设备的类型,以及桥接、tap设备参数等,如下:

qemu 打印到命令控制台 qemu 输出_qemu 打印到命令控制台

 -device选项用于给虚拟机分配虚拟设备,如磁盘设备、网卡设备等

-netdev选项用于配置虚拟设备的后端,对于网卡设备,常见的有tap、bridge、vhost-user等,tap设备是非常常见的一个后端,如使用libvirt创建虚拟机时,libvirt生成的qemu参数中,使用的就是tap设备,直接使用tap设备更加灵活。vhost-user通常用在dpdk等环境。

本文主要使用tap设备为后端,介绍数据包是如果从tap设备中读取出来,发送给虚拟机设备,以及如果从虚拟机中读取数据包,然后发送给tap设备。

tap设备是一个虚拟机设备,在kernel中没有相对应的物理设备,因此只创建出一个tap设备是没有任何用途的,我们可以使用以下命令在linux中创建一个tap设备:

qemu 打印到命令控制台 qemu 输出_虚拟设备_02

 tap设备主要由内核的tun模块实现,使用'ip tuntap'命令创建设备时,有两种模式,一种是tun,另一个是tap,分别对应点对点设备和以太网设备,或者说一个是三层设备,另一个是二层设备。

即使手工把网卡设置成UP状态,tap也是处于断开的状态,如下:

qemu 打印到命令控制台 qemu 输出_运维_03

只有当应用程序连接到这个设备时,tap设备的状态才会被kernel设置为连接状态,我们可以写个demo程序,从tap设备读取数据包,模拟qemu读取的过程。

程序会输出数据包的长度和数据包的目的mac地址,程序代码如下:

qemu 打印到命令控制台 qemu 输出_qemu_04

执行程序:

qemu 打印到命令控制台 qemu 输出_虚拟设备_05

 并且此时的tap设备状态为:

qemu 打印到命令控制台 qemu 输出_运维_06

qemu使用tap设备时,读写的逻辑和以上的demo程序是一样的,以下是qemu程序从tap设备读取报文的逻辑:

qemu 打印到命令控制台 qemu 输出_网络_07

当qemu启动时,会使用qemu_set_fd_handler函数把tap设备的文件描述符注册到事件循环中,当tap设备从kernel收到数据包时,kernel会通知到qemu,然后qemu会调用tap_send函数去读取,如下:

qemu 打印到命令控制台 qemu 输出_运维_08

从tap_send,可以看出qemu一次最多读取50个数据包,防止tap_send函数过多的占用cpu时间。

tap_read_packet函数会调用read函数读取数据包。

qemu_send_packet_async函数会调用虚拟设备的相关函数发送给虚拟机。

qemu在初始化虚拟设备的时候,会调用qemu_new_nic函数去注册相关回调函数,如下:

qemu 打印到命令控制台 qemu 输出_虚拟设备_09

qemu从tap接口读取到数据包后,会调用设备注册的rtl8139_can_receive函数。tap接口的处理流程基本就完成了,后续就是各个虚拟设备的实现了,如e1000/rtl8139等设备要模拟出相关的PCI物理设备,这样虚拟机中的驱动无需做任何修改就可以识别到设备。

通常情况下,这些虚拟设备会做以下工作:

  1. 根据各种硬件特性,预处理数据包,如计算hash值等。
  2. 选择合适的queue。一般qemu在初始化设备时,会把tap的queue和虚拟设备的queue对应起来。
  3. 把数据包放入queue中,构造metadata信息。
  4. 发送中断信息。

本文章选择rtl8139为例,介绍rtl8139如何处理buf。qemu从tap读取数据包后,会调用rtl8139_receive函数去处理数据包,如下:

qemu 打印到命令控制台 qemu 输出_qemu 打印到命令控制台_10

在rtl8139_receive函数的参数中,我们可以看到两个重要的参数,buf和size。buf就是数据包本身,size是数据包的大小,这两个信息也就是我们平时在抓包时所看到的内容。我们对应着代码中的顺序,介绍下接受函数主要做了什么处理:

  1. rtl8139_receiver_enabled函数检查驱动是否开启了收取功能,如在虚拟机中未加载设备驱动或者关闭了网卡设备,那么rtl8139没有必要再继续进行处理。
  2. 接着是根据驱动的配置是对数据包以太网头部的一个检查,驱动可以配置设备只收取单播、组播、广播或者所有数据包。
  3. 如果数据包小于64bytes,设备会填充至64bytes。
  4. rtl8139设备有两种缓冲管理模式,c mode和c+ mode,后者是一个增强模式,相比前者可以更高效的处理数据包以及支持更多的特性。qemu在这里会把数据包放到rtl8139的缓冲中,这个缓冲区域属于虚拟机的一块内存区域。
  5. 调用qemu相关函数,触发中断。这个时候虚拟机内部OS开始处理设备中断。

从虚拟机中发包的流程和上述收包的流程类似,不过方向是反过来的,发包前,虚拟机内部驱动需要构造出数据包的相关metadata,如并且告诉硬件数据包的地址和长度等信息,我们以rtl8139 c mode为例,看下linux内核如何处理这个过程:

qemu 打印到命令控制台 qemu 输出_网络_11

数据包在linux内核中经过tcpip协议栈后,最后一步要调用驱动程序的ndo_start_xmit方法,对于rtl8139设备,这个函数是rtl8139_start_xmit。

  1. 获取数据包的实际大小。
  2. 把数据包复制到发送缓冲区域。rtl8139 c mode对数据包的处理比较简单。
  3. 向PCI设备的IO接口写入数据。

qemu收到设备的IO接口写操作时,会调用如下的rtl8139设备代码:

qemu 打印到命令控制台 qemu 输出_网络_12

  1. 获取数据包的实际大小。
  2. 读取数据包的内容到txbuffer。
  3. 调用qemu的接口发送数据包。

以下是qemu发送数据包到tap设备的相关代码:

qemu 打印到命令控制台 qemu 输出_虚拟设备_13

可以看到qemu调用writev函数往tap设备写入数据包。