virtio 虚拟化系列之一:从 virtio 论文开始

栏目: 服务器 · 发布时间: 4年前

内容简介:@panic,SmartX 存储研发工程师。背景

作者/公司介绍

@panic,SmartX 存储研发工程师。

SmartX 是中国领先的超融合产品与企业云解决方案提供商 拥有国内最顶尖的分布式存储和超融合架构研发团队 在分布式存储、虚拟化计算、微服务、容器、前端开发、自动化测试等领域都做着行业最前沿的实践。

背景

Virtio 来源于 virtio: towards a de-facto standard for virtual I/O devices 这篇论文[1]。论文发表于 2008 年,已经十来年了,但是它的设计思想依旧不过时,今天来重读一下此文,看看 virtio 是如何统一半虚拟化的。

在那个时代(2008),Linux 作为 Guest OS 已经被多个系统支持,以及用户模式的 Linux 作为一个单独的进程存在。同时,对于 X86 来说,有 3 种 Hypervisor:Xen,KVM,VMWare。但是,当时每一个平台都想要有自己的网络,块设备,console 驱动程序。每个平台都实现自己的虚拟化仿真设备,它们很多相互重叠但是又有些不同,慢慢的性能优化以及驱动的维护都成为问题,所以,急需一个半虚拟化的统一模型,来解决性能以及分裂的麻烦。

论文提出了几个目标,总结来说就是提供两个通用的 ABI,Virtqueue和 Linux API for virtual IO device,以及提供虚拟设备方便的 feature 协商机制以及维持向后兼容性。

PCI 抽象

PCI 配置操作分成以下几个部分:

  • 读写 feature bits;

  • 读写配置空间;

  • 读写 status bits;

  • Device reset;

  • Virtqueue 的创建和销毁

抽象后的操作如下:

struct virtio_config_ops
{
bool (*feature)(struct virtio_device *vdev, unsigned bit);
void (*get)(struct virtio_device *vdev, unsigned offset,
void *buf, unsigned len);
void (*set)(struct virtio_device *vdev, unsigned offset,
const void *buf, unsigned len);
u8 (*get_status)(struct virtio_device *vdev);
void (*set_status)(struct virtio_device *vdev, u8 status);
void (*reset)(struct virtio_device *vdev);
struct virtqueue *(*find_vq)(struct virtio_device *vdev,
unsigned index,
void (*callback)(struct virtqueue *));
void (*del_vq)(struct virtqueue *vq);
};

Feature bits

定义了 Guest 和 Host 支持的功能,例如 VIRTIO_NET_F_CSUM bit 表示网络设备是否支持 checksum offload。feature bits 机制提供了未来扩充功能的灵活性,以及兼容旧设备的能力。

配置空间

一般通过一个数据结构和一个虚拟设备关联,Guest 可以读写此空间。

Status bits

这是一个 8 bits 的长度,Guest 用来标识 device probe 的状态,当 VIRIO_CONFIG_S_DRIVE_OK 被设置,那么 Guest 已经完成了 feature 协商,可以跟 host 进行数据交互了。

Device reset

重置设备,配置和 status bits。

Virtqueue 创建和销毁

find_vq 提供了分配 virtqueue 内存,和 Host 的 IO 空间的初始化操作。

Virtqueues 抽象: 一个传输层抽象

Virtqueues 主要包含了数据的操作。一个虚拟设备可能有一个 virtqueue,或者多个。例如 virtio-blk 只有一个 virtqueue,而 virtio-net/virtio-console 有两个 virtqueue,一个用于输入,一个用于输出。

为什么有些只有一个 virtqueue 就可以,有些需要两个呢?

主要差别是:

  1. 对于 virtio-blk/virtio-scsi,对于读写,IO 发起方都是 Guest OS,所以发起方在进行 IO 操作的时候,均可以通过调用下面的 add_buf,在 avail ring 里面放置请求;

  2. 对于输入输出的驱动,例如 virtio-net,驱动需要随时准备好接受网络数据的缓冲区,也就是说需要提前准备好 avail ring,所以,需要单独占用一个 virtqueue,提前填满空的请求,hypervisor 在收到数据包之后,可以立即放置到接收的 virtqueue 中,并通知 Guest OS。

同时,为了性能,Qemu 和 Guest driver 可以支持为 virtio-blk 创建多个 virtqueues,来支持 multi queue 特性(注:需要块层的 blk-mq 支持)。

Hypervisor 和 Guest OS 之间初始化如下:

  1. 首先,Hypervisor(Qemu) 需要有创建对应的 queue 结构;

  2. 然后 virtio device 通过读取 PCI IO 空间来查询 queue;

  3. Guest OS 随后分配 virtio 的 3 种描述符的内存空间。

对于 Step 2,3,使用 find_vq 这个接口来抽象。

Virtqueue 的操作如下:

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);
};

上述五个操作,定义了 virtuque 的 5 个操作,分成 2 类:

  1. IO 机制实现:add_buf,get_buf

  2. 通知机制实现:kick,disable_cb,enable_cb

