virtio网络Data Plane卸载原理——vhost-net master

接口用法

libvirt

  • libvirt在配置virtio网卡时,通过指定driver为vhost使能该特性,如下:
 <interface>
 	  <model type='virtio'/>
      <driver name='vhost'/>
      ......
</interface>

qemu

  • qemu将vhost设计为网络设备的一个属性,virtio网卡指定后端的网络设备为tap设备并使能vhost,用法如下:
-netdev tap,fd=37,id=hostnet0,vhost=on,vhostfd=38 -device virtio-net-pci,netdev=hostnet0,id=net0,mac=24:42:54:20:50:46,bus=pci.0,addr=0x7

数据结构

配置信息

  1. virtqueue的地址,这是组成vring的三个关键元素的主机虚拟地址,包括描述符表基址、可用环虚拟地址、已用环虚拟地址,slave必须知道这些才能取出virtio队列上的数据。
  2. Guest内存布局,在qemu中由一组RAM MemoryRegionSection构成,实际上就是qemu分配给虚机的整个内存。因为virtqueue中存放的是数据的地址(只是元数据),真正的数据散布在虚机的物理地址空间,因此slave必须获得整个虚机的内存布局才能访问virtqueue上指向的数据。
  3. ioeventfd,前端数据可用,内核通过此描述符通知用户态的qemu virtqueue队列数据可用,slave拿到描述符,可以接收前端数据通知,从而对virtqueue上的数据进行处理。
  4. irqfd,后端数据处理完成后,内核通过此描述符向虚机vCPU注入中断,通知前端数据已处理,slave使用此描述符,可以通知到前端virtqueue上数据已处理。
  • 以上4类信息,qemu设计了对应的数据结构来描述,如下:

vhost_vring_addr

  • vhost_vring_addr用于传递一个virtqueue的地址,它将队列的index、描述符表起始地址、已用环地址、可用环地址封装起来,通过VHOST_SET_VRING_ADDR命令字发送给vhost-net
struct vhost_vring_addr {
	unsigned int index;			/* 队列index */
	/* Option flags. */
	unsigned int flags;
	/* Start of array of descriptors (virtually contiguous) */
	uint64_t desc_user_addr;	/* 描述符表起始地址(HVA) */
	/* Used structure address. Must be 32 bit aligned */
	uint64_t used_user_addr;	/* 已用环地址(HVA)*/
	/* Available structure address. Must be 16 bit aligned */
	uint64_t avail_user_addr;	/* 可用环地址(HVA)*/
	......
};

vhost_memory

  • vhost_memory用于描述虚机内存布局,我们知道qemu用MemoryRegion来描述虚拟机的内存,用MemoryRegionSection来描述一段MemoryRegionvhot_memory想要描述内存布局,数据的输入来自这两个结构体,vhost_memory结构体的 vhost_memory_region成员对应MemoryRegionSection结构体,当内存布局被完整搜集后,master通过VHOST_SET_MEM_TABLE命令字发送给vhost-net。实际上,vhost_memory与qemu内存注册到kernel时的kvm_userspace_memory_region设计目的相同,都是作为临时结构体传递给内核。
struct vhost_memory_region {
    uint64_t guest_phys_addr;				/* 内存段的虚机起始物理地址(GPA) */
    uint64_t memory_size; /* bytes 内存段长度 */
    uint64_t userspace_addr;				/* 内存段的起始用户虚拟地址(HVA) */
    uint64_t flags_padding; /* No flags are currently specified. */
};
struct vhost_memory {
    uint32_t nregions;						/* 包含多少个内存段 */
    uint32_t padding;
    struct vhost_memory_region regions[0];	/* 内存段数组基址 */
}; 

vhost_vring_file

  • vhost_vring_file用于保存fd,有两种场景会使用这个数据结构,一种是qemu想传递用于前后端消息通知的fd时(这时每个virtio队列会关联不同的fd),fd封装成此结构体传递给vhost-net;一种是qemu想传递TAP设备的fd时(这时网卡设备上所有virtio队列设置为相同的fd)
struct vhost_vring_file {
	unsigned int index;		/* virtqueue队列索引 */
	/* fd文件描述符
	 * Pass -1 to unbind from file. 
	 **/
	int fd; 				
};

vhost网卡

Netdev

  • qemu支持多种driver的的网络设备,常见的就是网卡、tap设备、vhost-user等,Netdev描述了qemu中的网络设备,它在net.json文件中定义如下:
{ 'union': 'Netdev',
  'base': { 'id': 'str', 'type': 'NetClientDriver' },
  'discriminator': 'type',
  'data': {
    'nic':      'NetLegacyNicOptions',
    'user':     'NetdevUserOptions',
    'tap':      'NetdevTapOptions',	/* tap 设备选项 */
    ......
    'vhost-user': 'NetdevVhostUserOptions',
    'vhost-vdpa': 'NetdevVhostVDPAOptions' } }
  • qemu编译时自动生成对应的数据结构:
struct Netdev {
    char *id;
    NetClientDriver type;
    union { /* union tag is @type */
        NetLegacyNicOptions nic;
        NetdevUserOptions user;
        NetdevTapOptions tap;			/* tap设备作为网络设备后端驱动 */
	   ......
        NetdevVhostUserOptions vhost_user;	/* dpdk设备作为网络设备后端驱动 */
        NetdevVhostVDPAOptions vhost_vdpa;	/* dpdk-vdpa设备作为网络设备后端驱动 */
    } u;
};

NetdevTapOptions

  • netdev指定tap作为driver时,有如下的选项可以设置:
{ 'struct': 'NetdevTapOptions',
  'data': {
    '*ifname':     'str',
    '*fd':         'str',		/* 打开/dev/tun设备获得的tap设备fd,可选,可以由libvirt打开,也可以由qemu自己打开 */
	......
    '*vhost':      'bool',		/* 是否使能vhost特性 */
    '*vhostfd':    'str',		/* 打开/dev/tun设备获得的tap设备fd */
   ......} }
  • qemu编译时自动生成对应的数据结构:
struct NetdevTapOptions {
    bool has_ifname;
    char *ifname;
    bool has_fd;
    char *fd;
   ......
    bool has_vhost;
    bool vhost;					/* 是否使能vhost特性 */	
    bool has_vhostfd;
    char *vhostfd;
	......
};
  • 再次分析qemu的命令行,tap作为driver时,使能了vhost特性,并指定fd和vhostfd。其中fd是libvirt通过打开/dev/tun字符设备获得并传递给qemu的,vhostfd是libvirt通过打开/dev/vhost-net字符设备获得并传递给qemu的。
-netdev tap,fd=37,id=hostnet0,vhost=on,vhostfd=38 -device virtio-net-pci,netdev=hostnet0,id=net0,mac=24:42:54:20:50:46,bus=pci.0,addr=0x7

流程

  • 在分析设备初始化之前,我们解析libvirt传递的tap设备命令行参数如下:
has_fd = true
fd = "37"
has_vhost = true
vhost = true
has_vhostfd = true
vhostfd = "38"

netdev初始化

  • qemu主进程在启动时解析到-netdev,如果指定tap driver实现网络报文收发,libvirt会事先创建好tap设备,qemu只需要取出从libvirt传来的tap设备对应fd,类似迁移时从libvirt获取fd的过程,网络设备的初始化在qemu解析完参数后,由qemu_create_late_backends进入:
qemu_init
	qemu_create_late_backends
		net_init_clients
			qemu_opts_foreach(qemu_find_opts("netdev"),  net_init_netdev, NULL, errp)     
				net_init_netdev
					net_client_init
						net_client_init1
							net_client_init_fun[netdev->type](netdev, netdev->id, peer, errp)	<=>	net_init_tap
  • qemu主进程在初始化时会通过qemu_create_late_backends创建所有qemu设备模拟需要用到的后端设备,因为qemu是借助linux的tap设备(或者其它工具)模拟网卡,因此网卡设备的后端需要在这里提前准备好,供qemu适用。最终调用net_init_tap初始化tap设备。
  • net_init_tap根据qemu命令行参数进行对应的处理,如下:
net_init_tap
	if (tap->has_fd) {
		......
        fd = monitor_fd_param(cur_mon, tap->fd, &err);		/* 获取libvirt传来的tap设备fd */
        qemu_set_nonblock(fd);								/* 设置tap fd非阻塞 */
		......
        vnet_hdr = tap_probe_vnet_hdr(fd);				 	/* 检查kernel tap设备是否支持IFF_VNET_HDR特性 */	
        net_init_tap_one(tap, peer, "tap", name, NULL,
                         script, downscript,
                         vhostfdname, vnet_hdr, fd, &err);
		......
    }
  • vhost特性使能在tap设备初始化中完成,继续分析net_init_tap_one
net_init_tap_one
	if (tap->has_vhost ? tap->vhost :
        vhostfdname || (tap->has_vhostforce && tap->vhostforce)) {		/* 如果使能了tap设备vhost*/
        options.backend_type = VHOST_BACKEND_TYPE_KERNEL;				/* 设置vhost后端为kernel */
       	if (vhostfdname) {												/* 如果libvirt传入了vhostfd,这里的vhostfdname就不为空,我们的情况就是这样 */
            vhostfd = monitor_fd_param(cur_mon, vhostfdname, &err);		/* 取出vhostfd */
			......
            qemu_set_nonblock(vhostfd);									/* 设置vhostfd为非阻塞 */
        } else {														/* 如果libvirt没有传入vhostfd,qemu自己打开字符设备获取vhostfd */
            vhostfd = open("/dev/vhost-net", O_RDWR);					/* 从这里也能看到,vhostfd就是通过打开/dev/vhost-net字符设备获取的 */
			......
            qemu_set_nonblock(vhostfd);
        }
  • 到这里,针对tap设备的初始化和vhost特性的使能的相关工作基本结束,这个过程比较简单,没有实质性的内容,qemu只是根据参数将libvirt传来的fd获取到,并初始化相关数据结构。在获取到vhostfd之后,qemu会配置vhost,这个过程会涉及vhost protocol相关内容。

vhost后端驱动注册

  • qemu在获取vhostfd之后会对vhost进行配置,核心工作就是注册vhost api,如果是kernel作为vhost后端,就是注册kernel_ops作为vhost api,这个过程在vhost_net_init中完成,如下:
net_init_tap_one
	vhost_net_init
		vhost_dev_init
			vhost_set_backend_type
				dev->vhost_ops = &kernel_ops	/* 设置vhost设备的后端驱动的API */
			for (i = 0; i < hdev->nvqs; ++i, ++n_initialized_vqs) {
        		vhost_virtqueue_init(hdev, hdev->vqs + i, hdev->vq_index + i);
        	}
  • 针对每个vhost设备设置后端驱动,从这里可以看出,同一个虚机的不同vhost设备,可以有不同的后端驱动,比如kernel、用户态的dpdk或者用户态的dpdk-vdpa。tap设备的初始化还包括对virtio队列的初始化,这个在vhost_virtqueue_init中实现。

配置信息传输

eventfd

  • vhost驱动注册完成后,会依次为每个virtio队列创建eventfd并作为irqfd使用,传统的流程,每当qemu需要通知虚机virtio队列上的数据处理完成,就会往irqfd的wfd写1通知内核,另一端kvm读irqfd的rfd为1,则直接发起中断注入通知虚机。使能vhost特性之后,qemu不再处理virtio队列,需要将通知kvm的工作交给内核中的vhost-net来做,所以qemu创建eventfd完成后,需要将eventfd的rfd传递给了vhost-net,vhost-net根据rfd找到eventfd在内核的上下文eventfd_ctx,之后便使用此数据结构通知kvm注入中断,qemu将不再参与对virtio队列的处理。qemu通过VHOST_SET_VRING_CALL命令字传递irqfd,这个过程在vhost_virtqueue_init中完成,如下:
vhost_virtqueue_init
	event_notifier_init(&vq->masked_notifier, 0)						/* 创建irqfd */
		file.fd = event_notifier_get_fd(&vq->masked_notifier)			/* 取出irqfd的rfd传递给内核 */
			dev->vhost_ops->vhost_set_vring_call(dev, &file)	<=>	vhost_kernel_set_vring_call
				vhost_kernel_call(dev, VHOST_SET_VRING_CALL, file)		/* 通过VHOST_SET_VRING_CALL命令字传递fd */

virtqueue addr

  • virtio队列由描述符表、可用环、已处理环三个元素组成,虚机创建这三个元素后,将这三个元素的地址(GPA)写到VIRTIO_PCI_CAP_COMMON_CFG空间的queue_desc、 queue_avail、queue_used三个字段,通知qemu。qemu获取地址后将其翻译成(HVA),在主机侧创建对应的描述符表、可用环、已处理数据结构维护起来,从而实现virtio队列共享。这是传统的virtio队列初始化流程。使能vhost特性之后,除了完成上面的工作,在virtio探测到virtio设备加载驱动时,只要往VIRTIO_PCI_CAP_COMMON_CFGdevice_status字段写入状态(首先是ACKNOWLEDGE、然后是DRIVERDRIVER_OK),后端qemu就会对应的设置virtio设备的状态,对于vhost使能了net设备,就会启动vhost net设备,其中有一个核心步骤是传递virtio设备的地址到内核,流程如下:
virtio_ioport_write
	case VIRTIO_PCI_STATUS:
		virtio_set_status
			k->set_status(vdev, val)	<=>	virtio_net_set_status
				virtio_net_set_status
					virtio_net_vhost_status
						vhost_net_start
							for (i = 0; i < nvhosts; i++) {
								vhost_net_start_one(get_vhost_net(peer), dev);
								vhost_set_vring_enable(peer, peer->vring_enable);
							}
								vhost_dev_start
									hdev->vhost_ops->vhost_set_mem_table(hdev, hdev->mem)	<=> vhost_kernel_set_mem_table
									vhost_virtqueue_start									
  • vhost_virtqueue_start中会搜集qemu维护的virtiqueue相关信息,放到vhost_devvhost_virtqueue结构体中,当需要传递virtiqueue相关信息时,直接从该结构体取需要的信息即可,传递到内核的virtqueue地址vhost_vring_addr 便是如此,流程如下:同样从vhost_virtqueue取出信息组装
vhost_virtqueue_start
	vq->num = state.num = virtio_queue_get_num(vdev, idx);
	vq->desc_size = s = l = virtio_queue_get_desc_size(vdev, idx);
	
	a = virtio_queue_get_desc_addr(vdev, idx);
	vq->desc_phys = a;
	vq->desc = vhost_memory_map(dev, a, &l, false);
	
	vq->avail_size = s = l = virtio_queue_get_avail_size(vdev, idx);
    vq->avail_phys = a = virtio_queue_get_avail_addr(vdev, idx);
    vq->avail = vhost_memory_map(dev, a, &l, false);
    
   	vq->used_size = s = l = virtio_queue_get_used_size(vdev, idx);
    vq->used_phys = a = virtio_queue_get_used_addr(vdev, idx);
    vq->used = vhost_memory_map(dev, a, &l, true);
    
    vhost_virtqueue_set_addr(dev, vq, vhost_vq_index, dev->log_enabled);
    	dev->vhost_ops->vhost_set_vring_addr(dev, &addr)	<=>	vhost_kernel_set_vring_addr
    		vhost_kernel_call(dev, VHOST_SET_VRING_ADDR, addr)

guest memory layout

  • 在qemu启动vhost net的流程中,传递virtio队列地址前,还有一步是传递虚机的内存布局,同样在vhost_dev_start中完成:
vhost_dev_start
	hdev->vhost_ops->vhost_set_mem_table(hdev, hdev->mem)	<=> vhost_kernel_set_mem_table
		vhost_kernel_call(dev, VHOST_SET_MEM_TABLE, mem)
  • 这个流程很简单,将vhost_dev中的vhost_memory信息传递给内核,该信息就是guest memory的布局,再看看这个信息怎么搜集得来的,hdev->mem在vhost设备初始化时被分配好空间,如下:
vhost_dev_init
	hdev->mem = g_malloc0(offsetof(struct vhost_memory, regions));
  • 这里qemu使用了一个小trick,只为结构体的nregions和padding成员分配了内存,vhost_memory_region成员并没有分配空间,因为它后续会被用来动态分配,贴上vhost_memory结构体:
struct vhost_memory {
	uint32_t nregions;
	uint32_t padding;
	struct vhost_memory_region regions[0];
};
  • offsetof宏的意思就是取结构体(TYPE)的变量成员(MEMBER)在此结构体中的偏移量:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
  • 我们还注意到,qemu使用g_malloc0函数分配内存,这段内存会被初始化为0,因此nregions成员默认为0。vhost_memory结构体中regions成员未分配空间,继续分析regions空间在哪里实现的。回到vhost_dev_init函数:
vhost_dev_init
	hdev->memory_listener = (MemoryListener) {
        .name = "vhost",
        .begin = vhost_begin,
        .commit = vhost_commit,				/* 1 */
        .region_add = vhost_region_addnop,
        .region_nop = vhost_region_addnop,
        .log_start = vhost_log_start,
        .log_stop = vhost_log_stop,
        .log_sync = vhost_log_sync,			/* 2 */
        .log_global_start = vhost_log_global_start,
        .log_global_stop = vhost_log_global_stop,
        .eventfd_add = vhost_eventfd_add,
        .eventfd_del = vhost_eventfd_del,
        .priority = 10
    };
	memory_listener_register(&hdev->memory_listener, &address_space_memory);
  • vhost网络设备在初始化时注册了一个memory_listener,为什么需要这个?首先我们知道memory_listener的作用是当虚机内存发生变化时,其它模块

其它信息传输

TAP设备fd

  • qemu初始化vhost-net网卡时会打开内核提供的/dev/tun字符设备,返回一个fd用于读写网络包,在virtio-net场景下这个fd供qemu使用,但在vhost-net场景下,显然fd会被内核使用,进一步说是被内核的vhost-net模块使用。那么初始化vhost-net网卡时qemu也需要将这个fd传递给内核。仔细看头文件中的解释:
/* Attach virtio net ring to a raw socket, or tap device.
 * The socket must be already bound to an ethernet device, this device will be
 * used for transmit.  Pass fd -1 to unbind from the socket and the transmit
 * device.  This can be used to stop the ring (e.g. for migration). */
#define VHOST_NET_SET_BACKEND _IOW(VHOST_VIRTIO, 0x30, struct vhost_vring_file)
  • 让内核将virtio net的队列与socket或者tap设备关联,传入的vhost_vring_file中包含的fd信息可以是打开一个tap设备返回的fd,也可以是打开一个socket返回的fd。内核的工作就是将其与网卡的virtio队列关联起来,在后续的收发包时使用。
  • 流程在vhost_net_start_one函数中完成,如下:
vhost_net_start_one
	vhost_dev_start(&net->dev, dev);
	    if (net->nc->info->type == NET_CLIENT_DRIVER_TAP) {
        	qemu_set_fd_handler(net->backend, NULL, NULL, NULL);
        	file.fd = net->backend;
        	for (file.index = 0; file.index < net->dev.nvqs; ++file.index) {
           		if (!virtio_queue_enabled(dev, net->dev.vq_index +
                                      file.index)) {
                	/* Queue might not be ready for start */
                	continue;
            	}
            	vhost_net_set_backend(&net->dev, &file);	<=>	vhost_kernel_net_set_backend
        	}
    	}
  • 网络包在内核的发包流程:
vhost_net_open
	vhost_poll_init
		/* 用户态程序只要通过/dev/vhost-net打开字符设备
		 * 得到fd往里面写数据时就是触发注册好的handle_tx_net钩子函数 */
		vhost_poll_init(n->poll + VHOST_NET_VQ_TX, handle_tx_net, EPOLLOUT, dev);
handle_tx_net
	handle_tx
		sock = vhost_vq_get_backend(vq)
			handle_tx_copy(net, sock)
				sock->ops->sendmsg(sock, &msg, len)
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

享乐主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值