Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3569449
  • 博文数量: 205
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 7385
  • 用 户 组: 普通用户
  • 注册时间: 2013-01-23 18:56
个人简介

将晦涩难懂的技术讲的通俗易懂

文章分类

全部博文(205)

文章存档

2024年(8)

2023年(9)

2022年(4)

2021年(12)

2020年(8)

2019年(18)

2018年(19)

2017年(9)

2016年(26)

2015年(18)

2014年(54)

2013年(20)

分类: LINUX

2018-07-08 21:42:48

qemu-kvm 中的那些feature

——lvyilong316

1.整体介绍

我们知道,启动虚拟机时,前后端网络会进行feature的协商,通常我们说的前端就是guest内部的驱动,而后端就是dpdkvhost-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;最后qemuguest之间的传递又有kvm模块的参与,但是这个和我们今天分析的主题无关,所以不是重点。

所以整个协商过程涉及的核心是:dpdk vhost_userqemu:vhost_userqemu:virtio_netguest virtio_net,共4个模块,每个模块都有自己的feature,所以是4feature

另外一点,当我们谈论一个模块的feature时,一定是由三个概念组成:一个是模块代码本身支持的功能,我们称之为全量feature;另一个我们是我们根据实际情况使能(enable)feature;再一个是经过协商实际生效的feature。后面一个依次是前面一个的子集,这样结合上面提到的4个模块,实际上就涉及到了12feature ps:这并不一定代表有12个变量,同一个变量在协商的不同阶段可以表示不同的含义)。

下面结合各个模块的代码逐个分析这些feature的协商。由于整个协商是自后端到前端,再由前端到后端的,即dpdkàqemuàguestàqemuàdpdk,所以我们的分析也会按照这个路径。

2. feature协商过程分析

2.1 dpdk vhost_user初始化

dpdk在初始化时会初始化全局变量:VHOST_FEATURES。它的初始值就是dpdk vhost_user代码功能能够支持的全量feature,我们可以根据需要在程序启动时调用相应的api rte_vhost_feature_disable)禁用掉其中的一些feature,这样全局变量VHOST_FEATURES就变为了我们后端真正使能的feature了。

2.2 qemu 启动初始化后端代理

    qemu启动时,根据启动参数进行初始化,其中就会初始化我们上文提到的qemu:vhost_user结构。具体路径如下:

点击(此处)折叠或打开

  1. vhost_uesr_startàvhost_net_initàvhost_dev_init,在vhost_dev_init中有如下调用:
  2.     r = hdev->vhost_ops->vhost_call(hdev, VHOST_GET_FEATURES, &features);
  3.     if (r < 0) {
  4.         goto fail;
  5. }

这段代码的功能就是向dpdk发送VHOST_GET_FEATURES消息,来获取dpdk vhost_user后端使能的featuredpdk收到这个消息后就返回全局变量VHOST_FEATURES的值,qemu收到这个值后将其记录在struct vhost_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开始。

点击(此处)折叠或打开

  1. uint64_t vhost_net_get_features(struct vhost_net *net, uint64_t features)
  2. {
  3.     /* net->dev->feature: 从vhost_user 后端获取的feature,即前文的vhost_dev.feature
  4.      * vhost_net_get_feature_bits: 返回qemu后端代理,即vhost_dev的全量feature
  5.      * features : qemu前端代理,即VirtIOPCIProxy的host_features
  6.      */
  7.     return vhost_get_features(&net->dev, vhost_net_get_feature_bits(net),
  8.             features);
  9. }
  10. uint64_t vhost_get_features(struct vhost_dev *hdev, const int *feature_bits,
  11.                             uint64_t features)
  12. {
  13.     const int *bit = feature_bits;
  14.     while (*bit != VHOST_INVALID_FEATURE_BIT) {
  15.         uint64_t bit_mask = (1ULL << *bit);
  16.         /*当后端或qemu的后端代理有一方不支持这个feature,就对前端代理的对应feature清除改位,表示不支持*/
  17.         if (!(hdev->features & bit_mask)) {
  18.             features &= ~bit_mask;
  19.         }
  20.         bit++;
  21.     }
  22.     return features;
  23. }

其中的核心逻辑就是:如果后端vhost_userqemu中的qemu:vhost_user有一方不支持的特性,那么qemu:virtio_net也就不支持。这个逻辑我想应该很好理解,比较后端不支持的特性,前端也不应该开启。但是它的实际效果并非这么简单,因为参数feature_bits是一个int数组。准确的说,他是这个样子:

点击(此处)折叠或打开

  1. static const int user_feature_bits[] = {
  2.     VIRTIO_F_NOTIFY_ON_EMPTY, //24
  3.     VIRTIO_RING_F_INDIRECT_DESC, //28
  4.     VIRTIO_RING_F_EVENT_IDX, //29
  5. ……
  6. }

假如把其中第二个元素,也就是VIRTIO_RING_F_INDIRECT_DESC28)删除了,我们回头看vhost_get_features中的循环,就会发现28对应的bit根本不会背叛的,结构就是保持VirtIOPCIProxy. host_features初始化时的对应值,由于之前在DEFINE_VIRTIO_NET_FEATURESenable了全量,所以这个位也就默认开启了。这就是为什么我们的修改把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的处理:

点击(此处)折叠或打开

  1. case VIRTIO_PCI_HOST_FEATURES:
  2.         ret = vdev->host_features;
  3.         break;
  4. ……
  5. return ret;

这里的vdev->host_features就是上面前端代理的VirtIOPCIProxy. host_features到此为止dpdkvhost_user feature经过qemu传递就真正被guest获取到了

下面继续看guest读到后端feature后的处理:

点击(此处)折叠或打开

  1. memset(dev->features, 0, sizeof(dev->features));
  2.          for (i = 0; i < drv->feature_table_size; i++) {
  3.                    unsigned int f = drv->feature_table[i];
  4.                    if (device_features & (1 << f))
  5.                             set_bit(f, dev->features);
  6.          }

其中device_features是刚才从后端获取的featureguest会根据当前驱动(virtio_net)所支持的全量featuredrv->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的处理:

点击(此处)折叠或打开

  1. case VIRTIO_PCI_GUEST_FEATURES:
  2.         /* Guest does not negotiate properly? We have to assume nothing. */
  3.         if (val & (1 << VIRTIO_F_BAD_FEATURE)) {
  4.             val = virtio_bus_get_vdev_bad_features(&proxy->bus);
  5.         }
  6.         virtio_set_features(vdev, val);
  7.         break;

qemu通过调用virtio_set_featuresà virtio_net_set_features à vhost_net_ack_features

vhost_net_ack_features中,有如下操作:

点击(此处)折叠或打开

  1. void vhost_net_ack_features(struct vhost_net *net, unsigned features)
  2. {
  3.     /* 这里的net就是我们前文分析的qemu后端代理,qemu:vhost_user */
  4. net->dev.acked_features = net->dev.backend_features;
  5. ……
  6. }

到此为止,前端就将生效的feature传递到了qemu的后端代理,并存放于acked_features

2.6 qemu feature回馈dpdk

经过前面分析,前端的feature已经反向传递到了qemu,并存放再来后端代理的acked_features,接下来看这个feature是如果传递给后端dpdk的。虽然传递是在qemudpdk之间传递,但是触发还是需要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函数如下:

点击(此处)折叠或打开

  1. static int vhost_dev_set_features(struct vhost_dev *dev, bool enable_log)
  2. {
  3.     uint64_t features = dev->acked_features;
  4.     int r;
  5.     if (enable_log) {
  6.         features |= 0x1ULL << VHOST_F_LOG_ALL;
  7.     }
  8.     r = dev->vhost_ops->vhost_call(dev, VHOST_SET_FEATURES, &features);
  9.     return r < 0 ? -errno : 0;
  10. }

它从后端代理的acked_features中得到之前guest反馈的feature,然后通过VHOST_SET_FEATURES发送给dpdk端。

dpdk端通过以下逻辑将qemu反馈的前端feature设置到后端的vhost_user设备。

点击(此处)折叠或打开

  1. case VHOST_USER_SET_FEATURES:
  2.                    vhost_user_set_features(dev, msg.payload.u64);
  3.                    break;

到此,整个网络虚拟化前后端的feature就完成了。

3.总结

整个前后端的feature涉及的数据结构和消息传递是比较复杂的,看代码的时候也会被各种各样的feature字段搞混,但是我们结合整个前后端协商过程理解,这些feature的含义就比较清晰了。回顾整个协商过程,涉及核心的四个模块,三个进程,整个协商由后端发起,最终再返回给后端结束。最后放一张qemu中,前后端代理的关系图。

阅读(14436) | 评论(5) | 转发(0) |
给主人留下些什么吧!~~

xiaobingbing2019-12-02 14:48:27

图都看不到了呢?

5doumi2019-08-04 14:17:44

struct VirtIOPCIProxy {
    PCIDevice pci_dev;
    MemoryRegion bar;
    union {
        struct {
            VirtIOPCIRegion common;
            VirtIOPCIRegion isr;
            VirtIOPCIRegion device;
    &nb

5doumi2019-08-04 14:17:31

你文章里有个错误,VirtIOPCIProxy数据结构里是没有host_features成员的。
host_features应该在struct VirtIODevice数据结构里。

lvyilong3162019-06-13 23:39:06

hzj_001:很赞,方便加个微信不?

ok

回复 | 举报

hzj_0012019-06-06 16:21:18

很赞,方便加个微信不?