virtio 框架学习,更新中…
1. virtio 概述
KVM 是必须依赖硬件虚拟化技术辅助(例如 Intel VT-x、AMD-V)的 Hypervisor:
- CPU:有 VMX root 和 non-root 模式的支持,其运行效率是比较高的
- 内存:有 Intel EPT/AMD NPT 的支持,内存虚拟化的效率也比较高
- I/O:KVM 客户机的 I/O 操作需要
VM-Exit
到用户态由 QEMU 进行模拟。传统的方式是使用纯软件的方式来模拟 I/O 设备,效率并不高
为了解决 I/O 虚拟化效率低下的问题,可以在客户机中使用半虚拟化驱动(Paravirtualized Drivers,PV Drivers)来提高客户机的 I/O 性能。目前,KVM 中实现半虚拟化驱动的方式就是采用了
virtio
这个 Linux 下的设备驱动标准框架。
virtio 是 Linux 平台下一种 I/O 半虚拟化框架,由澳大利亚程序员 Rusty Russell 开发。他当时的目的是支持自己的虚拟化解决方案 Lguest,而在 KVM 中也广泛使用了 Virtio 作为半虚拟化 I/O 框架。
2. 软件方式模拟 I/O 设备
2.1 基本原理
下图是 QEMU 以纯软件方式模拟 I/O 设备的示意图:
- 当 Guest 中的设备驱动程序发起 I/O 操作请求时,KVM 模块中的 I/O Trap Code 会拦截这次 I/O 请求,经过处理后将本次 I/O 请求的信息存放到 I/O sharing page 中,并通知用户空间的 QEMU
- QEMU 从 I/O sharing page 中获得 I/O 操作的具体信息后,交由硬件模拟代码(QEMU I/O Emulation Code)来模拟本次 I/O 操作
- 模拟代码负责和实际的设备驱动进行交互,模拟此次 I/O 操作,获取返回结果并将其放回 I/O Sharing Page 中
- 最后,KVM 中的 I/O Trap Code 负责读取 I/O Sharing Page 中的操作结果,并将结果返回到客户机中
需要注意的是:
- 客户机作为一个QEMU 进程,在等待 I/O 时也可能被阻塞
- 当客户机通过 DMA 方式访问大块 I/O 时,QEMU 不会把 I/O 操作结果放到 I/O 共享页中,而是通过内存映射的方式将结果直接写进客户机的内存中,然后通过 KVM 模块告诉客户机 DMA 操作已经完成
2.2 优缺点
- 优点:可以通过软件模拟出各类硬件设备,而无需修改客户机操作系统
- 缺点:每次 I/O 操作的路径较长,有较多的
VM-Entry
、VM-Exit
发生,需要多次上下文切换,也需要多次数据复制,因此性能较差
3. 半虚拟化 I/O 框架 virtio
3.1 基本原理
如图所示,virtio 分为了前端驱动和后端驱动:
- 前端驱动 frontend:如
virtio_blk
、virtio_net
等,是在客户机中存在的驱动程序模块 - 后端驱动 backend:在 QEMU 中实现
在前后端驱动之间,还定义了两层来支持客户机和 QEMU 之间的通信:
- virtio 层:虚拟队列接口,它在概念上将前端驱动程序附加到后端处理程序。一个前端驱动程序可以使用 0 个或多个队列,具体数量取决于需求
例如:
virtio_net
网络驱动程序使用两个虚拟队列(接收/发送),而virtio_blk
驱动仅使用一个虚拟队列。
虚拟队列实际上被实现为客户机操作系统和 Hypervisor 之间的衔接点,但它可以通过任意方式实现,前提是客户机操作系统和 virtio 后端程序都遵循一定的标准,以相互匹配的方式实现它。
- virtio-ring 层:实现了环形缓冲区(ring buffer),用于保存前端驱动和后端处理程序执行的信息,并且它可以一次性保存前端驱动的多次 I/O 请求,再交由后端驱动批量处理,最后实际调用宿主机中的设备驱动来完成物理层面上的 I/O 操作。
这样做就可以根据约定实现批量处理而不是客户机中每次 I/O 请求都需要处理一次,从而提高了客户机与 Hypervisor 之间信息交换的效率
3.2 优缺点
- 优点:可获得很好的 I/O 性能,接近 Native。所以在使用 KVM 时,如果宿主机和客户机都支持 Virtio,一般都推荐使用 Virtio 以达到更高的 I/O 性能
- 缺点:必须在客户机中安装前端驱动,且按照 Virtio 的规定格式进行数据传输
4. virtio 的架构
virtio 是半虚拟化的解决方案,是半虚拟化 Hypervisor 的一组通用 I/O 设备的抽象。它提供了一套上层应用与各 Hypervisor 虚拟化设备(KVM、Xen、VMware 等)之间的通信框架和编程接口,减少了跨平台所带来的兼容性问题。客户机需要知道自己运行在虚拟化环境中,进而根据 Virtio 标准和 Hypervisor 协作,从而提高 I/O 性能。
- 前端驱动:Frontend Driver,是位于客户机内核中的驱动程序模块
- 后端驱动:Backend Driver,在宿主机用户空间的 QEMU 中实现
virtio 是半虚拟化驱动框架,可以提供接近 Native 的 I/O 性能,但是客户机中必须安装特定的 virtio 驱动,并按照 virtio 的规定格式进行数据传输
4.1 版本要求
Kernel >= 2.6.25
的内核都支持 virtio。由于 virtio 的后端处理程序是在位于用户空间中的 QEMU 中实现的,所以宿主机只需要比较新的内核即可,不需要特别编译 virtio 相关驱动。而客户机需要有特定 virtio 驱动程序的支持,以便客户机处理 I/O 操作请求时调用前端驱动。
客户机内核中关于 virtio 的部分配置如下:
# 需要启用的选项
CONFIG_VIRTIO_PCI=m
CONFIG_VIRTIO_BALLOON=m
CONFIG_VIRTIO_BLK=m
CONFIG_VIRTIO_NET=m
CONFIG_VIRTIO=m
CONFIG_VIRTIO_RING=y
# 其他相关的选项
CONFIG_VIRTIO_VSOCKETS=m
CONFIG_VIRTIO_VSOCKETS_COMMON=m
CONFIG_SCSI_VIRTIO=m
CONFIG_VIRTIO_CONSOLE=m
CONFIG_HW_RANDOM_VIRTIO=m
CONFIG_DRM_VIRTIO_GPU=m
CONFIG_VIRTIO_PCI_LEGACY=y
CONFIG_VIRTIO_INPUT=m
# CONFIG_VIRTIO_MMIO is not set
# 在子机中查看 VIRTIO 相关内核模块
> lsmod | grep virtio
virtio_balloon 18015 0
virtio_net 28063 0
virtio_blk 18166 2
virtio_pci 22934 0
virtio_ring 22746 4 virtio_blk,virtio_net,virtio_pci,virtio_balloon
virtio 14959 4 virtio_blk,virtio_net,virtio_pci,virtio_balloon
4.2 层次结构
如图所示,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
来适配。
4.3 前端代码层次结构
相关源码文件
Kernel 3.10.0
中关于 virito 的重要源码文件如下:
drivers/block/virtio_blk.c
drivers/char/hw_random/virtio_rng.c
drivers/char/virtio_console.c
drivers/net/virtio_net.c
drivers/scsi/virtio_scsi.c
drivers/virtio/virtio_balloon.c
drivers/virtio/virtio_mmio.c
drivers/virtio/virtio_pci.c
drivers/virtio/virtio_ring.c
drivers/virtio/virtio.c
include/linux/virtio_caif.h
include/linux/virtio_config.h
include/linux/virtio_console.h
include/linux/virtio_mmio.h
include/linux/virtio_ring.h
include/linux/virtio_scsi.h
include/linux/virtio.h
include/linux/vring.h
include/uapi/linux/virtio_9p.h
include/uapi/linux/virtio_balloon.h
include/uapi/linux/virtio_blk.h
include/uapi/linux/virtio_config.h
include/uapi/linux/virtio_console.h
include/uapi/linux/virtio_ids.h
include/uapi/linux/virtio_net.h
include/uapi/linux/virtio_pci.h
include/uapi/linux/virtio_ring.h
include/uapi/linux/virtio_rng.h
tools/virtio/linux/virtio_config.h
tools/virtio/linux/virtio_ring.h
tools/virtio/linux/virtio.h
tools/virtio/linux/vring.h
tools/virtio/vitrio_test.c
tools/virtio/vringh_test.c
linux-3.10
├─ drivers
| ├─ block
| | └─ virtio_blk.c
| |
| ├─ char
| | ├─ hw_random
| | | └─ virtio_rng.c
| | |
| | └─ virtio_console.c
| |
| ├─ net
| | └─ virtio_net.c
| |
| ├─ scsi
| | └─ virtio_scsi.c
| |
| └─ virtio
| ├─ virtio_balloon.c
| ├─ virtio_mmio.c
| ├─ virtio_pci.c
| ├─ virtio_ring.c
| └─ virtio.c
|
├─ include
| ├─ linux
| | ├─ virtio_caif.h
| | ├─ virtio_config.h
| | ├─ virtio_console.h
| | ├─ virtio_mmio.h
| | ├─ virtio_ring.h
| | ├─ virtio_scsi.h
| | ├─ virtio.h
| | └─ vring.h
| |
| └─ uapi
| └─ linux
| ├─ virtio_9p.h
| ├─ virtio_balloon.h
| ├─ virtio_blk.h
| ├─ virtio_config.h
| ├─ virtio_console.h
| ├─ virtio_ids.h
| ├─ virtio_net.h
| ├─ virtio_pci.h
| ├─ virtio_ring.h
| └─ virtio_rng.h
|
└─ tools
└─ virtio
├─ linux
| ├─ virtio_config.h
| ├─ virtio_ring.h
| ├─ virtio.h
| └─ vring.h
|
├─ virtio_test.c
└─ vringh_test.c
类结构层次
在 virtio 前端驱动即客户机内核中,virtio 的类层次结构如下图所示:
virtio_driver
最顶级的是virtio_driver
,在客户机 OS 中表示前端驱动程序,在include/linux/virtio.h
中定义:
/**
* virtio_driver - operations for a virtio I/O driver
* @driver: underlying device driver (populate name and owner).
* @id_table: the ids serviced by this driver.
* @feature_table: an array of feature numbers supported by this driver.
* @feature_table_size: number of entries in the feature table array.
* @probe: the function to call when a device is found. Returns 0 or -errno.
* @remove: the function to call when a device is removed.
* @config_changed: optional function to call when the device configuration
* changes; may be called in interrupt context.
*/
struct virtio_driver {
struct device_driver driver;
const struct virtio_device_id *id_table;
const unsigned int *feature_table;
unsigned int feature_table_size;
int (*probe)(struct virtio_device *dev);
void (*scan)(struct virtio_device *dev);
void (*remove)(struct virtio_device *dev);
void (*config_changed)(struct virtio_device *dev);
#ifdef CONFIG_PM
int (*freeze)(struct virtio_device *dev);
int (*restore)(struct virtio_device *dev);
#endif
};
virtio_device_id
每个 virtio 设备都有其对应的virtio_device_id
,该结构体在include/linux/mod_devicetable.h
中定义:
struct virtio_device_id {
__u32 device;
__u32 vendor;
};
#define VIRTIO_DEV_ANY_ID 0xffffffff
virtio_device
与驱动程序匹配的设备由virtio_device
封装,它表示在客户机 OS 中的设备,在include/linux/virtio.h
中定义:
/**
* virtio_device - representation of a device using virtio
* @index: unique position on the virtio bus
* @dev: underlying device.
* @id: the device type identification (used to match it with a driver).
* @config: the configuration ops for this device.
* @vringh_config: configuration ops for host vrings.
* @vqs: the list of virtqueues for this device.
* @features: the features supported by both driver and device.
* @priv: private pointer for the driver's use.
*/
struct virtio_device {
int index;
struct device dev;
struct virtio_device_id id;
const struct virtio_config_ops *config;
const struct vringh_config_ops *vringh_config;
struct list_head vqs;
/* Note that this is a Linux set_bit-style bitmap. */
unsigned long features[1];
void *priv;
};
virtio_config_ops
每一个virtio_device
都有一个virtio_config_ops
类型的指针*config
,它定义了配置 virtio 设备的操作,该结构体在include/linux/virtio_config.h
中定义:
/**
* virtio_config_ops - operations for configuring a virtio device
* @get: read the value of a configuration field
* @set: write the value of a configuration field
* @get_status: read the status byte
* @set_status: write the status byte
* @reset: reset the device
* @find_vqs: find virtqueues and instantiate them.
* @del_vqs: free virtqueues found by find_vqs().
* @get_features: get the array of feature bits for this device.
* @finalize_features: confirm what device features we'll be using.
* @bus_name: return the bus name associated with the device
* @set_vq_affinity: set the affinity for a virtqueue.
*/
struct virtio_config_ops {
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);
int (*find_vqs)(struct virtio_device *, unsigned nvqs,
struct virtqueue *vqs[],
vq_callback_t *callbacks[],
const char *names[]);
void (*del_vqs)(struct virtio_device *);
u32 (*get_features)(struct virtio_device *vdev);
void (*finalize_features)(struct virtio_device *vdev);
const char *(*bus_name)(struct virtio_device *vdev);
int (*set_vq_affinity)(struct virtqueue *vq, int cpu);
};
virtqueue
每一个virtqueue
包含了对应的virtio_device
以及对应的队列操作回调函数,它在include/linux/virtio.h
中定义:
/**
* virtqueue - a queue to register buffers for sending or receiving.
* @list: the chain of virtqueues for this device
* @callback: the function to call when buffers are consumed (can be NULL).
* @name: the name of this virtqueue (mainly for debugging)
* @vdev: the virtio device this queue was created for.
* @priv: a pointer for the virtqueue implementation to use.
* @index: the zero-based ordinal number for this queue.
* @num_free: number of elements we expect to be able to fit.
*
* A note on @num_free: with indirect buffers, each buffer needs one
* element in the queue, otherwise a buffer will need one element per
* sg element.
*/
struct virtqueue {
struct list_head list;
void (*callback)(struct virtqueue *vq);
const char *name;
struct virtio_device *vdev;
unsigned int index;
unsigned int num_free;
void *priv;
};
5. 相关数据结构
5.1 前端 Kernel
virtio_driver
在include/linux/virtio.h
中定义:
/**
* virtio_driver - operations for a virtio I/O driver
* @driver: underlying device driver (populate name and owner).
* @id_table: the ids serviced by this driver.
* @feature_table: an array of feature numbers supported by this driver.
* @feature_table_size: number of entries in the feature table array.
* @probe: the function to call when a device is found. Returns 0 or -errno.
* @remove: the function to call when a device is removed.
* @config_changed: optional function to call when the device configuration
* changes; may be called in interrupt context.
*/
struct virtio_driver {
struct device_driver driver;
const struct virtio_device_id *id_table;
const unsigned int *feature_table;
unsigned int feature_table_size;
int (*probe)(struct virtio_device *dev);
void (*scan)(struct virtio_device *dev);
void (*remove)(struct virtio_device *dev);
void (*config_changed)(struct virtio_device *dev);
#ifdef CONFIG_PM
int (*freeze)(struct virtio_device *dev);
int (*restore)(struct virtio_device *dev);
#endif
};
这里的virtio_device_id
有两个字段:
struct virtio_device_id {
__u32 device;
__u32 vendor;
};
#define VIRTIO_DEV_ANY_ID 0xffffffff
virtio_device
在include/linux/virtio.h
中定义:
/**
* virtio_device - representation of a device using virtio
* @index: unique position on the virtio bus
* @dev: underlying device.
* @id: the device type identification (used to match it with a driver).
* @config: the configuration ops for this device.
* @vringh_config: configuration ops for host vrings.
* @vqs: the list of virtqueues for this device.
* @features: the features supported by both driver and device.
* @priv: private pointer for the driver's use.
*/
struct virtio_device {
int index;
struct device dev;
struct virtio_device_id id;
const struct virtio_config_ops *config;
const struct vringh_config_ops *vringh_config;
struct list_head vqs;
/* Note that this is a Linux set_bit-style bitmap. */
unsigned long features[1];
void *priv;
};
virtio_config_ops
在include/linux/virtio_config.h
中定义:
/**
* virtio_config_ops - operations for configuring a virtio device
* @get: read the value of a configuration field
* vdev: the virtio_device
* offset: the offset of the configuration field
* buf: the buffer to write the field value into.
* len: the length of the buffer
* @set: write the value of a configuration field
* vdev: the virtio_device
* offset: the offset of the configuration field
* buf: the buffer to read the field value from.
* len: the length of the buffer
* @get_status: read the status byte
* vdev: the virtio_device
* Returns the status byte
* @set_status: write the status byte
* vdev: the virtio_device
* status: the new status byte
* @reset: reset the device
* vdev: the virtio device
* After this, status and feature negotiation must be done again
* Device must not be reset from its vq/config callbacks, or in
* parallel with being added/removed.
* @find_vqs: find virtqueues and instantiate them.
* vdev: the virtio_device
* nvqs: the number of virtqueues to find
* vqs: on success, includes new virtqueues
* callbacks: array of callbacks, for each virtqueue
* include a NULL entry for vqs that do not need a callback
* names: array of virtqueue names (mainly for debugging)
* include a NULL entry for vqs unused by driver
* Returns 0 on success or error status
* @del_vqs: free virtqueues found by find_vqs().
* @get_features: get the array of feature bits for this device.
* vdev: the virtio_device
* Returns the first 32 feature bits (all we currently need).
* @finalize_features: confirm what device features we'll be using.
* vdev: the virtio_device
* This gives the final feature bits for the device: it can change
* the dev->feature bits if it wants.
* @bus_name: return the bus name associated with the device
* vdev: the virtio_device
* This returns a pointer to the bus name a la pci_name from which
* the caller can then copy.
* @set_vq_affinity: set the affinity for a virtqueue.
*/
typedef void vq_callback_t(struct virtqueue *);
struct virtio_config_ops {
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);
int (*find_vqs)(struct virtio_device *, unsigned nvqs,
struct virtqueue *vqs[],
vq_callback_t *callbacks[],
const char *names[]);
void (*del_vqs)(struct virtio_device *);
u32 (*get_features)(struct virtio_device *vdev);
void (*finalize_features)(struct virtio_device *vdev);
const char *(*bus_name)(struct virtio_device *vdev);
int (*set_vq_affinity)(struct virtqueue *vq, int cpu);
};
virtqueue
在include/linux/virtio.h
中定义:
/**
* virtqueue - a queue to register buffers for sending or receiving.
* @list: the chain of virtqueues for this device
* @callback: the function to call when buffers are consumed (can be NULL).
* @name: the name of this virtqueue (mainly for debugging)
* @vdev: the virtio device this queue was created for.
* @priv: a pointer for the virtqueue implementation to use.
* @index: the zero-based ordinal number for this queue.
* @num_free: number of elements we expect to be able to fit.
*
* A note on @num_free: with indirect buffers, each buffer needs one
* element in the queue, otherwise a buffer will need one element per
* sg element.
*/
struct virtqueue {
struct list_head list;
void (*callback)(struct virtqueue *vq);
const char *name;
struct virtio_device *vdev;
unsigned int index;
unsigned int num_free;
void *priv;
};
5.2 后端 QEMU
VirtQueue
在hw/virtio/virtio.c
中定义:
struct VirtQueue
{
VRing vring;
/* Next head to pop */
uint16_t last_avail_idx;
/* Last avail_idx read from VQ. */
uint16_t shadow_avail_idx;
uint16_t used_idx;
/* Last used index value we have sjjignalled on */
uint16_t signalled_used;
/* Last used index value we have signalled on */
bool signalled_used_valid;
/* Notification enabled? */
bool notification;
uint16_t queue_index;
int inuse;
uint16_t vector;
void (*handle_output)(VirtIODevice *vdev, VirtQueue *vq);
void (*handle_aio_output)(VirtIODevice *vdev, VirtQueue *vq);
VirtIODevice *vdev;
EventNotifier guest_notifier;
EventNotifier host_notifier;
QLIST_ENTRY(VirtQueue) node;
};
VRing
在hw/virtio/virtio.c
中定义:
typedef struct VRing
{
unsigned int num;
unsigned int num_default;
unsigned int align;
hwaddr desc;
hwaddr avail;
hwaddr used;
} VRing;
未完待续…
参考文章
相关博客
- Virtio 概述和基本原理(KVM 半虚拟化驱动)| 笑遍世界
- Virtio:针对 Linux 的 I/O 虚拟化框架 | IBM Developer
- virtio 基本原理 (kvm 半虚拟化驱动) | 开源中国
- 说一说虚拟化绕不开的 io 半虚拟化 | 腾讯云加社区
- Virtio Vring 工作机制分析 | OenHan
- KVM Virtio Block 源代码分析 | OenHan
- vring的创建(基于kernel 3.10, qemu2.0.0) - leoufung| CSDN
- QEMU 通过virtio接收报文处理流程(QEMU2.0.0)- leoufung | CSDN
- VIRTIO 的 vring 收发队列创建流程 - leoufung | CSDN
- VIRTIO 中的前后端配合限速分析 - leoufung | CSDN
- virtio 路径 | 随便写写
官方文档
太初有道 - 博客园
- Virtio 前端驱动详解 - 太初有道 | cnblogs
- Virtio 后端驱动详解 - 太初有道 | cnblogs
- Virtio 前后端 notify 机制详解 - 太初有道 | cnblogs
- intel EPT 机制详解 - 太初有道 | cnblogs
- KVM 中 EPT 逆向映射机制分析 - 太初有道 | cnblogs
- QEMU 进程页表和 EPT 的同步问题 - 太初有道 | cnblogs
- KVM vCPU 线程调度问题的讨论 - 太初有道 | cnblogs
- virtio 之 vhost 工作原理简析 - 太初有道 | cnblogs
- PCI 设备详解一 - 太初有道 | cnblogs