前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >virtio 虚拟化系列之一:从 virtio 论文开始(文末有福利~)

virtio 虚拟化系列之一:从 virtio 论文开始(文末有福利~)

作者头像
Linux阅码场
发布2019-07-08 12:16:30
2.3K0
发布2019-07-08 12:16:30
举报
文章被收录于专栏:LINUX阅码场

作者/公司介绍

@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 的创建和销毁

抽象后的操作如下:

代码语言:javascript
复制
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 的操作如下:

代码语言:javascript
复制
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 种最基本数据结构的示意图:

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

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

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

Guest 放置请求

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

对于 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 的指针,调用对应的回调即可。

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

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

Virtio-blk 后端

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

  • Feature bits
  • Status bits
  • 配置空间
  • reset
  • ...

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

代码语言:javascript
复制
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/
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-06-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux阅码场 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 作者/公司介绍
  • PCI 抽象
  • Virtqueues 抽象: 一个传输层抽象
    • add_buf
      • get_buf
        • kick
          • disable_cb
            • enable_cb
            • virtqueue:数据结构以及通信机制
            • Guest 放置请求
            • 地址转换
            • Guest 收到完成通知
            • Virtio-blk 后端
            • Virtio-blk 前端
            • 总结
            • 参考
            相关产品与服务
            对象存储
            对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档