将晦涩难懂的技术讲的通俗易懂
分类: LINUX
2018-07-08 21:42:48
我们知道,启动虚拟机时,前后端网络会进行feature的协商,通常我们说的前端就是guest内部的驱动,而后端就是dpdk的vhost-user。但是guest驱动是不能直接和vhost_user进行协商的,中间需要通过qemu这个中介。 所以应该是图1这个样子。
图1
如果qemu要完成这样一个中介的角色,那么qemu一定需要两个代理(数据结构),这两个代理一个用来和dpdk进行沟通,而另一个用来和guest进行沟通。所以把上图展开的再详细一点,我们就得到了图2。
图2
首先,我们所说的和dpdk协商,本质上是和dpdk中的vhost_user协商,和qemu协商本质上是和guest中的virtio_net驱动进行协商。其次,我们把qemu中用于和dpdk vhost_user进行协商的代理称之为qemu:vhost_user,把qemu中用于和geust virtio_net协商的代理称为qemu:virtio_net;最后qemu和guest之间的传递又有kvm模块的参与,但是这个和我们今天分析的主题无关,所以不是重点。
所以整个协商过程涉及的核心是:dpdk vhost_user,qemu:vhost_user,qemu:virtio_net,guest virtio_net,共4个模块,每个模块都有自己的feature,所以是4个feature。
另外一点,当我们谈论一个模块的feature时,一定是由三个概念组成:一个是模块代码本身支持的功能,我们称之为全量feature;另一个我们是我们根据实际情况使能(enable)的feature;再一个是经过协商实际生效的feature。后面一个依次是前面一个的子集,这样结合上面提到的4个模块,实际上就涉及到了12个feature (ps:这并不一定代表有12个变量,同一个变量在协商的不同阶段可以表示不同的含义)。
下面结合各个模块的代码逐个分析这些feature的协商。由于整个协商是自后端到前端,再由前端到后端的,即dpdkàqemuàguestàqemuàdpdk,所以我们的分析也会按照这个路径。
dpdk在初始化时会初始化全局变量:VHOST_FEATURES。它的初始值就是dpdk vhost_user代码功能能够支持的全量feature,我们可以根据需要在程序启动时调用相应的api (rte_vhost_feature_disable)禁用掉其中的一些feature,这样全局变量VHOST_FEATURES就变为了我们后端真正使能的feature了。
在qemu启动时,根据启动参数进行初始化,其中就会初始化我们上文提到的qemu:vhost_user结构。具体路径如下:
点击(此处)折叠或打开
这段代码的功能就是向dpdk发送VHOST_GET_FEATURES消息,来获取dpdk vhost_user后端使能的feature,dpdk收到这个消息后就返回全局变量VHOST_FEATURES的值,qemu收到这个值后将其记录在struct vhost_dev.feature中,这里的struct vhost_dev就可以理解为我们上文提到的qemu:vhost_user。
我们说的qemu前端代理,即qemu:virtio_net,在qemu代码中对应的实际数据结构为VirtIOPCIProxy,而其host_features成员就是用来存放qemu对前端guest支持的feature的,这个成员在qemu启动中通过如下语句初始化:
DEFINE_VIRTIO_NET_FEATURES(VirtIOPCIProxy, host_features)
通过这个语句,将VirtIOPCIProxy. host_features初始化为qemu对前端支持的全量feature。
qemu启动过程中还会进一步对前端代理进行初始化,具体调用链为:
virtio_pci_device_plugged àvirtio_bus_get_vdev_features àvirtio_net_get_featuresà vhost_net_get_features
关键代码从vhost_net_get_features开始。
点击(此处)折叠或打开
其中的核心逻辑就是:如果后端vhost_user和qemu中的qemu:vhost_user有一方不支持的特性,那么qemu:virtio_net也就不支持。这个逻辑我想应该很好理解,比较后端不支持的特性,前端也不应该开启。但是它的实际效果并非这么简单,因为参数feature_bits是一个int数组。准确的说,他是这个样子:
点击(此处)折叠或打开
假如把其中第二个元素,也就是VIRTIO_RING_F_INDIRECT_DESC(28)删除了,我们回头看vhost_get_features中的循环,就会发现28对应的bit根本不会背叛的,结构就是保持VirtIOPCIProxy. host_features初始化时的对应值,由于之前在DEFINE_VIRTIO_NET_FEATURES中enable了全量,所以这个位也就默认开启了。这就是为什么我们的修改把VIRTIO_NET_F_STATUS这个feature删除了,guest反而可以enable的原因。但这样的结果是qemu的开发者有意为之还是误打误撞,也无从得知,但至少目前我们可以记住这个效果了。
最后不要忘了,返回的feature赋值给了我们前端代理的VirtIOPCIProxy. host_features。
当虚拟机启动的时候,根据扫到pci设备加载驱动,同样前端网络驱动virtio_net也是这里加载的,过程对应virtio_dev_probe的实现。其中有如下调用链:
dev->config->get_features(dev) à vp_get_features
而在vp_get_features中会写寄存器VIRTIO_PCI_HOST_FEATURES,这个写操作会被kvm捕获返回给qemu。然后我们看qemu的处理:
点击(此处)折叠或打开
这里的vdev->host_features就是上面前端代理的VirtIOPCIProxy. host_features,到此为止dpdk端vhost_user 的feature经过qemu传递就真正被guest获取到了。
下面继续看guest读到后端feature后的处理:
点击(此处)折叠或打开
其中device_features是刚才从后端获取的feature,guest会根据当前驱动(virtio_net)所支持的全量feature(drv->feature_table)能力,来和后端获取的feature取个交集,然后存入dev->features,这也是最终在guest内部真正生效的feature。
前面分析了前端驱动根据获取的后端feature和自身驱动支持的feature设置了guest内部真正生效的feature,下面看guest如何把这个生效的前端feature再回馈给我们的dpdk后端,也就是图2的蓝色路径。
首先,是guest经过以下处理:
dev->config->finalize_features(dev) à vp_finalize_features
最终会写寄存器VIRTIO_PCI_GUEST_FEATURES,同样这个写操作被kvm捕获传递给qemu。接下来看qemu的处理:
点击(此处)折叠或打开
qemu通过调用virtio_set_featuresà virtio_net_set_features à vhost_net_ack_features
在vhost_net_ack_features中,有如下操作:
点击(此处)折叠或打开
到此为止,前端就将生效的feature传递到了qemu的后端代理,并存放于acked_features中。
经过前面分析,前端的feature已经反向传递到了qemu,并存放再来后端代理的acked_features,接下来看这个feature是如果传递给后端dpdk的。虽然传递是在qemu和dpdk之间传递,但是触发还是需要guest来完成的。
首先guest加载完成驱动会有以下调用链:
add_status(dev, VIRTIO_CONFIG_S_DRIVER_OK) à vp_set_status
其中vp_set_status 会写寄存器VIRTIO_PCI_STATUS,这个操作也被kvm截获返回给qemu。
然后qemu会有以下调用:
virtio_set_statusà virtio_net_set_statusà virtio_net_vhost_statusà vhost_net_startà vhost_net_start_oneà vhost_dev_startà vhost_dev_set_features
其中vhost_dev_set_features函数如下:
点击(此处)折叠或打开
它从后端代理的acked_features中得到之前guest反馈的feature,然后通过VHOST_SET_FEATURES发送给dpdk端。
dpdk端通过以下逻辑将qemu反馈的前端feature设置到后端的vhost_user设备。
点击(此处)折叠或打开
到此,整个网络虚拟化前后端的feature就完成了。
整个前后端的feature涉及的数据结构和消息传递是比较复杂的,看代码的时候也会被各种各样的feature字段搞混,但是我们结合整个前后端协商过程理解,这些feature的含义就比较清晰了。回顾整个协商过程,涉及核心的四个模块,三个进程,整个协商由后端发起,最终再返回给后端结束。最后放一张qemu中,前后端代理的关系图。