qemu-kvm 中的那些feature
1.整体介绍
我们知道,启动虚拟机时,前后端网络会进行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中用于和dpdkvhost_user进行协商的代理称之为qemu:vhost_user,把qemu中用于和geust virtio_net协商的代理称为qemu:virtio_net;最后qemu和guest之间的传递又有kvm模块的参与,但是这个和我们今天分析的主题无关,所以不是重点。
所以整个协商过程涉及的核心是:dpdkvhost_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,所以我们的分析也会按照这个路径。
2. feature协商过程分析
2.1 dpdkvhost_user初始化
dpdk在初始化时会初始化全局变量:VHOST_FEATURES。它的初始值就是dpdk vhost_user代码功能能够支持的全量feature,我们可以根据需要在程序启动时调用相应的api (rte_vhost_feature_disable)禁用掉其中的一些feature,这样全局变量VHOST_FEATURES就变为了我们后端真正使能的feature了。
2.2 qemu 启动初始化后端代理
在qemu启动时,根据启动参数进行初始化,其中就会初始化我们上文提到的qemu:vhost_user结构。具体路径如下:
点击(此处)折叠或打开
- vhost_uesr_startàvhost_net_initàvhost_dev_init,在vhost_dev_init中有如下调用:
- r = hdev->vhost_ops->vhost_call(hdev, VHOST_GET_FEATURES, &features);
- if (r < 0) {
- goto fail;
- }
这段代码的功能就是向dpdk发送VHOST_GET_FEATURES消息,来获取dpdk vhost_user后端使能的feature,dpdk收到这个消息后就返回全局变量VHOST_FEATURES的值,qemu收到这个值后将其记录在structvhost_dev.feature中,这里的struct vhost_dev就可以理解为我们上文提到的qemu:vhost_user。
2.3 qemu启动初始化前端代理
我们说的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开始。
点击(此处)折叠或打开
- uint64_t vhost_net_get_features(struct vhost_net *net, uint64_t features)
- {
- /* net->dev->feature: 从vhost_user 后端获取的feature,即前文的vhost_dev.feature
- * vhost_net_get_feature_bits: 返回qemu后端代理,即vhost_dev的全量feature
- * features : qemu前端代理,即VirtIOPCIProxy的host_features
- */
- return vhost_get_features(&net->dev, vhost_net_get_feature_bits(net),
- features);
- }
- uint64_t vhost_get_features(struct vhost_dev *hdev, const int *feature_bits,
- uint64_t features)
- {
- const int *bit = feature_bits;
- while (*bit != VHOST_INVALID_FEATURE_BIT) {
- uint64_t bit_mask = (1ULL << *bit);
- /*当后端或qemu的后端代理有一方不支持这个feature,就对前端代理的对应feature清除改位,表示不支持*/
- if (!(hdev->features & bit_mask)) {
- features &= ~bit_mask;
- }
- bit++;
- }
- return features;
- }
其中的核心逻辑就是:如果后端vhost_user和qemu中的qemu:vhost_user有一方不支持的特性,那么qemu:virtio_net也就不支持。这个逻辑我想应该很好理解,比较后端不支持的特性,前端也不应该开启。但是它的实际效果并非这么简单,因为参数feature_bits是一个int数组。准确的说,他是这个样子:
点击(此处)折叠或打开
- static const int user_feature_bits[] = {
- VIRTIO_F_NOTIFY_ON_EMPTY, //24
- VIRTIO_RING_F_INDIRECT_DESC, //28
- VIRTIO_RING_F_EVENT_IDX, //29
- ……
- }
假如把其中第二个元素,也就是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。
2.4 guest 加载驱动获取后端feature
当虚拟机启动的时候,根据扫到pci设备加载驱动,同样前端网络驱动virtio_net也是这里加载的,过程对应virtio_dev_probe的实现。其中有如下调用链:
dev->config->get_features(dev) à vp_get_features
而在vp_get_features中会写寄存器VIRTIO_PCI_HOST_FEATURES,这个写操作会被kvm捕获返回给qemu。然后我们看qemu的处理:
点击(此处)折叠或打开
- case VIRTIO_PCI_HOST_FEATURES:
- ret = vdev->host_features;
- break;
- ……
- return ret;
这里的vdev->host_features就是上面前端代理的VirtIOPCIProxy. host_features,到此为止dpdk端vhost_user 的feature经过qemu传递就真正被guest获取到了。
下面继续看guest读到后端feature后的处理:
点击(此处)折叠或打开
- memset(dev->features, 0, sizeof(dev->features));
- for (i = 0; i < drv->feature_table_size; i++) {
- unsigned int f = drv->feature_table[i];
- if (device_features & (1 << f))
- set_bit(f, dev->features);
- }
其中device_features是刚才从后端获取的feature,guest会根据当前驱动(virtio_net)所支持的全量feature(drv->feature_table)能力,来和后端获取的feature取个交集,然后存入dev->features,这也是最终在guest内部真正生效的feature。
2.5 前端feature回馈qemu
前面分析了前端驱动根据获取的后端feature和自身驱动支持的feature设置了guest内部真正生效的feature,下面看guest如何把这个生效的前端feature再回馈给我们的dpdk后端,也就是图2的蓝色路径。
首先,是guest经过以下处理:
dev->config->finalize_features(dev) à vp_finalize_features
最终会写寄存器VIRTIO_PCI_GUEST_FEATURES,同样这个写操作被kvm捕获传递给qemu。接下来看qemu的处理:
点击(此处)折叠或打开
- case VIRTIO_PCI_GUEST_FEATURES:
- /* Guest does not negotiate properly? We have to assume nothing. */
- if (val & (1 << VIRTIO_F_BAD_FEATURE)) {
- val = virtio_bus_get_vdev_bad_features(&proxy->bus);
- }
- virtio_set_features(vdev, val);
- break;
qemu通过调用virtio_set_featuresà virtio_net_set_features à vhost_net_ack_features
在vhost_net_ack_features中,有如下操作:
点击(此处)折叠或打开
- void vhost_net_ack_features(struct vhost_net *net, unsigned features)
- {
- /* 这里的net就是我们前文分析的qemu后端代理,qemu:vhost_user */
- net->dev.acked_features = net->dev.backend_features;
- ……
- }
到此为止,前端就将生效的feature传递到了qemu的后端代理,并存放于acked_features中。
2.6 qemufeature回馈dpdk
经过前面分析,前端的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函数如下:
点击(此处)折叠或打开
- static int vhost_dev_set_features(struct vhost_dev *dev, bool enable_log)
- {
- uint64_t features = dev->acked_features;
- int r;
- if (enable_log) {
- features |= 0x1ULL << VHOST_F_LOG_ALL;
- }
- r = dev->vhost_ops->vhost_call(dev, VHOST_SET_FEATURES, &features);
- return r < 0 ? -errno : 0;
- }
它从后端代理的acked_features中得到之前guest反馈的feature,然后通过VHOST_SET_FEATURES发送给dpdk端。
dpdk端通过以下逻辑将qemu反馈的前端feature设置到后端的vhost_user设备。
点击(此处)折叠或打开
- case VHOST_USER_SET_FEATURES:
- vhost_user_set_features(dev, msg.payload.u64);
- break;
到此,整个网络虚拟化前后端的feature就完成了。
3.总结
整个前后端的feature涉及的数据结构和消息传递是比较复杂的,看代码的时候也会被各种各样的feature字段搞混,但是我们结合整个前后端协商过程理解,这些feature的含义就比较清晰了。回顾整个协商过程,涉及核心的四个模块,三个进程,整个协商由后端发起,最终再返回给后端结束。最后放一张qemu中,前后端代理的关系图。