Guest OS driver 初始化 Virtqueue 以及提交一个标准的 IO 流程是:

  1. Driver 初始化 virtqueue 结构,调用 find_vq,传入 IO 完成时的回调函数;

  2. 准备请求,调用 add_buf;

  3. Kick 通知后端有新的请求,Qemu/KVM 后端处理请求,先进行地址转换,然后提取数据以及操作,提交给设备;

  4. 请求完成,Qemu/KVM 写 IO 空间触发提前定义好的 MSI 中断,进而进入到 VM,Guest OS 回调被调用,接着 get_buf 被调用,一次 IO 到此全部处理完成;

add_buf

通过 5 个参数的接口定义了所有的通用数据放置的操作。

  • vq 表示一个 virtqueue;

  • sg 定义了一组 scatterlist,这些 sg 是灵魂,数据或者 header 都可以放在这里,自由定义。

  • out_num 表示 sg 中,有多少是 Guest 要丢给 Host 的;

  • in_num 表示 sg 中,有多少是 Guest 需要从 Host 拿过来的;

  • data 表示 private data,完成时 get_buf 返回此数据,一般代表一个 request 的指针。

add_buf 的通用实现是:

将 sg 放入到描述符 table 里面,并且串在一起,然后将第一个 desc idx 放到 avail ring 里面,并存放 data 到数组里。

get_buf

get_buf 的通用实现是:

检查 last_used_idx < used.idx,表示有已经完成的请求需要处理,然后返回 add_buf 存放的 data ,修改 last_used_idx。

kick

通过 PCI 来触发一次通知,表示有新的请求已经准备好了。通用的是现实通过 iowrite 操作来写 PCI 对应的 IO 空间,触发 VMEXIT。

disable_cb

设置 avail flags字段为 VRING_AVAIL_F_NO_INTERRUPT,让 Host 在请求完成后不通知 Guest。

enable_cb

disable_cb 的相反操作。

virtqueue:数据结构以及通信机制

在 virtio 1.1 [2] 之后,有两种内存布局:

  1. 老的 virtqueue 内存布局称为 Split Virtqueues;

  2. 新的 virtqueue 内存布局称为 Packed Virtqueues;

本文不关注 packed virtqueues。

下面是 split virtqueue 的 3 种最基本数据结构的示意图:

virtio 虚拟化系列之一:从 virtio 论文开始

这里面有 3 部分,Desc,Used,Avail。他们在物理内存上是连续的,这样方便寻址和映射。

Desc 定义了数据地址,长度,和 flags 和 Next 指针,可以实现多个 desc 项的串联,如图所示。

Avail 存放已经有数据的 desc 的 idx,Used 存放已经完成的 desc 的 idx,各自都有一个头指针和尾指针,来表示可以消费的区间。

Guest 放置请求

以 virtio-blk 为例,当使用 add_buf 添加一个请求后,描述符变化成下面的结构:

virtio 虚拟化系列之一:从 virtio 论文开始

对于 virtio-blk 来说,读写需要知道以下几个问题:

读写的设备的偏移:

  • virtio_blk_outhdr 头描述了读写的偏移,以及附加的信息,占用一个描述符项。

数据源或者目的地 buffer:

  • Iovec 描述了 buffer 空间的地址和长度,支持 scatter 和 gather IO。对于每一段 scatter/gather 空间,都占用一个描述符项。

操作完成状态:

  • Virtio_blk_inhdr 头描述了 IO 的状态,占用一个描述符项。

上图的 Data 数组是用来存放请求的指针,作 callback 用。在处理完成事件时,通过 get_buf 可以拿到这个指针,然后可以执行完成相关的上层回调。

请求的头部,以及状态和数据部分,是不同的地址区间,依次填充到可用的描述符表里面,并标记是读或者写。最后在添加了新的请求后,avail table 里面会添加了一条记录,指向整个请求的第一个描述符的 index。实际上一个请求,占用的描述符项的数量等于(2 + scatter-gather list 的长度),这种结构有很好的扩展性,可以描述任意类型的 IO 请求。

此后通过写 PCI 的 IO 空间来触发 notify 操作,Host 检查 last_avail_idx 跟 avail->idx 来判断有多少请求需要处理。notify 也会触发 KVM 的 VMEXIT 事件,造成较大开销。virtio 可以利用 flags 以及 features 来控制双向的 notify 频率,降低 VMEXIT 的调用,提高性能。

地址转换

virtio 的一个目标就是提高虚拟设备性能,就要消除数据拷贝,采用共享内存的方式访问数据。virtio 的 virtqueue 在 Guest 和 Host 之间是共享的,但是由于在两个不同的地址空间,一个是 Guest Physical Address, 一个是 Host Virtual Address。virtqueue 在 Guest 端构建并初始化,Host 端只需要经过地址转换来建立对应的 virtqueue 结构即可,不用再重新初始化 virtqueue 结构。

Virtio 在初始化的时候进行第一步的地址映射:

  • Guest 构建好 desc table;

  • 通过写 PCI IO 空间 VIRTIO_PCI_QUEUE_PFN 来告知 Host ,Guest 的 virtqueue 的 GPA 地址;

  • Host 收到了 GPA,然后转换成 Host 的虚拟地址。

因为 Host 是 Qemu 后端,Qemu 给虚拟机提供了内存,所以它知道 Guest OS 的物理地址范围。Qemu 根据自己记录的信息,可以将 gpa 转换成 hva。

Guest 收到完成通知

Host 在处理完请求之后,将 desc 的 head 编号放到 used table 里面,然后构造 irq,通过 ioctl 通知 KVM,有请求完成了。在 Guest driver 初始化的时候,提前注册了PCI 的 irq 的 handler。handler 调用 get_buf 来获取 last_used_idx 到 used->idx 区间,已经完成的请求,从 data 数组里面找到 request 的指针,调用对应的回调即可。

virtio 虚拟化系列之一:从 virtio 论文开始

至此,虚拟化设备的数据通路走通了,没有数据拷贝,高效的实现了数据在 Guest 和 Host 的传递。Guest 里面的 driver 一般叫做前端,Host 里面对应的虚拟硬件叫做后端。

下面看一下 virtio 如何处理 block 的虚拟化的呢。

Virtio-blk 后端

我们先从后端讲起,因为后端相当于一个虚拟的硬件,后端提供什么功能,前端才能使用什么功能。对于 block 设备的虚拟化,后端需要提供 virtio 定义的 PCI 的能力,包括:

  • Feature bits

  • Status bits

  • 配置空间

  • reset

  • ...

其中配置空间比较重要,通过 PCI 提供了Guest 访问 virtio 虚拟硬件的一些参数。对于 virtio-blk,包括基本的磁盘布局信息。

struct virtio_blk_config
{
uint64_t capacity;
uint32_t size_max;
uint32_t seg_max;
uint16_t cylinders;
uint8_t heads;
uint8_t sectors;
uint32_t blk_size;
uint8_t physical_block_exp;
uint8_t alignment_offset;
uint16_t min_io_size;
uint32_t opt_io_size;
} __attribute__((packed));

当 Guest OS 需要访问以上配置信息的时候,只需要调用 ioread 读对应的 offset,就可以读到数值。

同理,Guest OS 也可以通过 iowrite 来写对应的 offset,修改结构。

当完成 PCI 设置注册之后,前端 virtio-blk 调用 probe 来装载驱动。进行 feature 协商,以及基本的 IO 空间配置,此时前后端就可以进行数据传递。

当收到 Guest 的 notify 时,如图所示,Host 根据 last_used_idx 从 desc 表中重构 request,包括 virtio_blk_outhdr,iovec 等,这个过程需要进行地址空间转换,然后提交给 backend,就完成了,过程非常简单。当 IO 完成后,注入中断通知 Guest OS。

Virtio-blk 前端

这是 Linux kernel 里面的一个 PCI 驱动,在 probe 阶段完成:

  1. Virtqueue 的创建;

  2. Feature 的协商;

  3. PCI 配置空间读取 block 设备的空间布局等信息;

在完成 probe 之后,此时前后端就可以进行数据传递。

请求从 block 层到达 virtio-blk 驱动之后,构造 virtio_blk_outhdr,以及 scatterlist,然后通过 add_buf 放入描述符表以及notify host,至此 IO 提交完成。完成事件由中断触发。一个基本的 virtio-blk Guest driver 只需要 300 行左右就可以完成。

总结

Virtio 作者的目标是设计一套通用的,隐藏细节的,前后端方便实现,共享通用代码的虚拟化框架,从分析看来,通过 PCI 和 virtqueue 的抽象,的确能达到目的。

本文对部分细节只进行简单的说明,后续会在以下几个话题开展:

  • Qemu 和 virtio 的内存映射

  • 中断注入的实现

  • Virtio 流控以及性能优化

  • Vhost-user

当前,距离 virtio 问世已经十年有余,现在 virtio 的发展还在继续,包括增加更多的 feature bits,新的内存布局,以及 vhost-user,目的是提供更好的性能,以及更强大的功能。

参考

  1. ozlabs.org/~rusty/virti

  2. docs.oasis-open.org/vir

  3. kernel.org/

  4. qemu.org/

文章转自Linux阅码场


以上所述就是小编给大家介绍的《virtio 虚拟化系列之一:从 virtio 论文开始》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to Computer Science Using Python

Introduction to Computer Science Using Python

Dierbach, Charles / 2012-12 / $ 133.62

Introduction to Computer Science Using Python: A Computational Problem-Solving Focus introduces students to programming and computational problem-solving via a back-to-basics, step-by-step, objects-la......一起来看看 《Introduction to Computer Science Using Python》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

URL 编码/解码
URL 编码/解码

URL 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器