泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

泰晓RISC-V实验箱,不会吃灰,开箱即用
请稍侯

半虚拟化技术 - VIRTIO vring 简述

Liu Lichao 创作于 2021/02/03

By 法海 of TinyLab.org Jan 03, 2021

virtio 数据传输机制

前文提到,为了提高虚拟机的 IO 效率,virtio 标准应运而生,virtio 的核心机制就是通过共享内存在前端驱动与后端实现间进行数据传输,共享内存区域被称作 vring。

共享内存为 guest/host 通信奠定了基础,但是依然需要机制保障才能灵活、可靠、高效。本文基于最简单的场景描述 vring 的本质。

vring 共享内存基本原理

virtio vring 本质是共享内存,要求使用共享内存的软件模块可以访问这段内存。在虚拟化场景,guest/host 如何实现共享内存呢?

第一个问题:vring 描述符中存放的内存地址什么?

vring 由 guest 驱动申请,所以 vring 描述符内存放的地址是 GPA。

第二个问题:guest/host 如何实现共享?

总体看有三种情况:

  • 通过 qemu 模拟的设备,GPA 位于 qemu 的进程地址空间,qemu 天然可以访问。
  • qemu 外部模拟的设备,比如 vhost-net/vhost-user,需要建立新的内存映射。

    以 vhost-net 为例简要说明:

    1. 初始化过程中,qemu 通过 ioctl 命令字将 vring 的内存信息通知 vhost-net 内核模块。内存信息包括:GPA/userspace_addr/size 等。
    2. vhost-net 内核模块会记录 GPA 与 userspace_addr(qemu 进程上下文虚拟地址) 的内存映射。
    3. vhost-net 内核模块在启动内核线程时记录此线程为哪个 qemu 虚拟机服务,同时记录 qemu 虚拟机进程的页表信息,在内核线程运行时,使用对应的 qemu 虚拟机进程页表。这样 vhost-net 内核模块就可以访问 qemu 进程上下文的虚拟地址。
  • 对于一个真实的硬件设备,需要使用 IOMMU 辅助完成地址转换。

vring 的构成与实现

vring 定义与构成

摘自 virtio spec:

struct virtq { 
        // The actual descriptors (16 bytes each) 
        struct virtq_desc desc[ Queue Size ]; 
 
        // A ring of available descriptor heads with free-running index. 
        struct virtq_avail avail; 
 
        // Padding to the next Queue Align boundary. 
        u8 pad[ Padding ]; 
 
        // A ring of used descriptor heads with free-running index. 
        struct virtq_used used; 
};

vring 由三大区域构成:

  • Descriptor Table: 描述内存 buffer,主要包括 addr/len 等信息。
  • Available Ring: 用于驱动通知设备有新的可用的描述符。比如,通知后端设备,有一个待发送的报文描述符。
  • Used Ring: 用于通知驱动设备侧已用的描述符。比如,后端设备收到一个报文,需要将报文数据放入可用的描述符,并更新 Used Ring,同时通知前端驱动。

vring 结构

Descriptor Table

Descriptor Table 是描述符数组,每个描述符可以表示一个内存 buffer,结构体如下:

struct virtq_desc {
        le64 addr;
        le32 len;
        le16 flags;
        le16 next;
};

结构体元素含义:

  • addr:内存 buffer 其实地址(GPA)
  • len:长度
  • flags:控制信息
  bit 0
  1 :表示此描述符为 chain 描述符,在其它描述符中有连续数据
  bit 1
  1:write only for device
  0:read only for device
  • next:如果是 chain 类型描述符,此字段表示下一个描述符 id

Available Ring

struct virtq_avail {
        le16 flags;
        le16 idx;
        le16 ring[Queue Size];
};

Available Ring 用于驱动通知设备有可用的描述符。注意:驱动提供了新的可用描述符后,设备侧不一定要立即使用,比如 virtio-net 会提供一些描述符用于报文接收,当报文到达后按需使用这些描述符即可。

结构体元素含义:

flags:控制信息,比如 VIRTQ_AVAIL_F_NO_INTERRUPT 表示驱动侧不想接收通知
idx:驱动将把下一个描述符放在哪里,即 ring 数组的下标
ring[]:avail 描述符在 Descriptor Table 中的 id

Used Ring

struct virtq_used {
        le16 flags;
        le16 idx;
        struct virtq_used_elem ring[Queue Size];
};

struct virtq_used_elem {
        /* Index of start of used descriptor chain. */
        le32 id;
        /* Total length of the descriptor chain which was used (written to) */
        le32 len;
};

Used Ring 用于通知驱动设备侧已经使用的 buffer 和 长度。主要结构成员如上,相比 avail ring 结构多了 len 字段,用于表示设备侧写入的数据长度。对于只读数据类型,不改变 len 长度。

chained 描述符

当传输数据大小大于单个描述符的长度,可以使用多个描述符。这种描述符被称作 chained descriptor。

使用方法

除最后一个描述符外,之前的描述符设置 NEXT flag,desc.next 字段设置为下一个描述符的 id。

下图使用两个描述符描述了一段内存 buffer:

chained 描述符

注意:将 chained descriptors 加入 avail ring 时,只需要把第一个描述符 id 加入即可。

vring 前后端通信过程

具体步骤

  1. 驱动分配 vring
  2. 驱动更新描述符,如下图所示,一个描述符描述了起始地址为 0x8000 的长度为 2000 的设备侧只写的 buffer 区域。

    step 2 驱动更新描述符

  3. 驱动更新 avail ring

     avail->ring[idx] = desc;
     idx++;
    

    step 3 驱动更新 avail ring

  4. 驱动通知设备有新的可用描述符

  5. 设备侧感知到新可用描述符,在使用完成后,更新 Used Ring

    step 5 更新 Used Ring

  6. 设备侧通过虚拟中断方式通知驱动,有新的 Used 描述符,请及时处理

总体框图

step 1-4 总体框图:

总体步骤图 1-4

step 5-6 总体框图:

总体步骤图 5-6

其它问题

  • indirect descriptor

间接描述符表示一个描述符指向的数据还是描述符

indirect desc

  • packed descriptor

packed descriptors 是 virtio spec 1.1 提出的描述符格式。本文不涉及。主要为为了提高性能。

因为 split descriptors 每次操作涉及三个内存区域,cache miss 较多,packed descriptors 将三区域融合到一个区域,提高 cache 利用率。

packed desc

参考资料

  1. https://www.redhat.com/en/blog/virtqueues-and-virtio-ring-how-data-travels
  2. virtio spec 1.1


Read Related:

Read Latest: