深入分析vhost-user网卡实现原理 —— VirtIO Features协商

文章目录

  • 前言
  • 数据结构
    • 设备模型
      • device
        • VirtIONetPCI
        • VirtIONet
        • NICState
        • NetClientState
      • netdev
        • NetVhostUserState
        • vhost_net
        • vhost_dev
      • chardev
        • chardev
        • ChardevClass
        • SocketChardev
    • Features
      • VirtIONet
      • VirtIODevice
      • vhost_dev
      • NetVhostUserState
      • feature_bits
    • Backend
  • 流程详解
    • 启动过程
    • 网络连接
    • 网卡启动
    • 网卡使能
    • 网卡停用

前言

  • 本文简要介绍vhost-user网卡设备模型,重点解释vhost-user网卡涉及的到的数据结构以及features在各个数据结构中的含义与作用。

数据结构

设备模型

  • vhost网卡的设备模型分为三层,device层作为离guest最近的一层,在guest表现为一个pci设备,netdev次之,作为device的后端为其提供网卡的通用功能,chardev离guest最远,作为netdev的后端为网卡流量转发提供一种具体通信方式。以下qemu命令行表示一张网卡:
-chardev socket,id=charnet1,path=/run/openvswitch/vhu1,server 
-netdev vhost-user,chardev=charnet1,queues=2,id=hostnet1 
-device virtio-net-pci,mrg_rxbuf=on,mq=on,vectors=6,netdev=hostnet1,id=net1,mac=52:54:00:3f:8f:56,bus=pci.0,addr=0x4
  • 三者的依赖关系如下:
    深入分析vhost-user网卡实现原理 —— VirtIO Features协商_第1张图片

device

  • device设备通过如下qemu命令行参数指定,它的默认属性driver为virtio-net-pci,指定id为hostnet1的后端netdev设备:
-device virtio-net-pci,mrg_rxbuf=on,mq=on,vectors=6,netdev=hostnet1,id=net1,mac=52:54:00:3f:8f:56,bus=pci.0,addr=0x4
  • virtio net,virtio device和device继承关系如下:
    深入分析vhost-user网卡实现原理 —— VirtIO Features协商_第2张图片

VirtIONetPCI

  • 网卡设备的driver是virtio-net-pci,driver的名中既有virtio-net,又有pci,可以看出这个数据结构是个复合的数据结构,的确,这个数据结构既描述了网卡在虚机内部的pci信息,又描述了网卡在qemu端的信息。
struct VirtIONetPCI {
    VirtIOPCIProxy parent_obj;	/* 网卡pci设备相关信息*/
    VirtIONet vdev;				/* 主机侧网卡设备状态 */
};

VirtIONet

  • VirtIONet数据结构在virtio-net设备实例化,virtio_net_device_realize函数中被初始化,保存网卡VirtIO数据队列,控制队列,网卡状态,网卡后端类型,网卡MAC地址,后端是否支持TSO、UFO包卸载能力,用户配置的网卡属性等。
struct VirtIONet {
    VirtIODevice parent_obj;
    uint8_t mac[ETH_ALEN];
    uint16_t status;
    VirtIONetQueue *vqs;	/* 网卡数据队列 */
    VirtQueue *ctrl_vq;		/* 控制队列 */
    NICState *nic;			/* 网卡状态 */
	......
    uint32_t has_vnet_hdr;	/* 后端为tap设备的网卡是否有vnet头,用于探测网卡支持的卸载能力 */
    size_t host_hdr_len;
    size_t guest_hdr_len;
    uint64_t host_features;	/* 用户配置的网卡属性,mrg_rxbuf,mq,tso,ufo */
	......
    uint8_t has_ufo;		/* 网卡后端是否支持UDP包卸载 */
    uint32_t mergeable_rx_bufs;	/* 是否开启接收端缓存合并 */
	......
    uint8_t vhost_started;	/* 后端为vhost的网卡是否为启动状态 */
	......
};

NICState

  • NICState核心数据结构是一组地址,保存NetClientState客户端地址,基于NetClientState我们可以查找到网卡设备device层的后端netdev层。
typedef struct NICState {
    NetClientState *ncs;
    NICConf *conf;
    void *opaque;
    bool peer_deleted;
} NICState;

NetClientState

  • 前面我们提到Qemu将网卡设备分为三层,NetClientState就是连接device层和netdev层的数据结构,从数据结构的名字可以想象,对于网卡后端netdev设备,device设备是作为其客户端存在,device和netdev,也可以看成是peer-to-peer的关系:
struct NetClientState {
    NetClientInfo *info;		/* 1 */
    int link_down;				/* 2 */
    QTAILQ_ENTRY(NetClientState) next;	
    NetClientState *peer;		/* 3 */
	......
}

