Qemu之Network Device全虚拟方案一:前端网络流的建立
KVM在I/O虚拟化方面,传统的方式是使用Qemu纯软件的方式来模拟I/O设备,其中包括经常使用的网卡设备。这次我们重点分析Qemu为实现网络设备虚拟化的全虚拟化方案。本主题从三个组成方面来完整描述,包括:1. 前端网络流的建立; 2. 虚拟网卡的创建; 3. 网络I/O虚拟化 in Guest OS。
本篇主要讲述“前端网络流的建立”。
VM网络配置方式
根据KVM的网络配置方案,大概分为如下几种:
1. 默认用户模式;
2. 基于网桥(Bridge)的模式;
3. 基于NAT(Network Address Translation)的模式;
4. 网络设备的直接分配(基于Intel VT-d技术);
在“kvm安装与启动过程说明”一文中,Guest OS启动命令中没有传入的网络配置,此时QEMU默认分配rtl8139类型的虚拟网卡类型,使用的是默认用户配置模式,这时候由于没有具体的网络模式的配置,Guest的网络功能是有限的。
网桥模式是目前比较简单,也是用的比较多的模式,所以我们这里主要分析基于网桥模式下的VM的收发包流程。
网桥的原理与创建
网桥的原理
网桥(Bridge)也称桥接器,是连接两个局域网的存储转发设备,用它可以完成具有相同或相似体系结构网络系统的连接。一般情况下,被连接的网络系统都具有相同的逻辑链路控制规程(LLC),但媒体访问控制协议(MAC)可以不同。网桥工作在数据链路层,将两个LAN连起来,根据MAC地址来转发帧,其实就是一个简单的二层交换机。
Linux网络协议栈已经支持了网桥的功能,但需要进行相关的配置才可以进行正常的转发。而要配置Linux网桥功能,需要配置工具bridge-utils,大家可以从网上下载源码编译、安装,生成网桥配置的工具名称为brctl。
关于Linux网桥,这里不再过多的叙述,有兴趣的同学可以自行研究下,其实就是一些MAC地址学习、转发表的维护、及生成树的协议管理等功能,大家就认为它是个实现在内核中的二层交换机就行。
Linux网桥的具体实现可以参看我以前总结的关于网桥的流程分析:http://blog.csdn.net/hsly_support/article/details/8762896
网桥的创建
1 2 | $brctl addbr br0 #添加br0这个bridge$brctl addif br0 eth0 #将br0与eth0绑定起来 |
通过上述两条命令,bridge br0成为连接本机与外部网络的接口。
Tap的原理与创建
qemu-system-x86_64命令关于bridge模式的网络参数如下:
1 | -net tap[,vlan=n][,name=str][,fd=h][,ifname=name][,script=file][,downscript=dfile][,helper=helper][,sndbuf=nbytes][,vnet_hdr=on|off][,vhost=on|off][,vhostfd=h][,vhostforce=on|off] |
主要参数说明:
tap:表示创建一个tap设备;
ifname:表示tap设备接口名字;
script:表示host在启动guest时自动配置的脚本,默认为/etc/qemu-ifup;
downscript:表示host在关闭guest时自动执行的脚本;
fd=h: 连接到现在已经打开着的TAP接口的文件描述符,一般让QEMU会自动创建一个TAP接口;
helper=helper: 设置启动客户机时在宿主机中运行的辅助程序,包括去建立一个TAP虚拟设备,它的默认值为/usr/local/libexec/qemu-bridge-helper,一般不用自定义,采用默认值即可;
sndbuf=nbytes: 限制TAP设备的发送缓冲区大小为n字节,当需要流量进行流量控制时可以设置该选项。其默认值为“sndbuf=0”,即不限制发送缓冲区的大小。
什么是Tap设备
qemu在这里使用了Tap设备,那Tap设备是什么呢?
TUN/TAP虚拟网络设备的原理比较简单,在Linux内核中添加了一个TUN/TAP虚拟网络设备的驱动程序和一个与之相关连的字符设备/dev/net/tun,字符设备tun作为用户空间和内核空间交换数据的接口。当内核将数据包发送到虚拟网络设备时,数据包被保存在设备相关的一个队列中,直到用户空间程序通过打开的字符设备tun的描述符读取时,它才会被拷贝到用户空间的缓冲区中,其效果就相当于,数据包直接发送到了用户空间。通过系统调用write发送数据包时其原理与此类似。
TUN/TAP驱动程序中包含两个部分,一部分是字符设备驱动,还有一部分是网卡驱动部分。利用网卡驱动部分接收来自TCP/IP协议栈的网络分包并发送或者反过来将接收到的网络分包传给协议栈处理,而字符驱动部分则将网络分包在内核与用户态之间传送,模拟物理链路的数据接收和发送。Tun/tap驱动很好的实现了两种驱动的结合。
总而言之,Tap设备实现了这么一种能力,对于用户空间来讲实现了网络数据包在内核态与用户态之间的传输,对于内核管理来说,它是一个网络设备或者直接呈现的是一个网络接口,可以像普通网络接口一样进行收发包。
Tap设备的创建
当命令行中通过-net tap指明创建Tap设备后,在Qemu的main函数中首先解析到了tap的参数选项,然后进入了设备创建流程:
main() file: vl.c, line: 2345net_init_clients() file: net.c, line: 991net_init_client() file: net.c, line: 962net_client_init() file: net.c, line: 701net_client_init1() file: net.c, line: 628net_client_init_fun[opts->kind](opts, name, peer)
在Qemu中,所有的-net类型都由net client这个概念来表示:net_client_init_fun由各类net client对应的初始化函数组成:
1 2 3 4 5 6 7 8 9 | static int (* const net_client_init_fun[NET_CLIENT_OPTIONS_KIND_MAX])( const NetClientOptions *opts, const char *name, NetClientState *peer) = { [NET_CLIENT_OPTIONS_KIND_NIC] = net_init_nic, <------网卡设备类型 [NET_CLIENT_OPTIONS_KIND_TAP] = net_init_tap, <------Tap设备类型 [NET_CLIENT_OPTIONS_KIND_SOCKET] = net_init_socket, [NET_CLIENT_OPTIONS_KIND_HUBPORT] = net_init_hubport, }; |
net_tap_init()主要做了两件事:
1. 通过tap_open(){open(“/dev/net/tun”)}返回了Tap设备的文件描述符fd;
2. 将Tap设备的文件描述符加入Qemu事件监听列表;
3. 通过Qemu-ifup脚本将创建的Tap设备接口加入网桥中,这里假设Tap设备名为tap1;
主要的调用流程如下:
net_init_tap() file: tap.c, line: 589 net_tap_init() file: tap.c, line: 552 tap_open() file: tap-linux.c, line: 38 fd = open(PATH_NET_TUN,O_RDWR) file: tap-linux.c, line: 43 net_tap_fd_init() file: tap.c, line: 325 tap_read_poll()file: tap.c, line: 81 tap_update_fd_handler() file: tap.c, line: 72qemu_set_fd_handler2() file: iohandler.c, line: 51QLIST_INSERT_HEAD(&io_handlers, ioh, next); file: iohandler.c, line: 72
可以看到最后Tap设备的事件通知加入了io_handlers的事件监听列表中,fd_read事件对应的动作为tap_send(),fd_write事件对应的动作为tap_writable()。
Tap设备接口加入网桥命令:
1 | $brctl addif br0 tap1 |
Qemu主线程中的事件监听
io_handlers的事件监听在哪里发生呢?
Qemu的Main函数通过一系列的初始化,并创建线程进行VM的启动,最后来到了main_loop()(file:vl.c, line: 3790)
main_loop() file: vl.c, line: 1631main_loop_wait() file: main-loop.c, line: 473qemu_iohandler_fill() file: io_handler.c, line: 93 os_host_main_loop_wait()file: main-loop.c, line: 291 select(nfds + 1, &rfds,&wfds, &xfds, tvarg) <----此处对注册的源进行监听,包括Tap fd;qemu_iohandler_poll() file: io_handler.c, line: 115 <---调用事件对应动作进行处理;
前端网络数据流程
如图中所示,红色箭头表示数据报文的入方向,步骤:
1. 网络数据从Host上的物理网卡接收,到达网桥;
2. 由于eth0与tap1均加入网桥中,根据二层转发原则,br0将数据从tap1口转发出去,即数据由Tap设备接收;
3. Tap设备通知对应的fd数据可读;
4. fd的读动作通过tap设备的字符设备驱动将数据拷贝到用户空间,完成数据报文的前端接收。
绿色箭头表示数据报文的出方向,步骤与入方向相反,这里不再详细叙述
上文针对Qemu在前端网络流路径的建立方面做了详细的描述,数据包从Host的物理网卡经过Host Linux内核中的Bridge,经过Tap设备到达了Qemu的用户态空间。而Qemu是如何把数据包送进Guest中的呢,这里必然要说到到虚拟网卡的建立。
当命令行传入nic相关参数时,Qemu就会解析网络相关的参数后进入虚拟网卡的创建流程。而在上文中提到对于所有-net类型的设备,都视作一个net client来对待。而在net client的建立之前,需要先创建Qemu内部的hub和对应的port,来关联每一个net client,而对于每个创建的-net类型的设备都是可以可以配置其接口的vlan号,从而控制数据包在其中配置的vlan内部进行转发,从而做到多个虚拟设备之间的switch。
Hub及port的建立
main() file: vl.c, line: 2345net_init_clients() file: net.c, line: 991net_init_client() file: net.c, line: 962net_client_init() file: net.c, line: 701net_client_init1() file: net.c, line: 628 peer =net_hub_add_port(u.net->has_vlan ? u.net->vlan : 0, NULL);
net_hub_add_port()传入的第一个参数为vlan号,如果没有配置vlan的话则默认为0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | NetClientState *net_hub_add_port(int hub_id, const char *name) { NetHub *hub; NetHubPort *port; QLIST_FOREACH(hub, &hubs, next) { <----从全局的hubs链中查找是否已经存在if (hub->id == hub_id) { break; } } if (!hub) { hub = net_hub_new(hub_id); <----若不存在则创建新的 } port = net_hub_port_new(hub, name);<----创建在对应hub下创建新的port return &port->nc; } |
对于在此过程中创建的各种数据结构关系如下图:
相关的解释:
1. hubs全局链挂载的NetHub结构对应于指定创建的每一个vlan号;
2. 每个hub下面可以挂载属于同一vlan的多个NetHubPort来描述的port;
3. 每个NetHubPort下归属的NetClientStatenc结构表示了具体挂载的net client;
4. “e1000”,”tap1”都有自己的net client结构,而各自的NetClientInfo都指向了net_hub_port_info;该变量定义了一系列hub操作函数;
5. 最后net_hub_add_port()返回了每个nic对应的NetClientState结构,即图中的peer。
nic设备与hub port的关联
nic设备完成hub及port的创建后,进入nic相关的初始化,即net_init_nic()。
1 2 3 4 5 6 7 8 9 10 11 12 | static int net_init_nic(const NetClientOptions *opts, const char *name, NetClientState *peer) { NICInfo *nd; idx = nic_get_free_idx(); nd = &nd_table[idx]; nd->netdev = peer; nd->name = g_strdup(name); ...... nd->used = 1; nb_nics++; return idx; } |
解释:
1. 首先从nd_table[]中找到空闲的NICInfo结构,每一个nd_table项代表了一个NIC设备;
2. 填充相关的内容,其中最重要的是net->netdev = peer,此时hub中的NetClient与NICInfo关联,通过NICInfo可找到NetClient;
tap设备与hub port的关联
tap设备完成hub及port的创建后,进入tap相关的初始化,即net_init_tap()。前文中已描述过部分的tap设备相关初始化内容,主要是tap设备的打开和事件监听的设置。而这里主要描述hub port与tap设备的关联。
主要调用流程:
net_init_tap() file: tap.c, line: 589 net_tap_init() file: tap.c, line: 552 tap_open() file: tap-linux.c, line: 38 fd = open(PATH_NET_TUN,O_RDWR) file: tap-linux.c, line: 43 net_tap_fd_init() file: tap.c, line: 325
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | static TAPState *net_tap_fd_init(NetClientState *peer, const char *model, const char *name, int fd, int vnet_hdr) { NetClientState *nc; TAPState *s; nc = qemu_new_net_client(&net_tap_info, peer, model, name); s = DO_UPCAST(TAPState, nc, nc); s->fd = fd; s->host_vnet_hdr_len = vnet_hdr ? sizeof(struct virtio_net_hdr) : 0; s->using_vnet_hdr = 0; s->has_ufo = tap_probe_has_ufo(s->fd); tap_set_offload(&s->nc, 0, 0, 0, 0, 0); tap_read_poll(s, 1); s->vhost_net = NULL; return s; } |
该函数最主要的工作就是建立了代表tap设备的TAPState结构与hub port的关联;Tap设备对应的hub port的NetClientState的peer指向了TAPState的NetClient,而TAPState的NetClient的peer指向了hub port的NetClientInfo;
tap_read_poll()就是上文分析的设置tap设备读写监听事件。
虚拟网卡的建立
上面图中可以看到TAP设备结构与tap对应的hub port通过NetClientInfo的peer指针相互进行了关联,而代表e1000的NICInfo中的NetClientInfo与e1000对应的hub port只有单向的关联,从hub port到NICInfo并没有关联,因此建立两者相互关联需要说到Guest的虚拟网卡的建立。
Guest OS物理内存管理
在说明虚拟网卡的建立前,首先得说一下Guest OS中的物理内存管理。
在虚拟机创建之初,Qemu使用malloc()从其进程地址空间中申请了一块与虚拟机的物理内存大小相等的区域,该块区域就是作为了Guest OS的物理内存来使用。
物理内存通常是不连续的,例如地址0xA0000至0xFFFFF、0xE0000000至0xFFFFFFFF等通常留给BIOS ROM和MMIO,而不是物理内存。
设:
虚拟机包括n块物理内存,分别记做P1, P2, …, Pn;
每块物理内存的起始地址分别记做PB1, PB2, …, PBn;
每块物理内存的大小分别为PS1, PS2, …, PSn。
Qemu根据虚拟机的物理内存布局,将该区域划分成n个子区域,分别记做V1, V2, …, Vn;
第i个子区域与第i块物理内存对应,每个子区域的起始线性地址记做VB1, VB2, …,VBn;
每个子区域的大小等于对应的物理内存块的大小,仍是PS1, PS2, …, PSn。
在Qemu创建虚拟机的时候会向KVM通告Guest OS所使用的物理内存布局,采用KVMSlot的数据结构来表示:
1 2 3 4 5 6 7 8 | typedef struct KVMSlot { hwaddr start_addr; <----------Guest物理地址块的起始地址 ram_addr_t memory_size; <----------大小 void *ram; <----------QUMU用户空间地址 int slot; <----------slot号 int flags; <----------内存属性 } KVMSlot; |
调用关系:
Qemu:
kvm_set_phys_mem()-> file:kvm-all.c, line:550kvm_set_user_memory_region()-> file:kvm-all.c, line:191 kvm_vm_ioctl()->通过KVM_SET_USER_MEMORY_REGION进入Kernel KVM:
kvm_vm_ioctl_set_memory_region()-> file: kvm_main.c, line:931__kvm_set_memory_region() file: kvm_main.c, line:734
Guest OS访问任意一块物理地址GPA时,都可以通过KVMSlot记载的关系来得到Qemu的虚拟地址映射即HVA,Qemu中地址空间与VM中地址空间的关系如下图:
虚拟网卡
为什么先要提下Guest OS的物理内存管理呢,因为作为一个硬件设备,OS要控制其必然通过PIO与MMIO来与其交互。而目前的网卡主要涉及到MMIO,而且还是PCI接口,所以必然落入图中的PCI mem。
Qemu根据传入的创建指定NIC类型的参数来进行指定NIC的创建操作,对于e1000虚拟网卡来说,其通过type_init(e1000_register_types)注册了MODULE_INIT_QOM类型的设备,而当Qemu创建e1000虚拟设备时,通过内部的PCI Bus设备抽象层来进行了e1000的初始化,该抽象层这里不做过多的描述。主要关注网卡部分的初始化:
static TypeInfoe1000_info = { file: e1000.c,line: 1289 .name = "e1000",
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(E1000State),
.class_init = e1000_class_init, <-------初始化入口函数 }; static voide1000_class_init(ObjectClass *klass, void *data) file: e1000.c, line: 1271 { DeviceClass *dc= DEVICE_CLASS(klass); PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);
k->init = pci_e1000_init; <-------注册虚拟网卡pci层的初始化函数 k->exit = pci_e1000_uninit;
k->romfile = "pxe-e1000.rom";
k->vendor_id = PCI_VENDOR_ID_INTEL;
k->device_id = E1000_DEVID;
k->revision = 0x03;
k->class_id = PCI_CLASS_NETWORK_ETHERNET;
dc->desc = "Intel Gigabit Ethernet";
dc->reset = qdev_e1000_reset;
dc->vmsd = &vmstate_e1000;
dc->props = e1000_properties;
} static intpci_e1000_init(PCIDevice *pci_dev)
{
e1000_mmio_setup(d); <-------e1000的mmio访问建立pci_register_bar(&d->dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY,&d->mmio); <-------注册mmio空间pci_register_bar(&d->dev, 1, PCI_BASE_ADDRESS_SPACE_IO,&d->io); <-------注册pio空间 d->nic =qemu_new_nic(&net_e1000_info, &d->conf, object_get_typename(OBJECT(d)),d->dev.qdev.id, d);
<----初始化nic信息,并注册虚拟网卡的相关操作函数,结构如下,同时创建了与虚拟网卡对应的net client结构。在 add_boot_device_path(d->conf.bootindex,&pci_dev->qdev, "/ethernet-phy@0"); 加入系统启动设备配置中
} static NetClientInfonet_e1000_info = {
.type = NET_CLIENT_OPTIONS_KIND_NIC, .size = sizeof(NICState),
.can_receive = e1000_can_receive,
.receive = e1000_receive, <----------主要是receive函数 .cleanup = e1000_cleanup,
.link_status_changed = e1000_set_link_status,
};
最后PCI设备抽象层将e1000代表的net client与上文描述的e1000所占用的NICinfo所对应hub port的NetClientState *peer进行关联。
至此就完成了虚拟网卡的建立,最终在Qemu的hub中的各Nic的关系如下:
上文针对Qemu在前端网络流路径的建立方面做了详细的描述,数据包从Host的物理网卡经过Host Linux内核中的Bridge, 经过Tap设备到达了Qemu的用户态空间。而Qemu是如何把数据包送进Guest中的呢,这里必然要说到到虚拟网卡的建立。
当命令行传入nic相关参数时,Qemu就会解析网络相关的参数后进入虚拟网卡的创建流 程。而在上文中提到对于所有-net类型的设备,都视作一个net client来对待。而在net client的建立之前,需要先创建Qemu内部的hub和对应的port,来关联每一个netclient,而对于每个创建的-net类型的设备都是可以可以配置其接口的vlan号,从而控制数据包在其中配置的vlan内部进行转发,从而做到多个 虚拟设备之间的switch。
Hub及port的建立
main() file: vl.c,line: 2345 net_init_clients() file:net.c, line: 991 net_init_client() file:net.c, line: 962 net_client_init() file:net.c, line: 701 net_client_init1() file: net.c, line: 628 peer = net_hub_add_port(u.net->has_vlan ?u.net->vlan : 0, NULL);
net_hub_add_port()传入的第一个参数为vlan号,如果没有配置vlan的话则默认为0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | NetClientState *net_hub_add_port(int hub_id, const char *name) { NetHub *hub; NetHubPort *port; QLIST_FOREACH(hub, &hubs, next) { <----从全局的hubs链中查找是否已经存在 if (hub->id == hub_id) { break; } } if (!hub) { hub = net_hub_new(hub_id); <----若不存在则创建新的 } port = net_hub_port_new(hub, name);<----创建在对应hub下创建新的port return &port->nc; } |
对于在此过程中创建的各种数据结构关系如下图:
相关的解释:
1. hubs全局链挂载的NetHub结构对应于指定创建的每一个vlan号;
2. 每个hub下面可以挂载属于同一vlan的多个NetHubPort来描述的port;
3. 每个NetHubPort下归属的NetClientState nc结构表示了具体挂载的net client;
4. “e1000”,”tap1”都有自己的net client结构,而各自的NetClientInfo都指向了net_hub_port_info;该变量定义了一系列hub操作函数;
5. 最后net_hub_add_port()返回了每个nic对应的NetClientState结构,即图中的peer。
nic设备与hub port的关联
nic设备完成hub及port的创建后,进入nic相关的初始化,即net_init_nic()。
1 2 3 4 5 6 7 8 9 10 11 12 | static int net_init_nic(const NetClientOptions *opts, const char *name, NetClientState *peer) { NICInfo *nd; idx = nic_get_free_idx(); nd = &nd_table[idx]; nd->netdev = peer; nd->name = g_strdup(name); ...... nd->used = 1; nb_nics++; return idx; } |
解释:
1. 首先从nd_table[]中找到空闲的NICInfo结构,每一个nd_table项代表了一个NIC设备;
2. 填充相关的内容,其中最重要的是net->netdev = peer,此时hub中的NetClient与NICInfo关联,通过NICInfo可找到NetClient;
tap设备与hub port的关联
tap设备完成hub及port的创建后,进入tap相关的初始化,即net_init_tap()。前文中已描述过部分的tap设备相关初始化内容,主要是tap设备的打开和事件监听的设置。而这里主要描述hub port与tap设备的关联。
主要调用流程:
net_init_tap() file: tap.c, line: 589 net_tap_init()file: tap.c, line: 552 tap_open() file: tap-linux.c, line: 38 fd = open(PATH_NET_TUN,O_RDWR) file: tap-linux.c, line: 43 net_tap_fd_init() file: tap.c, line: 325
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | static TAPState *net_tap_fd_init(NetClientState *peer, const char *model, const char *name, int fd, int vnet_hdr) { NetClientState *nc; TAPState *s; nc = qemu_new_net_client(&net_tap_info, peer, model, name); s = DO_UPCAST(TAPState, nc, nc); s->fd = fd; s->host_vnet_hdr_len = vnet_hdr ? sizeof(struct virtio_net_hdr) : 0; s->using_vnet_hdr = 0; s->has_ufo = tap_probe_has_ufo(s->fd); tap_set_offload(&s->nc, 0, 0, 0, 0, 0); tap_read_poll(s, 1); s->vhost_net = NULL; return s; } |
该函数最主要的工作就是建立了代表tap设备的TAPState结构与hub port的关联;Tap设备对应的hubport的NetClientState的peer指向了TAPState的NetClient,而TAPState的NetClient的peer指向 了hubport的NetClientInfo;
tap_read_poll()就是上文分析的设置tap设备读写监听事件。
虚拟网卡的建立
上面图中可以看到TAP设备结构与tap对应的hub port通过NetClientInfo的peer指针相互进行了关联,而代表e1000的NICInfo中的NetClientInfo与e1000对 应的hub port只有单向的关联,从hub port到NICInfo并没有关联,因此建立两者相互关联需要说到Guest的虚拟网卡的建立。
Guest OS物理内存管理
在说明虚拟网卡的建立前,首先得说一下Guest OS中的物理内存管理。
在虚拟机创建之初,Qemu使用malloc()从其进程地址空间中申请了一块与虚拟机的物理内存大小相等的区域,该块区域就是作为了Guest OS的物理内存来使用。
物理内存通常是不连续的,例如地址0xA0000至0xFFFFF、0xE0000000至0xFFFFFFFF等通常留给BIOS ROM和MMIO,而不是物理内存。
设:
虚拟机包括n块物理内存,分别记做P1, P2, …, Pn;
每块物理内存的起始地址分别记做PB1, PB2, …, PBn;
每块物理内存的大小分别为PS1, PS2, …, PSn。
Qemu根据虚拟机的物理内存布局,将该区域划分成n个子区域,分别记做V1, V2, …, Vn;
第i个子区域与第i块物理内存对应,每个子区域的起始线性地址记做VB1, VB2, …, VBn;
每个子区域的大小等于对应的物理内存块的大小,仍是PS1, PS2, …, PSn。
在Qemu创建虚拟机的时候会向KVM通告Guest OS所使用的物理内存布局,采用KVMSlot的数据结构来表示:
1 2 3 4 5 6 7 8 | typedef struct KVMSlot { hwaddr start_addr; <----------Guest物理地址块的起始地址 ram_addr_t memory_size; <----------大小 void *ram; <----------QUMU用户空间地址 int slot; <----------slot号 int flags; <----------内存属性 } KVMSlot; |
调用关系:
Qemu:
kvm_set_phys_mem()-> file:kvm-all.c, line:550kvm_set_user_memory_region()-> file:kvm-all.c, line:191 kvm_vm_ioctl()-> 通过KVM_SET_USER_MEMORY_REGION进入KernelKVM:
kvm_vm_ioctl_set_memory_region()-> file: kvm_main.c,line:931 __kvm_set_memory_region() file: kvm_main.c, line:734
Guest OS访问任意一块物理地址GPA时,都可以通过KVMSlot记载的关系来得到Qemu的虚拟地址映射即HVA,Qemu中地址空间与VM中地址空间的关系如下图:
虚拟网卡
为什么先要提下Guest OS的物理内存管理呢,因为作为一个硬件设备,OS要控制其必然通过PIO与MMIO来与其交互。而目前的网卡主要涉及到MMIO,而且还是PCI接口,所以必然落入图中的PCI mem。
Qemu根据传入的创建指定NIC类型的参数来进行指定NIC的创建操作,对于e1000虚拟网卡来说,其通过 type_init(e1000_register_types) 注册了MODULE_INIT_QOM类型的设备,而当Qemu创建e1000虚拟设备时,通过内部的PCI Bus设备抽象层来进行了e1000的初始化,该抽象层这里不做过多的描述。主要关注网卡部分的初始化:
static TypeInfo e1000_info = { file: e1000.c, line: 1289 .name ="e1000",
.parent =TYPE_PCI_DEVICE,
.instance_size= sizeof(E1000State),
.class_init =e1000_class_init, <-------初始化入口函数 }; static void e1000_class_init(ObjectClass*klass, void *data) file: e1000.c,line: 1271 { DeviceClass *dc = DEVICE_CLASS(klass); PCIDeviceClass *k =PCI_DEVICE_CLASS(klass);
k->init =pci_e1000_init; <-------注册虚拟网卡pci层的初始化函数 k->exit =pci_e1000_uninit;
k->romfile = "pxe-e1000.rom";
k->vendor_id= PCI_VENDOR_ID_INTEL;
k->device_id= E1000_DEVID;
k->revision= 0x03;
k->class_id= PCI_CLASS_NETWORK_ETHERNET;
dc->desc ="Intel Gigabit Ethernet";
dc->reset =qdev_e1000_reset;
dc->vmsd =&vmstate_e1000;
dc->props =e1000_properties;
} static int pci_e1000_init(PCIDevice *pci_dev)
{
e1000_mmio_setup(d); <-------e1000的mmio访问建立 pci_register_bar(&d->dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY,&d->mmio); <-------注册mmio空间 pci_register_bar(&d->dev, 1,PCI_BASE_ADDRESS_SPACE_IO, &d->io); <-------注册pio空间d->nic = qemu_new_nic(&net_e1000_info, &d->conf,object_get_typename(OBJECT(d)), d->dev.qdev.id, d);
<----初始化nic信息,并注册虚拟网卡的相关操作函数,结构如下,同时创建了与虚拟网卡对应的net client结构。在add_boot_device_path(d->conf.bootindex, &pci_dev->qdev,"/ethernet-phy@0"); 加入系统启动设备配置中
} static NetClientInfo net_e1000_info = {
.type =NET_CLIENT_OPTIONS_KIND_NIC, .size = sizeof(NICState),
.can_receive =e1000_can_receive,
.receive =e1000_receive, <----------主要是receive函数 .cleanup =e1000_cleanup,
.link_status_changed = e1000_set_link_status,
};
最后PCI设备抽象层将e1000代表的net client与上文描述的e1000所占用的NICinfo所对应hub port的NetClientState*peer进行关联。
至此就完成了虚拟网卡的建立,最终在Qemu的hub中的各Nic的关系如下:
前面两文主要对前端网络流的数据路径和虚拟网卡的创建进行了说明,这些可以看做是Guest OS网络数据包收发的准备工作,那么网络数据包是如何在Guest OS中进进出出的呢,本文就是重点讲述Guest OS的数据包的收发路径,其中涉及到一个重要的虚拟化技术,即I/O虚拟化。
Guest OS中网络数据包的接收
前文我们讲到Qemu主线程通过监听Tap设备文件读写事件来收发数据包,当有属于Guest OS的数据包在Host中收到后,Host根据配置通过Bridge,Tap设备来到了Qeumu的用户态空间,Qemu通过调用了预先注册的Tap的读 事件处理函数进行处理,如下图:
tap_send() file: tap
.c, line:
192tap_read_packet() 通过read从/net/dev/tun的fd中读取数据包
qemu_send_packet_async() file: net
.c, line:
384qemu_send_packet_async_with_flags() file: net
.c, line:
363qemu_net_queue_send() file: net
.c, line:
170qemu_net_queue_deliver() file: queue
.c, line:
140qemu_deliver_packet() file: net
.c, line:
317ret= nc->info->receive()
;
此时的nc->info即为初始化时注册的net_hub_port_info,该结构内容为:
1 2 3 4 5 6 7 8 | static NetClientInfo net_hub_port_info = { .type = NET_CLIENT_OPTIONS_KIND_HUBPORT, .size = sizeof(NetHubPort), .can_receive = net_hub_port_can_receive, .receive = net_hub_port_receive, <-------nc->info->receive() .receive_iov = net_hub_port_receive_iov, .cleanup = net_hub_port_cleanup, }; |
我们继续看net_hub_port_receive():
net_hub_port_receive()
file: hub.c,
line:
108net_hub_receive()
file: hub.c,
line:
44
net_hub_receive()这个函数比较有意思,其遍历对应hub下的每个port,通过qume_send_packet()将数据包转发给不是本port的其它port,其实就是vlan环境下二层转发机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | static ssize_t net_hub_receive(NetHub *hub, NetHubPort *source_port, const uint8_t *buf, size_t len) { NetHubPort *port; QLIST_FOREACH(port, &hub->ports, next) { if (port == source_port) { continue; } qemu_send_packet(&port->nc, buf, len); } return len; } |
前文最后一图中可以看到,我们配置的该hub或说是vlan下的其它port只有一个就是e1000虚拟网卡,因此qemu_send_packet(&port->nc, buf, len);中的port->nc即e1000的nc结构;继续跟踪数据流向:
qemu_send_packet() file: net
.c, line:
392qemu_send_packet_async() file: net
.c, line:
384qemu_send_packet_async_with_flags() file: net
.c, line:
363qemu_net_queue_send() file: net
.c, line:
170qemu_net_queue_deliver() file: queue
.c, line:
140qemu_deliver_packet() file: net
.c, line:
317ret= nc->info->receive()
;
这个路径是不是和Tap转发的路径很像,但是这里nc->info并不是net_hub_port_info了, 而是e1000创建时注册的net_e1000_info,有疑问的同学可以仔细去看下e1000的初始化流程,这里不过多叙述。看下 net_e1000_info中有些什么:
1 2 3 4 5 6 7 8 | static NetClientInfo net_e1000_info = { .type = NET_CLIENT_OPTIONS_KIND_NIC, .size = sizeof(NICState), .can_receive = e1000_can_receive, .receive = e1000_receive, <-------nc->info->receive() .cleanup = e1000_cleanup, .link_status_changed = e1000_set_link_status, }; |
数据包到达了e1000_receive了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | e1000_receive(NetClientState *nc, const uint8_t *buf, size_t size) { E1000State *s = DO_UPCAST(NICState, nc, nc)->opaque; //获取e1000的状态控制结构 // E1000Stat很重要,其中包含了所有e1000硬件相关的寄存器信息,数据包描述符信息 ...... base = rx_desc_base(s) + sizeof(desc) * s->mac_reg[RDH]; ...... pci_dma_write(&s->dev, le64_to_cpu(desc.buffer_addr), buf + desc_offset + vlan_offset, copy_size); ...... pci_dma_write(&s->dev, base, &desc, sizeof(desc)); ...... set_ics(s, 0, n); //触发中断 } |
有上述函数过程的可以看到,数据包从buf中通过pci_dma_write接口注入到了e1000的数据包接收内存中,当然这里的dma并不是真 正的硬件DMA操作,而是虚拟化成普通内存的写,因为Guest OS的物理内存是Qemu的虚拟内存,因此Qemu可以直接访问,而Guest并不知道这一切。最后通过set_ics进行Guest收包中断的注入。这 一块涉及了中断虚拟化,我们后面单独进行分析。
这样数据包就进入了Guest的物理内存中,收包中断也安排好了,就好像是纯粹的真实的物理网卡到OS的数据流程。而这一切都是全虚拟化应该达到的效果。
Guest OS中网络数据包的发送
当Guest OS中有数据包要发送时,在全虚拟化情况下,Guest会像通常那样走普通网卡驱动流程,将数据包的内容写入待发送的skbuffer的地址空间中,同时 将待发送的skbuffer地址放入发送ring中,配置网卡的发送寄存器就可将数据包发送出去,而在Guest模式下,当Guest访问PIO或者 MMIO时会触发VM Exit,进入到Host OS 中的kvm。
而设备的模拟是在Qemu中进行的,KVM对该中异常退出无法处理,会将该退出原因注入给Qemu来处理,流程可参考“KVM Run Process之KVM核心流程”一文。
Qemu通过对触发io exit的地址的范围检测,找到对应的PIO/MMIO的地址空间,并调用地址空间注册时的一系列对应寄存器操作处理函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | static const MemoryRegionOps e1000_mmio_ops = { .read = e1000_mmio_read, .write = e1000_mmio_write, .endianness = DEVICE_LITTLE_ENDIAN, .impl = { .min_access_size = 4, .max_access_size = 4, }, }; static void (*macreg_writeops[])(E1000State *, int, uint32_t) = { putreg(PBA), putreg(EERD), putreg(SWSM), putreg(WUFC), putreg(TDBAL), putreg(TDBAH), putreg(TXDCTL), putreg(RDBAH), putreg(RDBAL), putreg(LEDCTL), putreg(VET), [TDLEN] = set_dlen, [RDLEN] = set_dlen, [TCTL] = set_tctl, [TDT] = set_tctl, [MDIC] = set_mdic, [ICS] = set_ics, [TDH] = set_16bit, [RDH] = set_16bit, [RDT] = set_rdt, [IMC] = set_imc, [IMS] = set_ims, [ICR] = set_icr, [EECD] = set_eecd, [RCTL] = set_rx_control, [CTRL] = set_ctrl, [RA ... RA+31] = &mac_writereg, [MTA ... MTA+127] = &mac_writereg, [VFTA ... VFTA+127] = &mac_writereg, }; |
我们这里举个例子就拿[TCTL] = set_tctl发送控制寄存器说事:
set_tctl()
file: e1000.c,
line:
944start_xmit()
file: e1000.c,
line:
630
start_xmit()是上面分析的e1000_receive流程相反的一个函数,其将数据包从Guest的物理内 存,其实就是Qemu的虚拟内存中读出来,处理一下发送描述符之类的操作,最后发个中断通知下Guest发送情况。这样Guest完全感知不到,认为其在 一个真实的环境下完成了收发包处理。
1 2 3 4 5 6 7 8 9 | static void start_xmit(E1000State *s) { ...... base = tx_desc_base(s) + sizeof(struct e1000_tx_desc) * s->mac_reg[TDH]; pci_dma_read(&s->dev, base, &desc, sizeof(desc)); process_tx_desc(s, &desc); ...... set_ics(s, 0, cause); } |
从start_xmit到转发到Tap设备,再到Host的bridge发送出去,该路径为接收的反方向,因此不再详细描述。
Conclusion
至此我们看到了全虚拟化方案下,整个数据流的路径是怎么样的。
该路径非常的复杂,其中包含了多次的数据包内存的拷贝,和频繁的虚拟机退出,模式的切换,因此效率是非常低的。