前言

在上一篇文章(网络虚拟化——QEMU虚拟网卡)中,讨论了经典的网络设备全虚拟化技术。这种技术不需要guest内核对虚拟网络设备有任何的感知和特殊处理,但性能较差,一次读写操作可能会产生多次需要Hypervisor模拟实现的行为。

为了改善虚拟网络设备的性能,各类Hypervisor都提供了自己的半虚拟化网络技术。在这种模式下,guest内核中使用的网络设备和驱动是为虚拟化场景特殊设计和实现的,驱动的实现特别适配了Hypervisor的一些特性,尽量减少了需要Hypervisor介入进行模拟实现的指令数量,从而改善了虚拟网络设备的性能。但随着Linux下Hypervisor类型的不断增加,内核中出现了各种虚拟化专用的网络驱动和设备,这些设备驱动功能基本相同却又有细微的差别,还只能适配特定的Hypervior后端实现。而且这些设备驱动也没有得到充分的维护和性能优化。

因此,到2008年,linux内核社区大佬Rusty Russell提出了virtio的模型和实现:virtio: Towards a De-Facto Standard For Virtual I/O Devices。virtio是一种标准的半虚拟化IO设备模型,Rusty Russell希望通过这个模型,将半虚拟化的IO设备驱动(网卡、磁盘块设备等)统一起来,便于后续的维护、扩展和优化。任何Hypervisor或其他后端都可以根据virtio设备的标准实现virtio设备的后端功能,从而避免继续向内核中加入新的半虚拟化设备驱动。guest用户也可以在不同虚拟化实现下使用相同的虚拟IO设备和功能,不用考虑不同的Hypervisor下还要适配不同虚拟设备的问题。

本文将对virtio技术进行分析和介绍,包括virtio的原理、接口和linux下的虚拟网络实现virtio-net。

本文主要根据Russell的论文内容进行介绍,具体的virtio接口和实现在过去的十多年里必然已经大不相同了,但根本的思想和原理并没有变。

问题

  1. virtio作为通用的IO虚拟化模型,是如何定义通用的IO控制面和数据面接口的?或者说,基于virtio的网络设备virtio-net和块存储设备virtio-blk,有哪些共通点?
  2. 在linux内核下,有virtio、virtio-pci、virtio-net、virtio-blk等virtio相关驱动。这些驱动是如何组织的,多个驱动间是什么关系?
  3. 一个virtio设备,是如何加入到虚拟机设备模型中,被内核发现和驱动的?
  4. virtio-net具体又提供了哪些标准接口?控制面和数据面接口是如何定义的?
  5. virtio技术为虚拟化而产生,但它能否脱离虚拟化环境使用?例如在普通的容器环境或者物理机环境?

virtio

virtio作为一种通用的虚拟IO设备驱动模型,主要定义了两方面的标准模型和接口:控制面的设备配置和初始化,以及数据面的数据传输。

上图是在qemu/kvm虚拟机中实现virtio的架构。可见基本逻辑和其他虚拟网卡是相同的,只是交互方式通过vring队列实现。

控制面定义

virtio的控制面接口可以分为4个部分:

1. 读写特性位

特性位用于device和driver间同步设备特性,例如VIRTIO_NET_F_CSUM表示网卡是否支持checksum offload。driver读取特性位来获取网卡后端支持的特性,driver写入特性位来通知网卡后端需要使用的特性。

2. 读写配置

配置是一个表示设备配置信息的数据结构。driver和device间通过这个结构来获取和设置设备的配置,例如网卡的MAC地址等。

3. 读写状态位

状态位用于driver通知后端自己的初始化进度。driver将状态位设置为VIRTIO_CONFIG_S_DRIVER_OK就表示driver已经完成特性初始化,host在收到这个消息后就可以确定driver需要使用的设备特性。

4. 重启设备

用于移除或者重置virtio设备驱动。

每个virtio设备会有一个virtio_config_ops,其中包括了对上述控制面接口的实现。这些接口的实现和系统提供virtio设备的方式有关,如果是最常见的virtio-pci模式,则这些实现基本上都是对下面会介绍的virtio_pci_common_cfg配置空间的IO读写操作。

