Virtio
Virtio 是一个应用广泛的半虚拟化解决方案,是半虚拟化 Hypervisor 的一组通用 I/O 设备的抽象。它提供了一套上层应用与各 Hypervisor 虚拟化设备(KVM、Xen、VMware 等)之间的通信框架和编程接口,减少了跨平台所带来的兼容性问题。客户机需要知道自己运行在虚拟化环境中,进而根据 Virtio 标准和 Hypervisor 协作,从而提高 I/O 性能。
virtio 的架构
半虚拟化方式需要借助 virtio 实现,在 GuestOS 中需要安装前端驱动(块设备驱动、网络设备驱动、PCI 设备驱动等),QEMU 中集中调用后端驱动,两者之间通信通过 virtio-ring 实现。这种方案无需频繁切换上下文,减少了内存拷贝次数,I/O 效率较高,目前是公有云虚拟机选择的主流方案。
Virtio 分为了前端驱动和后端驱动,virtio 本质上是子机和母机间的一套基于共享内存的通信框架
- 前端驱动:Frontend Driver,是位于客户机内核中的驱动程序模块,如
virtio_blk、virtio_net等 - 后端驱动:Backend Driver,在宿主机用户空间的 QEMU 中实现
virtio-vring
virtio-ring 分成三个部分:
- Desc table:描述符表, 目的是存 scatterlist 指向数据的地址和长度
- 有 1024 项条目, 存放数据的 GPA 和长度, 每项数据长度不超过 4k
- 描述符表的添加和删除只能由 guest 完成
- Available Ring:由 Guest 修改, 指向 Desc table 的头部
- Used Ring:由 Host 修改
Vring 特性:
- 只有 host 将请求处理完毕,在 used ring 中对请求做了应答,guest 才能将先前添加的描述符表项删除,比一般的环形队列多了应答的机制。
- guest 将请求添加后通过写 ioport 引起 VMExit 来通知 host,host 处理完请求后通过向 guest 注入中断来通知 guest。
TODO
virtio-blk 读写流程
总体框架图
TODO
阶段 1 Guest Kernel
do_virtblk_request从调度器取出合并后的IO request,调用do_req()开始对 request 进行处理。- 由于单个 request 可能包含多个 BIO,调用
blk_rq_map_sg()将 request 中所有的biovec转换为 scatterlist(sg 条目数等于 biovec 的数目),增加状态 sg 条目。 - 调用
virtqueue_add_buf()将 scatterlist 写入到 vring 的描述符表,更新 available ring 中的条目指向刚刚写入的描述符表的头。 do_virtblk_request会不停的取 IO request,转换为 scatterlist 并写入到 vring,直到 vring 满。vring 满或将调度器的 IO request 取完后,调用 virtqueue_kick()。virtqueue_kick()会写 IO 端口,引起缺页,缺页导致 VMExit,退出到 QEMU,进入到 QEMU 中的 IO 处理函数。
阶段 2 QEMU Host
- VMExit 的退出经过下面的路径,virtio_ioport_write()->virtio_queue_notify()->virtio_queue_notify_vq()->handle_output()进入到 IO 处理函数:
virtio_blk_handle_output() - QEMU 的后端驱动从 Available Ring 取得 I/O 请求,经过 I/O 合并后,异步提交给 QEMU 的通用块层
- 调用
virtqueue_pop()从 vring 中取得 guest 写入的 IO 请求,通过virtqueue_map_sg()将 guest 的物理地址转换为 QEMU 的虚拟地址,转换后,QEMU 就可以直接访问子机的内存 - 将 vring 从取得的 IO 请求转换为
VirtIOBlockReq,由virtio_blk_handle_request()进行处理,对应写操作,如果磁盘块连续,最多可以对 32 个写请求进行合并,合并后再提交到 QEMU 的通用块层;对于读请求,考虑到实时性,不会合并,直接提交。
- 调用
- 对每个 IO 请求,QEMU 都会启动一个协程进行异步执行,执行完毕后,通过
virtqueue_push()更新 vring 的 used ring 的条目指向刚刚执行完毕的 request 在描述符表的头。 - 调用
virtio_notify()向 guest 注入中断,通知 guest 请求已经执行完毕。
阶段 3 Guest Kernel
- guest 在
virtio-blk的中断处理函数中,读取used ring取得已经执行完毕的请求 - 调用
virtqueue_get_buf()释放request占用的buffer,更新available ring,整个 IO 流程完毕
virtio-net 收发包流程
总体框架图
TODO
发包流程
发包流程跟磁盘 IO 读写流程类似,不同的是:
- QEMU 将包写入到 tap 设备后无需通过中断通知 guest,这个跟物理网卡的行为也是一致的
收包流程
TODO
常见问题
Q:virtio-pci 驱动与 virtio-net,virtio-blk 等 virtio 驱动的关系?
A:virtio-pci 是 PCI 驱动,而 virtio-net,virtio-blk 是各种 virtio 设备驱动。PCI 有完整的规范,QEMU 模拟 PCI 设备是非常容易的,因此 virtio 框架采取先模拟一个 PCI 设备,在 virtio-pci 驱动中根据 virtio 设备的类型(blk,net,serial 等)创建一个 virtio 设备,再在子机中用内核的统一设备模型将 virtio 设备管理起来,实现只需要一个 virtio-pci 驱动就可以扩展出各种 virtio 设备的目的。
Q:为什么 virtio-blk 只需要一个 virtqueue(vring),而 virtio-net 需要两个 virtqueue(vring)?
A:对 virtio-blk 而言,无论是读还是写,请求都是从 guest 发起的,提交请求时 guest 填充描述符表,请求执行完毕后 guest 将先前填充的描述符表删除,描述符表的填充和删除只会由 guest 发起,因此,一个 vring 就足够了。网络发包对 vring 的操作与磁盘几乎相同,唯一不同的是,发包成功后 qemu 无需注入中断通知 guest。但是,对收包而言,差异就非常大,必须要求 guest 随时提供足够的描述符表以便在包到达时能够将包拷贝到描述符表对应的内存 buffer 中,如果收发使用同一个 vring,对 vring 的操作会有竞争,增加了复杂性。
Q:网络收包时,host 是否会填充描述符表?
A:不会,描述符表的填充和删除都是由 guest 完成的,guest 只会更新 available ring,host 只会更新 used ring,host 只能从描述符表获取 guest 写入的地址,qemu 根据方向决定是将数据拷贝出来还是向里面拷贝数据,如:对读 IO,将磁盘读到的数据拷贝到 guest buffer;对写 IO,将 guest buffer 的内容写入到磁盘;对发包,将 guest buffer 的数据写入到 tap;对收包,将 tap 读取的包内容拷贝到 guest buffer。
源码阅读
版本要求
Kernel >= 2.6.25的内核都支持 virtio。由于 virtio 的后端处理程序是在位于用户空间中的 QEMU 中实现的,所以宿主机只需要比较新的内核即可,不需要特别编译 virtio 相关驱动。而客户机需要有特定 virtio 驱动程序的支持,以便客户机处理 I/O 操作请求时调用前端驱动。
客户机内核中关于 virtio 的部分配置如下:
|
|
层次结构
如图所示,virtio 大致分为三个层次:前端驱动(位于客户机)、后端驱动(位于 QEMU)以及中间的传输层。
每一个 virtio 设备(块设备、网卡等)在系统层面看来,都是一个 PCI 设备。这些设备之间有共性部分,也有差异部分。
共性部分:
- 都需要挂接相应的 buffer 队列操作 virtqueue_ops
- 都需要申请若干个 buffer 队列,当执行 I/O 输出时,需要向队列写入数据
- 都需要执行 pci_iomap 将设备配置寄存器区间映射到内存区间
- 都需要设置中断处理
- 等中断来了,都需要从队列读出数据,并通知客户机系统,数据已入队
差异部分:
- 与设备相关联的系统、业务、队列中写入的数据含义各不相同
- 例如,网卡在内核中是一个
net_device,与协议栈系统关联起来。同时,向队列中写入什么数据、数据的含义如何,各个设备也不相同。队列中来了什么数据,是什么含义,如何处理,各个设备也不相同
如果每个 virtio 设备都完整的实现自己的功能,就会造成不必要的代码冗余。针对这个问题,virtio 又设计了
virtio_pci模块,以处理所有 virtio 设备的共性部分。这样一来所有的 virtio 设备在系统看来都是一个 PCI 设备,其设备驱动都是virtio_pci
但是,virtio_pci并不能完整的驱动任何一个设备。因此,virtio_pci在调用probe()接管每一个设备时,会根据其virtio_device_id来识别出具体是哪一种设备,然后相应的向内核注册一个 virtio 类型的设备。
在注册设备之前,virtio_pci驱动已经为该设备做了许多共性操作,同时还为该设备提供了各种操作的适配接口,这些都通过virtio_config_ops来适配。
前端代码层次结构
相关源码文件
Kernel 3.10.0中关于 virito 的重要源码文件如下:
|
|
类结构层次
在 virtio 前端驱动即客户机内核中,virtio 的类层次结构如下图所示:
virtio_driver
最顶级的是virtio_driver,在客户机 OS 中表示前端驱动程序,在include/linux/virtio.h中定义:
|
|
virtio_device_id
每个 virtio 设备都有其对应的virtio_device_id,该结构体在include/linux/mod_devicetable.h中定义:
|
|
virtio_device
与驱动程序匹配的设备由virtio_device封装,它表示在客户机 OS 中的设备,在include/linux/virtio.h中定义:
|
|
virtio_config_ops
每一个virtio_device都有一个virtio_config_ops类型的指针*config,它定义了配置 virtio 设备的操作,该结构体在include/linux/virtio_config.h中定义:
|
|
virtqueue
每一个virtqueue包含了对应的virtio_device以及对应的队列操作回调函数,它在include/linux/virtio.h中定义:
|
|
相关数据结构
前端 Kernel
virtio_driver
在include/linux/virtio.h中定义:
|
|
这里的virtio_device_id有两个字段:
|
|
virtio_device
在include/linux/virtio.h中定义:
|
|
virtio_config_ops
在include/linux/virtio_config.h中定义:
|
|
virtqueue
在include/linux/virtio.h中定义:
|
|
后端 QEMU
VirtQueue
在hw/virtio/virtio.c中定义:
|
|
VRing
在hw/virtio/virtio.c中定义:
|
|
参考资料
-
No backlinks found.