typedef struct NetClientInfo {
    NetClientDriver type;		/* 4 */
	......
}
  1. peer端网卡状态和具体能力信息
  2. peer端网卡是否连接正常
  3. Qemu将所有device层的网卡信息通过next字段链接到全局变量net_clients中,方便查找网卡设备
  4. 对于device持有的NetClientState,它的类型字段type必然是NET_CLIENT_DRIVER_NIC, 对于device对应的具体后端,比如本篇中介绍的vhost-user,或者是tap设备,亦或是vdpa设备,peer侧持有的NetClientState,其类型字段type各不相同。
  • 网卡的driver类型定义如下,对于vhost-user网卡,NetClientState端的类型为NET_CLIENT_DRIVER_NIC,peer端的类型为NET_CLIENT_DRIVER_VHOST_USER,对于vhost-net网卡,NetClientState端的类型为NET_CLIENT_DRIVER_NIC,peer端的类型为NET_CLIENT_DRIVER_TAP
typedef enum NetClientDriver {
    NET_CLIENT_DRIVER_NONE,
    NET_CLIENT_DRIVER_NIC,
    NET_CLIENT_DRIVER_USER,
    NET_CLIENT_DRIVER_TAP,
    NET_CLIENT_DRIVER_L2TPV3,
    NET_CLIENT_DRIVER_SOCKET,
    NET_CLIENT_DRIVER_VDE,
    NET_CLIENT_DRIVER_BRIDGE,
    NET_CLIENT_DRIVER_HUBPORT,
    NET_CLIENT_DRIVER_NETMAP,
    NET_CLIENT_DRIVER_VHOST_USER,
    NET_CLIENT_DRIVER_VHOST_VDPA,
#if defined(CONFIG_VMNET)
    NET_CLIENT_DRIVER_VMNET_HOST,
#endif /* defined(CONFIG_VMNET) */
#if defined(CONFIG_VMNET)
    NET_CLIENT_DRIVER_VMNET_SHARED,
#endif /* defined(CONFIG_VMNET) */
#if defined(CONFIG_VMNET)
    NET_CLIENT_DRIVER_VMNET_BRIDGED,
#endif /* defined(CONFIG_VMNET) */
    NET_CLIENT_DRIVER__MAX,
} NetClientDriver;

netdev

  • netdev设备作为device的后端,默认属性driver是vhost-user,指定id为charnet1作为它的chardev,作为具体的网卡流量转发方式,命令行参数如下:
-netdev vhost-user,chardev=charnet1,queues=2,id=hostnet1

NetVhostUserState

  • NetVhostUserState数据结构有承上启下的作用,它不但将作为客户端的device和实现具体通信的chardev设备,还保存了指向vhost_net设备的地址:
typedef struct NetVhostUserState {
    NetClientState nc;				/* 1 */	
    CharBackend chr; 				/* 2 */	
    VHostNetState *vhost_net;		/* 3 */
	......
    uint64_t acked_features;		/* 4 */
    bool started;					/* 5 */
} NetVhostUserState;
  1. nc字段指向作为client的device侧
  2. vhost-user设备不会直接用socket来实现网卡通信,而是通过CharBackend来间接使用,换句话说,socket不是vhost-user通信唯一方式,vhost-user不关心具体的通信实现,而是交给CharBackend,因此这里chr指向的是一个CharBackend。
  3. vhost-user设备需要实现virtio数据面卸载slave的功能,Qemu侧需要维护相关数据结构,这个结构就是vhost_dev,vhost_net字段间接保存了其地址
  4. 当vhost_net设备判断到slave侧无法工作时,会停止vhost_net设备,释放其内存并删除其数据,在设备停止之前需要保存Guest,Qemu,Slave三者在虚机启动时协商好的virtio-net feature。为什么需要保存?因为virtio-net feature是在Guest virtio-net驱动加载时完成协商,虚机正常运行中通常不会再次协商,除非重新加载virtio-net,因此Qemu需要保留一份在slave恢复工作后重新设置该feature给slave。
  5. started字段表明vhost_net是否启动,Qemu在和前端virtio-net驱动交互过程中,需要基于这个信息判断是否需要进行某些操作,比如当Guest在设置virtio-net的状态时,Qemu在处理过程中会判断vhost_net设备是否启动,如果没有,会首先启动vhost_net设备,因此vhost_net设备正常工作是Guest能够使用virtio-net设备的前提。

vhost_net

  • 其核心字段是dev,指向具体的vhost_dev数据结构:
typedef struct vhost_net VHostNetState;
struct vhost_net {
    struct vhost_dev dev;
	......
};

vhost_dev

  • 所有vhost设备实现VirtIO数据面卸载都通过数据结构vhost_dev来实现,它包含了vhost protocol实现相关的所有信息,这里我们解释几个features字段:
struct vhost_dev {
    VirtIODevice *vdev;
	......
    uint64_t features;				/* 1 */
    uint64_t acked_features;		/* 2 */
    uint64_t backend_features;		/* 3 */
    uint64_t protocol_features;		/* 4 */
	......
};
  1. features, 从slave侧获取到的slave支持的VirtIO Features。
  2. acked_features,经过Guest,Qemu,Slave三者在虚机启动时协商后的VirtIO Features。
  3. backend_features,Qemu维护的Slave定义并支持的Feature集合,这是Slave Specific的,非VirtIO Features。
  4. protocol_features,Vhost 协议支持的feature,由Vhost 协议定义,非VirtIO Features.

chardev

-chardev socket,id=charnet1,path=/run/openvswitch/vhu1,server

chardev

  • TODO
struct Chardev {
    Object parent_obj;	/* 字符设备基类,ChardevClass与Chardev通过指向相同父类的ObjectClass建立联系 */
	......
    CharBackend *be;
	......
    int be_open;		/* 标记字符设备是否被打开 */
	......
    GSource *gsource;
    GMainContext *gcontext;
};

ChardevClass

  • 所有的字符设备,都可以抽象出一组相同的操作,通过ChardevClass来描述,具体的字符设备实现如socket字符设备,可以实现这组操作的子集,当Qemu操作基类字符设备时就可以调用到,对于socket字符设备来说,在char_socket_class_init中注册了socket字符设备的操作:
typedef struct ChardevClass {
    ObjectClass parent_class;
	......
    void (*open)(Chardev *chr, ChardevBackend *backend,			/* qmp_chardev_open_socket */
                 bool *be_opened, Error **errp);
    int (*chr_write)(Chardev *s, const uint8_t *buf, int len);	/* tcp_chr_write */
	......
    int (*chr_wait_connected)(Chardev *chr, Error **errp);		/* tcp_chr_wait_connected */
    void (*chr_disconnect)(Chardev *chr);						/* tcp_chr_disconnect */
	......
} ChardevClass;

SocketChardev

  • chardev字符设备支持多种通信方式,比如采用socket时是ChardevSocket,采用udp时是ChardevUdp,这些设备都是chardev字符设备,即它们的基类都是字符设备。
struct SocketChardev {
    Chardev parent;
    QIOChannel *ioc; /* Client I/O channel */
    QIOChannelSocket *sioc; /* Client master channel */
	......
    SocketAddress *addr;	/* socket设备地址 */
	......
}

Features

  • Qemu在实现vhost-user设备时涉及到很多features,定义在不同的结构体中,我们逐一介绍并解释其意义:

VirtIONet

  • VirtIONet中存放命令行设置的网卡属性:
struct VirtIONet {
	......
	uint64_t host_features;
}

VirtIODevice

  • 根据VirtIO规范,所有VirtIO设备后端提供features,作为device features,前端驱动读取该features后选择自己支持的feature,设置到device作为最终协商的features。
struct VirtIODevice
{
	......
    uint64_t guest_features;	/* 1 */
    uint64_t host_features;		/* 2 */
    uint64_t backend_features;	/* 3 */
};
  1. guest_features,virtio driver按照规范在与host协商时,从pci空间读取的device features中,选择自己支持的features设置到后端,后端在设置完features后(VIRTIO_PCI_COMMON_GF),保存其值到guest_features。相关函数:virtio_set_features
  2. host_features,提供给guest的device features集合,前端virtio driver读取pci空间(VIRTIO_PCI_COMMON_DF)时会返回该值,guest的features只能是device features的子集。它将用户配置并保存在VirtIONet host_features的属性作为输入,再根据VirtIONet关联的peer设备的能力,手动增删部分features,计算出最终features集合,存放到VirtIODevice host_features中。相关函数:virtio_net_get_features
  3. backend_features,如果virtio设备是virtio网卡,且virtio网卡为tap,vhost或者vdpa,数据面会被卸载,该字段存放的是Qemu获取的从slave获取得到的features。相关函数:virtio_net_get_features

vhost_dev

struct vhost_dev {
	......
    uint64_t features;			/* 1 */
    uint64_t acked_features;	/* 2 */
    uint64_t backend_features;	/* 3 */
    uint64_t protocol_features;	/* 4 */
}
  1. 从slave侧通过vhost协议命令字VHOST_USER_GET_FEATURES获取的features。相关函数:vhost_dev_init
  2. Guest,Qemu,Slave协商后最终达成的features。相关函数:virtio_net_set_features
  3. Slave侧定义并支持的features。相关函数:vhost_user_backend_init
  4. Slave侧定义的vhost protocol features。相关函数:vhost_user_backend_init

NetVhostUserState

  • 当Slave侧无法工作时,Qemu侧需要标记vhost_dev设备停止使用,清空数据结构中保存的信息,此时唯一需要保存的是前端virtio 驱动初始化时协商得到的virtio features,acked_features。以便在vhost_dev设备恢复后作为初始值。NetVhostUserState中的acked_features作保存vhost_dev中的acked_features之用。
typedef struct NetVhostUserState {
    NetClientState nc;
	......
    uint64_t acked_features;
} NetVhostUserState;

feature_bits

  • Qemu在响应前端设置的features时,会将结果放到acked_features中,当vhost_dev启动时设置给slave,此操作的前提是slave必须支持这些features,Qemu将vhost-user设备slave侧支持的features硬编码为user_feature_bits,将vhost-net设备slave侧支持的features硬编码为kernel_feature_bits,如下所示。当Qemu在保存前端设置的features时,如果是vhost-user网卡,只允许存在于user_feature_bits中的features被设置,vhost-net网卡亦然。
/* Features supported by others. */
static const int user_feature_bits[] = {
    VIRTIO_F_NOTIFY_ON_EMPTY,
    VIRTIO_RING_F_INDIRECT_DESC,
    VIRTIO_RING_F_EVENT_IDX,

    VIRTIO_F_ANY_LAYOUT,
    VIRTIO_F_VERSION_1,
    VIRTIO_NET_F_CSUM,
    VIRTIO_NET_F_GUEST_CSUM,
    VIRTIO_NET_F_GSO,
    VIRTIO_NET_F_GUEST_TSO4,
    VIRTIO_NET_F_GUEST_TSO6,
    VIRTIO_NET_F_GUEST_ECN,
    VIRTIO_NET_F_GUEST_UFO,
    VIRTIO_NET_F_HOST_TSO4,
    VIRTIO_NET_F_HOST_TSO6,
    VIRTIO_NET_F_HOST_ECN,
    VIRTIO_NET_F_HOST_UFO,
    VIRTIO_NET_F_MRG_RXBUF,
    VIRTIO_NET_F_MTU,
    VIRTIO_F_IOMMU_PLATFORM,
    VIRTIO_F_RING_PACKED,
    VIRTIO_NET_F_RSS,
    VIRTIO_NET_F_HASH_REPORT,
    /* This bit implies RARP isn't sent by QEMU out of band */
    VIRTIO_NET_F_GUEST_ANNOUNCE,
    VIRTIO_NET_F_MQ,
    VHOST_INVALID_FEATURE_BIT
};

/* Features supported by host kernel. */
static const int kernel_feature_bits[] = {
    VIRTIO_F_NOTIFY_ON_EMPTY,
    VIRTIO_RING_F_INDIRECT_DESC,
    VIRTIO_RING_F_EVENT_IDX,
    VIRTIO_NET_F_MRG_RXBUF,
    VIRTIO_F_VERSION_1,
    VIRTIO_NET_F_MTU,
    VIRTIO_F_IOMMU_PLATFORM,
    VIRTIO_F_RING_PACKED,
    VIRTIO_NET_F_HASH_REPORT,
    VHOST_INVALID_FEATURE_BIT
};

Backend

  • backend意指vhost-user网卡的backend,即slave或dpdk,这里我们只介绍一个数据结构vhost_user_socket
struct vhost_user_socket {
	......
	char *path;				/* 1 */
	bool is_server;			/* 2*/
	/*
	 * The "supported_features" indicates the feature bits the
	 * vhost driver supports. The "features" indicates the feature
	 * bits after the rte_vhost_driver_features_disable/enable().
	 * It is also the final feature bits used for vhost-user
	 * features negotiation.
	 */
	uint64_t supported_features;	/* 3 */
	uint64_t features;				/* 4 */
	uint64_t protocol_features;		/* 5 */
	......
}
  1. path,vhost-user网卡的socket路径
  2. is_server,标记dpdk侧是服务端还是客户端,当dpdk为客户端时会周期性的连接Qemu,当dpdk为服务端是会等到Qemu来连接,客户端和服务端只是建立连接的发起者不同,但无论哪种方式vhost协议的协商流程都相同,都是Qemu侧发起。
  3. supported_features,dpdk vhost库默认支持的virtio features。
  4. features,最终提供给Qemu的virtio features。
  5. protocol_features,dpdk支持的vhost protocol features。

流程详解

  • 这里我们用一张图表示vhost-user网卡初始化,features协商的架构与流程,如下:
    深入分析vhost-user网卡实现原理 —— VirtIO Features协商_第3张图片
  • 图示说明参考左上角,特别说明:
  1. 红色数字表示features相关时序,并非整个vhost-user设备生命周期时序
  2. 灰色文档枚举出部分dpdk侧支持的virtio features和vhost protocol features,不一定完整
  • 下面的流程主要围绕vhost-user网卡生命周期中features协商相关过程进行介绍,整个过程需要参考上述图片

启动过程

  • Qemu在启动虚机时,如果配置有vhost-user网卡,在设备初始化时会做如下三个工作:
  1. 字符设备初始化
  2. vhost-user网卡设备初始化
  3. virtio-net设备初始化
  • 以上流程在virtio网络Data Plane卸载原理 —— vhost协议协商流程中有详细介绍,这里不再赘述。

网络连接

  • Qemu在启动过程的设备实例化阶段,会初始化chardev设备,然后初始化netdev设备,在初始化netdev时会检查与slave端的连接是否建立,如果没有则会等待连接建立,然后注册连接到达时的回调接口,初始化netdev设备并等待连接的调用栈如下:
qemu_init
	qemu_create_late_backends
		net_init_clients
			/* 针对每个网卡设备调用初始化函数net_init_netdev */
			qemu_opts_foreach(qemu_find_opts("netdev"),
                          	  net_init_netdev, NULL, errp)) 
              	net_client_init
              		net_client_init1
              			/* 根据网卡类型匹配初始化函数列表,调用对应的初始化函数
              			 * 这里是vhost-user类型,调用net_init_vhost_user函数 */
              			net_client_init_fun[netdev->type](netdev, netdev->id, peer, errp)	<=> net_init_vhost_user
              				net_vhost_user_init
              					do {
              						qemu_chr_fe_wait_connected
              					} while (!s->started);
              						qemu_chr_wait_connected
              							cc->chr_wait_connected	<=> tcp_chr_wait_connected
  • 等待连接的方式随Qemu使能vhost-user网卡的模式而有所差别,Qemu模式为server时,会被动等待slave连接,为client时则主动连接slave,这里我们分析server模式的流程:
tcp_chr_wait_connected
	while (s->state != TCP_CHARDEV_STATE_CONNECTED) {
		if (s->is_listen) {
            tcp_chr_accept_server_sync(chr);
            	qio_net_listener_wait_client
            		g_main_loop_run(loop);
  • 从上可以看到,Qemu作为server时会一直等待客户端连接,连接建立之后,Qemu会注册IO到达的回调函数如下:
net_vhost_user_init
	do {
		qemu_chr_fe_wait_connected(&s->chr, &err);
		qemu_chr_fe_set_handlers(&s->chr, NULL, NULL,
                                 net_vhost_user_event, NULL, nc0->name, NULL,
                                 true);
  	} while (!s->started);
  • 这里的net_vhost_user_event就是连接到达的回调函数,首次启动时slave端发起连接后,此函数就会被调用,在虚机运行过程中,如果连接断开或者重连,net_vhost_user_event也会被调用,vhost-user网卡的启动和停用都在这个回调中实现。

网卡启动

  • vhost-user网卡的启动在slave发起连接后触发,如下:
net_vhost_user_event
	switch (event) {
	case CHR_EVENT_OPENED:
       	vhost_user_start(queues, ncs, s->vhost_user)
			vhost_net_init		
  • 仔细分析网卡启动时vhost_dev的初始化流程:
struct vhost_net *vhost_net_init(VhostNetOptions *options)
{
	int r;
	/* 判断是否为vhost-net网卡,vhost-net网卡的数据面由kernel卸载,因此backend为kernel */
    bool backend_kernel = options->backend_type == VHOST_BACKEND_TYPE_KERNEL;
    /* 为vhost_net结构体分配内存 */
    struct vhost_net *net = g_new0(struct vhost_net, 1);	
    if (backend_kernel) {
    	/* 如果是vhost-net网卡,获取backend的fd,通常就是tap设备的fd */
        r = vhost_net_get_fd(options->net_backend);
        /* TODO */
        net->dev.backend_features = qemu_has_vnet_hdr(options->net_backend)
            ? 0 : (1ULL << VHOST_NET_F_VIRTIO_NET_HDR);
        /* 设置backend文件描述符 */
        net->backend = r;
        net->dev.protocol_features = 0;
    } else {
    	/* 初始化各features字段 */
        net->dev.backend_features = 0;
        net->dev.protocol_features = 0;
        net->backend = -1;

        /* vhost-user needs vq_index to initiate a specific queue pair */
        net->dev.vq_index = net->nc->queue_index * net->dev.nvqs;
    }
	/* 初始化vhost_dev结构体,核心动作是启动vhost_dev设备,完成vhost protocol协商 */
    r = vhost_dev_init(&net->dev, options->opaque,
                       options->backend_type, options->busyloop_timeout,
                       &local_err);
	/* 从NetVhostUserState获取acked_features
	 * 作为vhost_dev acked_features字段的初始值 
	 */
    /* Set sane init value. Override when guest acks. */
    if (net->nc->info->type == NET_CLIENT_DRIVER_VHOST_USER) {
        features = vhost_user_get_acked_features(net->nc);
        if (~net->dev.features & features) {
            fprintf(stderr, "vhost lacks feature mask %" PRIu64
                    " for backend\n",
                    (uint64_t)(~net->dev.features & features));
            goto fail;
        }
    }
	/* 初始化vhost_dev.acked_features字段 */
    vhost_net_ack_features(net, features);
}
  • 两种场景会导致网卡启动,一是虚机首次启动,slave端连接vhost-user的socket路径后,二是slave断开连接后的重连。针对第一种场景,从注释我们可以了解到,vhost_dev.acked_features的初始值来自NetVhostUserState.acked_features,目的是保证其有效性,之后当Guest设置features时会覆盖acked_features。针对第二种场景,NetVhostUserState.acked_features存放的值是vhost_dev停止后保存的值,用于恢复vhost_dev原有的acked_features。这里为什么有两个数据结构来保存同一个acked_features?原因是这两个结构中acked_features的功能不一样,NetVhostUserState中的acked_features是用于备份vhost_dev中的acked_features,vhost_dev停止时会清空数据结构信息,启动时在vhost协议协商过程中会重新加载vhost_dev中的各个字段,唯有acked_features字段不行,因为它需要Guest virtio driver协商,这个时机只在虚机启动或者virtio driver重新加载时才会出现,因此acked_features需要保存留作恢复vhost_dev.acked_features之用。
  • 仔细分析vhost_dev_init中features相关逻辑:
int vhost_dev_init(struct vhost_dev *hdev, void *opaque,
                   VhostBackendType backend_type, uint32_t busyloop_timeout,
                   Error **errp)
{
    uint64_t features;
    vhost_set_backend_type(hdev, backend_type);					/* 1 */
    	dev->vhost_ops = &user_ops;
    hdev->vhost_ops->vhost_backend_init(hdev, opaque, errp);	/* 2 */
    hdev->vhost_ops->vhost_get_features(hdev, &features);		/* 3 */
    hdev->features = features;									/* 4 */
    ......
}
  1. 根据backend类型注册具体的vhost协议操作接口,对于vhost-user设备,注册的接口为user_ops
  2. 调用user_ops的接口vhost_user_backend_init初始化backend。
  3. 调用user_ops的接口vhost_user_get_features获取dpdk侧支持的virtio features
  4. 保存获取的virtio features,自此我们确认vhost_dev.features中保存的是slave侧获取到的virtio features
  • vhost_user_backend_init中会初始化backend_features以及protocol_features:
#define VIRTIO_F_BAD_FEATURE		30

#define VHOST_USER_F_PROTOCOL_FEATURES 30

static int vhost_user_backend_init(struct vhost_dev *dev, void *opaque,
                                   Error **errp)
{
    uint64_t features, protocol_features;
	/* 发送vhost协议VHOST_USER_GET_FEATURES命令字获取dpdk侧支持的features */
    vhost_user_get_features(dev, &features);
    /* 检查dpdk的是否支持VHOST_USER_F_PROTOCOL_FEATURES */
	if (virtio_has_feature(features, VHOST_USER_F_PROTOCOL_FEATURES)) {
		/* 如果支持,将VHOST_USER_F_PROTOCOL_FEATURES设置为后端的features */
        dev->backend_features |= 1ULL << VHOST_USER_F_PROTOCOL_FEATURES;
		/* 发送VHOST_USER_GET_PROTOCOL_FEATURES命令字获取protocol features */
        vhost_user_get_u64(dev, VHOST_USER_GET_PROTOCOL_FEATURES,
                                 &protocol_features);
		/* 保存VHOST_USER_GET_PROTOCOL_FEATURES features到protocol_features */
        dev->protocol_features =
            protocol_features & VHOST_USER_PROTOCOL_FEATURE_MASK;
		......
	}
	.....
}
  • 从上面可以看出,backend_features和virtio features毫无关系,它的作用是保存vhost-user backend定义的features,对于dpdk来说只定义了一个features,就是VHOST_USER_F_PROTOCOL_FEATURES。这里还有一个小trick,dpdk将VHOST_USER_F_PROTOCOL_FEATURESfeature定义为bit 30,这个bit位在virtio规范里被作为保留位,Host置位该bit表示协商失败,Guest永远不会读取并设置该bit位,因此该位被dpdk赋予新的含义,即是否支持VHOST_USER_F_PROTOCOL_FEATURES
  • 如果backend支持VHOST_USER_F_PROTOCOL_FEATURES,则还需要进一步调用vhost协议命令字获取dpdk支持的protocol features并保存。
  • 再次总结网卡启动过程涉及到的feature作用如下:
  1. features:Qemu侧保存的dpdk侧支持的所有features,该值从dpdk侧通过vhost协议查询得到。可以是virtio features,也可以是自定义的features。自定义的features目前只有一个VHOST_USER_F_PROTOCOL_FEATURES,它的bit位不能与virtio features bit位冲突。
  2. acked_features:Qemu侧保存的vhost-user网卡使能的virtio features,这个值只能是virtio features,并且只能是user_feature_bits的其中之一。
  3. backend_features :Qemu侧保存的backend支持的features,dpdk只实现了一个,即VHOST_USER_F_PROTOCOL_FEATURES。与virtio规范定义的features无关。
  4. protocol_features:Qemu侧保存dpdk所支持的所有protocol features,通过vhost协议查询得到。与virtio规范定义的features无关。

网卡使能

  • vhost-user网卡的feature协商时机并不是dpdk连接Qemu的ChardevSocket时,而取决于Guest virtio driver初始化virtio-net设备的顺序,摘抄virtio规范中要求的设备初始化顺序如下:
The driver MUST follow this sequence to initialize a device:
1. Reset the device.
2. Set the ACKNOWLEDGE status bit: the guest OS has noticed the device.
3. Set the DRIVER status bit: the guest OS knows how to drive the device.
4. Read device feature bits, and write the subset of feature bits understood by the OS and driver to the
device. During this step the driver MAY read (but MUST NOT write) the device-specific configuration
fields to check that it can support the device before accepting it.
5. Set the FEATURES_OK status bit. The driver MUST NOT accept new feature bits after this step.
6. Re-read device status to ensure the FEATURES_OK bit is still set: otherwise, the device does not
support our subset of features and the device is unusable.
7. Perform device-specific setup, including discovery of virtqueues for the device, optional per-bus setup,
reading and possibly writing the device’s virtio configuration space, and population of virtqueues.
8. Set the DRIVER_OK status bit. At this point the device is “live”.
  • 第4步,virtio driver读取设备提供的features,device(Qemu)返回VirtIODevice中的host_features,driver选择其中支持的features设置到device,Qemu侧触发如下流程:
virtio_pci_common_write
	switch (addr) {
	case VIRTIO_PCI_COMMON_GF:
		virtio_set_features
			virtio_set_features_nocheck
  • 单独分析virtio_set_features_nocheck函数:
static int virtio_set_features_nocheck(VirtIODevice *vdev, uint64_t val)
{
    VirtioDeviceClass *k = VIRTIO_DEVICE_GET_CLASS(vdev);
    bool bad = (val & ~(vdev->host_features)) != 0;		/* 1 */
    val &= vdev->host_features;							/* 2 */
    if (k->set_features) {
        k->set_features(vdev, val); <=> virtio_net_set_features
    }
    vdev->guest_features = val;							/* 3 */
    return bad ? -1 : 0;
}
  1. 如果guest设置的feature不在device提供的feature中, 返回错误
  2. 将guest设置但host不具备的feature对应bit位去掉
  3. 保存guest设置的feature到guest_features
  • virtio_net_set_features函数最终调用vhost_ack_features,保存Guest设置的feature到acked_features,以备设置features到slave:
vhost_ack_features(&net->dev, vhost_net_get_feature_bits(net), features);
	void vhost_net_ack_features(struct vhost_net *net, uint64_t features)
	{
		/* 将backend_features作为向slave设置feature的初始值 */
   		net->dev.acked_features = net->dev.backend_features;
   		/* guest设置的feature保存到acked_features中 */
    	vhost_ack_features(&net->dev, vhost_net_get_feature_bits(net), features);
	}
  • 遍历slave支持的所有feature user_feature_bits,逐一比较guest是否使能,如果使能则将acked_features对应bit置位:
void vhost_ack_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);
        if (features & bit_mask) {
            hdev->acked_features |= bit_mask;
        }
        bit++;
    }
}
  • 自此我们知道Guest设置的feature信息两个地方有记录,一是VirtIODevice.guest_features,这里保存的feature经过了host_features过滤,二是vhost_dev.acked_features,这里保存的feature不只经过host_feature过滤,还经过user_feature_bits的过滤,只保留了user_feature_bits支持的feature,通常情况下两者是相同的。除此之外,可以确认Guest设置feature后Qemu会保存,但不会立即同步设置给slave。那什么时候设置呢?这是在virtio 规范定义的设备初始化的第5步,设置device状态时。如下:
virtio_pci_common_write
	switch (addr) {
	case VIRTIO_PCI_COMMON_STATUS:
		virtio_set_status(vdev, val & 0xFF);
			k->set_status	<=> virtio_net_set_status
	......
	}
  • virtio_net_set_status在设置状态时会通过VirtIONet.vhost_started首先检查vhost设备是否启动,未启动则会触发启动流程,如下:
virtio_net_set_status
	virtio_net_vhost_status
		if (!n->vhost_started) {
			vhost_net_start
				vhost_net_start_one
		 	n->vhost_started = 1
		}
  • vhost设备启动的过程中,首先是同步Guest在VIRTIO_PCI_COMMON_GF时设置的feature给slave,之后就是核心动作:根据vhost协议,将virtio数据面卸载所需信息传递给slave:
vhost_dev_start
	vhost_dev_set_features
		/* 获取vhost_dev中存放的acked_features */
		uint64_t features = dev->acked_features
		/* 通过VHOST_USER_SET_FEATURES命令字传递给slave */
		dev->vhost_ops->vhost_set_features
	/* 传递虚机内存布局给slave */
	hdev->vhost_ops->vhost_set_mem_table
	/* 传递virtio队列相关信息 */
	vhost_virtqueue_start
  • 总结Guest网卡初始化,有两个阶段会涉及到virtio feature协商,一是在设置features时,此时Guest设置的值会保存到Qemu的vhost_dev.acked_features字段中,二是在设置设备状态为OK时,此时Qemu会检查vhost设备是否已启动,如果没有启动则会启动网卡,这个过程中会调用vhost-user网卡注册的feature设置函数,将保存在acked_features中的features设置给slave。

网卡停用

  • 当slave侧工作异常比如openvswitch重启时,与vhost-user网卡的socket连接会断开,此时vhost-user网卡无法正常工作,Qemu需要触发网卡停用流程,让网卡停止工作,网卡停用和网卡启动类似,在net_vhost_user_event触发:
net_vhost_user_event
	switch (event) {
	case CHR_EVENT_CLOSED:
		/* 触发下半部,在主线程中指向连接断开相关回调 */
		aio_bh_schedule_oneshot(ctx, chr_closed_bh, opaque);
	}
  • 我们最后分析下网卡停用的回调处理:
static void chr_closed_bh(void *opaque)
{
    const char *name = opaque;
    NetClientState *ncs[MAX_QUEUE_NUM];
    NetVhostUserState *s;
    int queues, i;
	/* 遍历所有网卡net_clients,将其中非NET_CLIENT_DRIVER_NIC类型的NetClientState找到
	 * 在vhost-user场景下,即获取所有NET_CLIENT_DRIVER_USER类型网卡的NetClientState
	 */
    queues = qemu_find_net_clients_except(name, ncs,
                                          NET_CLIENT_DRIVER_NIC,
                                          MAX_QUEUE_NUM);
	/* NetVhostUserState的父类是NetClientState
	 * 通过NetClientState找到NetVhostUserState的指针
	 */
    s = DO_UPCAST(NetVhostUserState, nc, ncs[0]);
	/* 针对每个vhost设备,将其acked_features保存到NetVhostUserState的acked_features字段中 */
    for (i = queues -1; i >= 0; i--) {
        s = DO_UPCAST(NetVhostUserState, nc, ncs[i]);
        if (s->vhost_net) {
            s->acked_features = vhost_net_get_acked_features(s->vhost_net);
        }
    }
	/* 标记网卡link状态位down */
    qmp_set_link(name, false, &err);
	/* 仍然更新socket连接的IO回调为net_vhost_user_event */
    qemu_chr_fe_set_handlers(&s->chr, NULL, NULL, net_vhost_user_event,
                             NULL, opaque, NULL, true);
	......
}
  • 综上可以确认在vhost-user socket连接断开时,Qemu会保存acked_features,在连接恢复时也会将acked_features恢复。这样Guest设备初始化时协商的virtio feature就不会丢失。

你可能感兴趣的:(网络虚拟化,VirtIO,虚拟化,网络,虚拟化,Qemu)