virtqueue:数据传输模型

virtio中定义了virtqueue作为guest驱动和host后端间的数据传输结构。块设备只需要一个virtqueue用于数据读写,而网络设备则需要两个virtqueue分别用于网络报文的收和发。

virtqueue是一个队列的抽象模型。guest驱动负责向virtqueue中插入一个个数据buffer,而host后端则负责处理这些buffer。每个buffer都可以由多段不连续的数据空间链接而成,每段数据空间可以有不同的读写权限用于不同的用途。例如用于块设备读取的buffer,可以包含一段guest负责写入的读取信息(位置、长度等),以及一段host负责写入的读取数据内容。buffer的具体结构和设备类型相关。

virtqueue需要支持5个接口,从而实现数据在guest和host间的传输:

struct virtqueue_ops {int (*add_buf)(struct virtqueue *vq,struct scatterlist sg[],unsigned int out_num,unsigned int in_num,void *data);void (*kick)(struct virtqueue *vq);void *(*get_buf)(struct virtqueue *vq,unsigned int *len);void (*disable_cb)(struct virtqueue *vq);bool (*enable_cb)(struct virtqueue *vq);
};

add_buf用于向virtqueue中插入一个待host处理的buffer,参数data是一个由驱动定义的标识符,用于标识buffer;

kick用于通知host有新的buffer加入,需要处理;

get_buf用于从virtqueue中获取一个host处理完成的buffer,返回值就是add_buf时传入的data参数;

disable_cbenable_cb类似于普通设备驱动中的关中断和开中断,用于设置virtqueue的callback函数在host处理完一个buffer后是否会被调用。callback函数是在driver初始化时注册给virtqueue的。

virtio_ring:数据传输实现

virtqueue是数据传输的抽象模型,而virtio_ring则是这个模型的一种高效实现。

一个virtio_ring由三个部分构成:descriptor资源数组、available ring和used ring。

  • descriptor资源数组。
struct vring_desc
{__u64 addr;__u32 len;__u16 flags;__u16 next;
};

每个descriptor可以指示一段内存空间的地址(addr)和长度(len)。多个descriptor可以形成一个链(next),用于表示virtqueue模型中的一个buffer。descriptor还有一个字段flags,用于指示当前descriptor是否是链尾,以及数据段是可读的还是可写的。

  • available ring,用于guest提交descriptor链供host处理。
struct vring_avail
{__u16 flags;__u16 idx;__u16 ring[NUM];
};

这是一个环形队列,ring[NUM]中每个位置保存一个descriptor链的索引(在descriptor资源数组中的下标),idx用于指示最后插入的descriptor链的位置。flags用于guest通知host是否需要在处理完buffer后产生中断。

virtqueue的add_buf就是通过available ring来实现。

  • used ring,用于host返回处理完成的descriptor链。
struct vring_used_elem
{__u32 id;__u32 len;
};struct vring_used
{__u16 flags;__u16 idx;struct vring_used_elem ring[];
};

和available ring一样,used ring也是一个环形队列。flags用于host通知guest是否需要在增加buffer后kick。唯一不同的是,used ring中的每个元素除了包括descriptor index之外,还包括了一个len字段,用于表示host处理后的descriptor链中有效数据的总长度。

virtqueue的get_buf就通过这个ring实现。

descriptor的所有权就一直按《descriptor数组->available ring->used ring->descriptor数组》这个循环不断流转,如下图所示。

值得注意的是,和常见的环形队列不同,vring中并没有对端的消费进度字段。因此guest driver和host backend事实上在向vring中插入元素时是不知道vring中的剩余空间情况的。之所以不用担心vring插入时出现溢出的问题,是因为vring实现时将descriptor数组、available ring和used ring设置成了相同大小。因此只要还有descriptor可以向vring中插入,就说明vring上一定还有空余的位置。

还有一点要说明的是,descriptor链被guest插入available ring的顺序和被host处理完成并插入used ring的顺序不一定是相同的,因为后发出的请求有可能被先执行完成(例如块设备读写,后发的的小块读写可能在先发的大块读写前完成)。那么这里就有个疑问:是否可能顺序靠后的descriptor被回收了导致available ring可以被写入而覆盖了顺序在前的descriptor?这也是不可能的,vring是一个先进先出队列,顺序靠前的descriptor永远被先开始处理,因此当后面的descriptor被回收时,在它前面的descriptor肯定已经被对端处理过了,其descriptor index信息已经不再需要,available ring将descriptor index覆盖也不影响对端对descriptor本身的处理。

virtio设备驱动

基于前文介绍的virtio接口定义和vring实现,可以实现各种类型的virtio设备驱动。目前被广泛使用的virtio驱动主要有两种:virtio-blk用于virtio块设备,以及virtio-net用于virtio网络设备。

virtio-blk

virtio-blk只需要一个virtqueue来发送块读写请求并获取结果。其中每个buffer(descriptor链)由三部分构成:请求信息virtio_blk_outhdr、读写数据段信息和结果状态。一般实现中会把这三部分分别放置在三个descriptor中。

1. virtio_blk_outhdr

struct virtio_blk_outhdr
{__u32 type;__u32 ioprio;__u64 sector;
};

host只读的descriptor

type字段表示请求的类型:读、写、或者其他磁盘操作命令。

ioprio字段表示请求的优先级,数值越大优先级越高,后端可以根据该字段决定请求处理顺序。

sector字段表示读写请求的偏移位置。这里的sector表示偏移位置以扇区(512字节)为单位。

2. 数据段

纯粹的数据段,操作类型决定host可读或可写。

3. 结果状态

只有1个字节,host可写,用于host反馈请求的处理结果是成功(0)、失败(1)或不支持(2)。

virtio-net

virtio-net需要两个virtqueue分别用于网络报文的发送和接收。virtio-net中的buffer也有一个header,用于传递checksum offload和segmentation offload。

struct virtio_net_hdr
{// Use csum_start, csum_offset#define VIRTIO_NET_HDR_F_NEEDS_CSUM 1__u8 flags;#define VIRTIO_NET_HDR_GSO_NONE 0#define VIRTIO_NET_HDR_GSO_TCPV4 1#define VIRTIO_NET_HDR_GSO_UDP 3#define VIRTIO_NET_HDR_GSO_TCPV6 4#define VIRTIO_NET_HDR_GSO_ECN 0x80__u8 gso_type;__u16 hdr_len;__u16 gso_size;__u16 csum_start;__u16 csum_offset;
};

flags、csum_start、csum_offset用于checksum offload,当flags为VIRTIO_NET_HDR_F_NEEDS_CSUM时后端从csum_start位置开始计算checksum并填入csum_offset位置处。

gso_type、hdr_len、gso_size用于segmentation offload,gso_type指示分段的类型,hdr_len表示首部的长度(首部是不能分段的部分,每个报文都要携带),gso_size表示分段后的数据长度(不包括首部)。

后端根据上述字段对descriptor链中的报文数据进行offload的功能处理,当然前提是virtio-net初始化时guest和host协商使用了这些offload功能。

virtio-pci:virtio的PCI设备实现

PCI是目前最常用的通用总线,大部分hypervisor都支持了PCI设备的模拟和增加。因此,virtio也提供了基于PCI总线的探测配置接口和实现,从而提供一套完整的设备发现、配置和运行能力。

virtio-pci上的PCI设备ID为1AF4:1000~1AF4:10FF。1AF4是vendor id,由Qumranet提供,一般virtio后端都默认使用这个ID作为virtio设备的vendor id,Linux中的virtio驱动也只支持这个ID的设备。但也有例外,例如阿里云的神龙网卡提供的virtio-net设备,vendor id就是阿里巴巴自己的vendor id(1DED),驱动这些设备时就需要修改网卡驱动中支持的ID列表。

当PCI总线上出现ID在这个范围的设备时,virtio-pci就会认为是virtio设备并为其注册一个virtio_device设备信息到virtio总线上。virtio-pci本身并不需要知道virtio设备到底是什么类型,而是会遍历已经加载的virtio-net、virtio-blk等virtio驱动来找到合适的驱动。virtio总线只是virtio-pci中的逻辑,因此在linux kernel看来,所有的PCI virtio设备的驱动都是virtio-pci。

virtio-pci设备同样需要通过设备IO来协商设备与驱动的特性和配置。IO空间大概是这样的结构:

struct virtio_pci_io
{__u32 host_features;__u32 guest_features;__u32 vring_page_num;__u16 vring_ring_size;__u16 vring_queue_selector;__u16 vring_queue_notifier;__u8 status;__u8 pci_isr;__u8 config[];
}

其中的字段分别用于获取和配置设备特性、vring地址、kick IO地址、设备状态等。这个结构在Russell的论文中只是概念性的定义。Linux内核的实现中已经有了一些改变。在手头的5.9.11内核中,对应的结构为:

/* Fields in VIRTIO_PCI_CAP_COMMON_CFG: */
struct virtio_pci_common_cfg {/* About the whole device. */__le32 device_feature_select;    /* read-write */__le32 device_feature;      /* read-only */__le32 guest_feature_select; /* read-write */__le32 guest_feature;       /* read-write */__le16 msix_config;     /* read-write */__le16 num_queues;      /* read-only */__u8 device_status;      /* read-write */__u8 config_generation;     /* read-only *//* About a specific virtqueue. */__le16 queue_select;        /* read-write */__le16 queue_size;      /* read-write, power of 2. */__le16 queue_msix_vector;  /* read-write */__le16 queue_enable;        /* read-write */__le16 queue_notify_off;    /* read-only */__le32 queue_desc_lo;        /* read-write */__le32 queue_desc_hi;       /* read-write */__le32 queue_avail_lo;      /* read-write */__le32 queue_avail_hi;      /* read-write */__le32 queue_used_lo;       /* read-write */__le32 queue_used_hi;       /* read-write */
};

字段比上面的更详细,但用途基本是对应的。

小结

上文主要基于Rusty Russell在2008年的virtio论文,介绍了virtio的相关技术原理。virtio技术在这十几年中得到了广泛的应用,但其在linux内核中的驱动实现却和十几年前设计时几乎没有区别,可见virtio设计的通用性、兼容性和可扩展性都非常优秀。

最后我们尝试回答一下开头提出的问题:

1. virtio作为通用的IO虚拟化模型,是如何定义通用的IO控制面和数据面接口的?或者说,基于virtio的网络设备virtio-net和块存储设备virtio-blk,有哪些共通点?

对于控制面,virtio为每个设备封装了virtio_config_ops接口,用于配置和启动设备。

对于数据面,virtio定义了virtqueue抽象传输模型,virtqueue提供了一系列操作接口来完成数据收发和事件通知。virtio_config_ops中的find_vqs接口提供了virtqueue的创建和获取能力。virtqueue具体通过virtio-ring实现,driver向available ring中输入请求,host backend处理请求后向used ring中输入回应。

上述模型和实现是virtio设备通用的,virtio-net和virtio-blk都基于这套模型和接口实现。不同之处只在于使用的virtqueue数量,以及virtqueue/vring中的请求/回应的结构与内容不同,这些都和设备的具体功能和行为密切相关。

2. 在linux内核下,有virtio、virtio-pci、virtio-net、virtio-blk等virtio相关驱动。这些驱动是如何组织的,多个驱动间是什么关系?

linux内核中和virtio相关的驱动主要有:virtio、virtio_ring、virtio_pci、virtio_net、virtio_blk等。其中:

virtio提供了virtio总线和设备控制面的接口。

virtio_ring提供了数据面,也就是virtqueue接口和对应的vring实现。

virtio_pci提供了virtio设备作为PCI设备加载时的通用驱动入口,它依赖virtio和virtio_ring提供的接口。

virtio_net提供了virtio网络设备的标准驱动,它依赖virtio和virtio_ring提供的接口。virtio_net将自己注册为virtio总线的一种设备驱动。

virtio_blk提供了virtio块存储设备的标准驱动,它依赖virtio和virtio_ring提供的接口。virtio_blk将自己注册为virtio总线的一种设备驱动。

3. 一个virtio设备,是如何加入到虚拟机设备模型中,被内核发现和驱动的?

一个virtio PCI设备加载时,内核会尝试所有注册的PCI设备驱动,最后发现可以被virtio_pci驱动。virtio_pci再调用注册到virtio总线上的设备驱动,最后发现可以被virtio_net驱动。virtio_net通过virtio_pci的标准配置接口和host协商设备特性和初始化设备,之后通过virtio_ring提供的接口收发网络数据。

4. virtio-net具体又提供了哪些标准接口?控制面和数据面接口是如何定义的?

virtio设备的控制面和数据面接口都是标准的,只是具体数据格式和含义有区别。virtio-net有自己的feature bit集合,每个virtio-net设备至少使用两个virtqueue用于报文的收和发。virtio-net收发的数据buffer都包括virtio_net_hdr作为头部,用于表示driver和host设置的offload参数。

5. virtio技术为虚拟化而产生,但它能否脱离虚拟化环境使用?例如在普通的容器环境或者物理机环境?

理论上说,virtio设备需要driver和host后端两部分协同完成。在非虚拟化环境下,这个后端可以是内核的vhost模块。vhost模块是在内核中实现的virtio后端功能,是为了进一步提升virtio设备的效率而产生的:

virtio为虚拟IO设备提供了一套标准的接口和实现。同时由于其半虚拟化的特质,virtio驱动在设计和实现时尽可能减少了主要操作路径上会触发host后端操作(vmexit)的指令以提升IO效率。但在执行IO操作时,仍会不可避免的需要触发后端操作。例如virtio-net驱动发包时,在向tx virtqueue写入buffer后必然要kick后端来处理buffer,这个kick就是一个IO写操作。当后端在用户态qemu进程中实现时,这就需要经过guest driver->kvm->qemu->kvm->guest的过程,和普通的虚拟设备驱动是没有区别的,效率仍然低下。为了缩短这个过程,后端实现被放入了内核态,作为一个内核模型/内核线程运行,也就是vhost。有了vhost后,后端操作的流程就变成了guest driver->kvm->vhost->kvm->guest。看似和之前差不多,但是kvm和vhost之间的交互只是一个内核函数调用,性能比之前的kvm和qemu间的用户/内核切换要好的多。同时,使用vhost也提升了后端完成实际IO操作的性能。大部分情况下,后端完成IO操作(例如块设备读写或网络收发)仍然要通过内核接口,例如qemu仍然需要使用文件或socket接口实现,这又需要引入系统调用和状态切换。而使用vhost之后,这些内核能力可以由vhost模块直接调用,又一次减少了状态切换开销。

基于vhost,virtio设备其实不一定需要在虚拟化环境下使用,可以在用户态实现virtio驱动,在初始化时直接与vhost交互完成配置,这样就可以在非虚拟化环境下实现一个用户态的纯虚拟virtio设备。

在下一篇文章中,我们将讨论vhost的原理与实现。

网络虚拟化——virtio相关推荐

  1. 网络虚拟化——vhost

    在上一篇文章(网络虚拟化--virtio)中,我们讨论了virtio技术的由来.原理和实现.virtio为虚拟IO设备提供了一套标准的接口和实现.同时由于其半虚拟化的特质,virtio驱动在设计和实现 ...

  2. 《深入浅出DPDK》读书笔记(十二):DPDK虚拟化技术篇(半虚拟化Virtio)

    Table of Contents 半虚拟化Virtio 132.Virtio使用场景 133.Virtio规范和原理 11.2.1 设备的配置 1. 设备的初始化 2. 设备的发现 3. 传统模式v ...

  3. 网络虚拟化——SR-IOV

    之前的多篇文章介绍了网络虚拟化常见的技术实现,特别是virtio/vhost技术的实现.虽然virtio/vhost架构显著改善了虚拟网络的性能,但virtio网卡仍然是软件模拟的设备,其性能稳定性和 ...

  4. 网络虚拟化——vhost-user

    在上一篇文章(网络虚拟化--vhost_dillanzhou的博客-CSDN博客)中,介绍了vhost技术和vhost-net内核模块的原理.通过将virtio设备后端的数据面功能转移到内核中,vho ...

  5. Linux 的 I / O 虚拟化 virtio 框架详解

    本文原文链接: https://developer.ibm.com/technologies/linux/articles/l-virtio/ Linux的 I / O 虚拟化 Virtio 框架 简 ...

  6. 网络虚拟化——vduse

    在上一篇文章<网络虚拟化-vDPA_dillanzhou的博客-CSDN博客>中,我们讨论了vDPA技术.通过vDPA技术,guest/host上的用户态/内核态virtio驱动都可以通过 ...

  7. KVM 网络虚拟化基础 - 每天5分钟玩转 OpenStack(9)

    网络虚拟化是虚拟化技术中最复杂的部分,学习难度最大. 但因为网络是虚拟化中非常重要的资源,所以再硬的骨头也必须要把它啃下来. 为了让大家对虚拟化网络的复杂程度有一个直观的认识,请看下图 这是 Open ...

  8. 华为dra路由方式分为relay和proxy_华为云计算(3)——网络虚拟化

    ​关注微信公众号IT小组,获取更多干货知识~ 一.网络虚拟化 网络虚拟化在介绍网络虚拟化之前,需要考虑一个问题--为什么需要网络虚拟化呢?之所以要对网络进行虚拟化,是因为在没有进行虚拟化之前,每个服务 ...

  9. 【微软公有云系列】Hyper-v(WinSer 2012 R2)网络虚拟化(三)租户隔离

    前两章讲了挺多的理论知识,是希望让大家有一点了解,对网络基础不太好的朋友们或许看的会有点(◎﹏◎),不过不要紧,心里大概有个概念就行,接下来的会带给大家一些动手实践的Demo,加深大家对网络虚拟化(N ...

最新文章

  1. 用于ONNX的TensorRT后端
  2. hibernate中session接口方法总结
  3. jQuery 表格插件
  4. 外挂学习之路(8)--- 释放技能call
  5. 阿里云应用配置管理ACM发布,重新定义云上配置的存放方式
  6. java控制单元测试_java – 当单元测试控制器时,模拟一个Spring Validator
  7. 关于计算机网络的鼻祖是,笔记本的前世今生!谁才是笔记本的开山鼻祖?
  8. 根据指定字段排序编号(SQL Server 2005,Update,Order By)
  9. mui+vue文件上传(图片)
  10. 差分码、相对码、绝对码、空号差分码、传号差分码
  11. 关于Canvas 常用API汇总
  12. 可汗学院公开课——统计学学习:62-81
  13. Flutter开发:使用SafeArea(安全区域)
  14. Unity 自动化构建方案:一键实现版本管理与打包、压缩
  15. <C++>多态的实战,详解三个具体案例
  16. 从零开始搭建一个前端框架(一)环境准备并完成简单打包
  17. 王者荣耀又开始“神仙”操作,全新的神仙英雄在体验服上线!
  18. 数据结构循环队列C++实现
  19. vs修改程序图标、任务栏图标
  20. 9.3.2 自然连接(NATURAL JOIN)

热门文章

  1. 一些好看的代码注释图案
  2. 计算机专业二本分数线大概是,我高考大概只能考400分了,想学计算机,你们说能到什么学校...
  3. CSS文本溢出省略号
  4. gopath_了解GOPATH
  5. MPLS VPN 跨域-optionB
  6. 无限扩展器dhcp服务器,tplink无线扩展器怎么设置 tplink无线扩展器设置方法
  7. 华为/华三交换机用lldb命令快速查看邻居交换机
  8. 怎样使excel的数据顺序倒过来
  9. 苹果为降频门道歉,品胜为2亿苹果用户省百亿美金
  10. 微信内置浏览器js选择默认浏览器,微信内自动访问手机浏览器的解决方案