virtio是对虚拟化环境下guest kernel中io操作的一种优化。
首先需要说明的是,在内核的角度来看,virtio设备及其driver,和其他设备及驱动一样,都是普通的设备,并没有什么特殊性。也就是说,内核并不知道这种io优化的存在。
virtio设备,在系统层面看,就是pci设备。但是,为了提高io效率,对io操作做出了优化。
主要方案是:
1) 当virtio设备输出数据时,driver将数据送到buffer队列中(从virtio网卡驱动的代码来看,此操作无内存拷贝,直接将数据所占的内存作为buffer添加到队列中就完成了),然后通过io指令写设备寄存器(vp_dev->ioaddr + VIRTIO_PCI_QUEUE_NOTIFY),以通知虚拟机系统(kvm+qemu)。虚拟机系统捕获了io指令,就得到了通知,从buffer队列中获取设备输出的数据。
2) 当需要向virtio设备输入数据时,虚拟机系统将数据送到buffer队列中,然后触发设备中断。driver收到中断后,直接从队列中取出数据即可(从virtio网卡驱动的代码来看,队列中的数据已经不需要再进行内存拷贝,队列中的数据已经是sk_buff结构了)。
从上面的机制来看,virtio并不是完全没有了io操作。例如,设备输出数据时,在将数据送入buffer队列后,还是执行了io操作,以通知虚拟机系统。但是,这个io操作,并不是将输出数据写入设备,而是将数据已入队这件事,写入设备。
以上是virtio的大体原理。下面来看看virtio的设计思路。
大体分如下4个层次。
一、buffer队列
既然virtio通过buffer队列实现设备输入输出。那么,如果每一种设备都来实现一下buffer队列,不是浪费么?没错,virtio考虑到这个共性需求,因此就实现一个共同的buffer队列模块——virtio_ring(一个环型队列)。但是,如果哪天buffer队列的实现,需要重新设计怎么办?考虑到这一点,再对buffer队列的操作包装出一个抽象层——struct virtqueue_ops。每一种buffer队列的实现,只要提供一个virtqueue_ops结构变量给用户使用即可。这就实现了队列操作与队列实现的解耦。
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);
};
二、pci层
每一个virtio设备(例如:块设备或网卡),在系统层面看来,都是一个pci设备。这些设备之间,有共性部分,也有差异部分。
1)共性部分:这些设备都需要挂接相应的buffer队列操作virtqueue_ops,都需要申请若干个buffer队列,当执行io输出时,需要向队列写入数据;都需要执行pci_iomap将设备配置寄存器区间映射到内存区间;都需要设置中断处理;等中断来了,都需要从队列读出数据,并通知虚拟机系统,数据已入队。
2) 差异部分:设备中系统中,如何与业务关联起来。各个设备不相同。例如,网卡在内核中是一个net_device,与协议栈系统关联起来。同时,向队列中写入什么数据,数据的含义如何,各个设备不相同。队列中来了数据,是什么含义,如何处理,各个设备不相同。
如果每个virtio设备都完整实现自己的功能,又会形成浪费。
针对这个现象,virtio又设计了virtio_pci模块,以处理所有virtio设备的共性部分。这样一来,所有的virtio设备,在系统层面看来,都是一个pci设备,其设备驱动都是virtio_pci。
但是,virtio_pci并不能完整的驱动任何一个设备。因此,virtio_pci在probe(接管)每一个设备时,根据每个pci设备的subsystem vendor/device id来识别出这具体是哪一种virtio设备,然后相应的向内核注册一个virtio设备。当然,在注册virtio设备之前,virtio_pci驱动已经为此设备做了诸多共性的操作。同时,还为设备提供了各种操作的适配接口,例如,一些常用的pci设备操作,还有申请buffer队列的操作。这些操作,都通过virtio_config_ops结构变量来适配。
三、virtio驱动
这里讲virtio驱动,指的是具体的各个设备的驱动了。例如,网卡或块设备。有了前面所述的各模块的工作,virtio各个设备的驱动实现,就相对简单了。大体来说,除了完成本设备特有的功能以外,剩下的基本就是buffer队列相关操作了。
那就是申请几个队列,并提供相应的回调函数。有数据要输出,往队列中送就行了。队列来数据了,自然会有中断产生,中断处理中,自然会触发回调来处理。
四、virtio_bus
内核中的各种对象,总是有秩序的。为了管理每种具体的virtio驱动及每个具体的virtio设备,干脆搞了一个virtio_bus出来。当然,这个bus并不存在实际的硬件电路,纯粹起个管理与适配作用。就这个管理与适配功能而言,他和pci总线是相似的。全部的virtio driver与virtio device,在virtio_bus中都能够找到。每当有新的virtio driver或者virtio device注册到系统中时,系统都会执行一次设备与驱动的匹配操作。
首先需要说明的是,在内核的角度来看,virtio设备及其driver,和其他设备及驱动一样,都是普通的设备,并没有什么特殊性。也就是说,内核并不知道这种io优化的存在。
virtio设备,在系统层面看,就是pci设备。但是,为了提高io效率,对io操作做出了优化。
主要方案是:
1) 当virtio设备输出数据时,driver将数据送到buffer队列中(从virtio网卡驱动的代码来看,此操作无内存拷贝,直接将数据所占的内存作为buffer添加到队列中就完成了),然后通过io指令写设备寄存器(vp_dev->ioaddr + VIRTIO_PCI_QUEUE_NOTIFY),以通知虚拟机系统(kvm+qemu)。虚拟机系统捕获了io指令,就得到了通知,从buffer队列中获取设备输出的数据。
2) 当需要向virtio设备输入数据时,虚拟机系统将数据送到buffer队列中,然后触发设备中断。driver收到中断后,直接从队列中取出数据即可(从virtio网卡驱动的代码来看,队列中的数据已经不需要再进行内存拷贝,队列中的数据已经是sk_buff结构了)。
从上面的机制来看,virtio并不是完全没有了io操作。例如,设备输出数据时,在将数据送入buffer队列后,还是执行了io操作,以通知虚拟机系统。但是,这个io操作,并不是将输出数据写入设备,而是将数据已入队这件事,写入设备。
以上是virtio的大体原理。下面来看看virtio的设计思路。
大体分如下4个层次。
一、buffer队列
既然virtio通过buffer队列实现设备输入输出。那么,如果每一种设备都来实现一下buffer队列,不是浪费么?没错,virtio考虑到这个共性需求,因此就实现一个共同的buffer队列模块——virtio_ring(一个环型队列)。但是,如果哪天buffer队列的实现,需要重新设计怎么办?考虑到这一点,再对buffer队列的操作包装出一个抽象层——struct virtqueue_ops。每一种buffer队列的实现,只要提供一个virtqueue_ops结构变量给用户使用即可。这就实现了队列操作与队列实现的解耦。
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);
};
二、pci层
每一个virtio设备(例如:块设备或网卡),在系统层面看来,都是一个pci设备。这些设备之间,有共性部分,也有差异部分。
1)共性部分:这些设备都需要挂接相应的buffer队列操作virtqueue_ops,都需要申请若干个buffer队列,当执行io输出时,需要向队列写入数据;都需要执行pci_iomap将设备配置寄存器区间映射到内存区间;都需要设置中断处理;等中断来了,都需要从队列读出数据,并通知虚拟机系统,数据已入队。
2) 差异部分:设备中系统中,如何与业务关联起来。各个设备不相同。例如,网卡在内核中是一个net_device,与协议栈系统关联起来。同时,向队列中写入什么数据,数据的含义如何,各个设备不相同。队列中来了数据,是什么含义,如何处理,各个设备不相同。
如果每个virtio设备都完整实现自己的功能,又会形成浪费。
针对这个现象,virtio又设计了virtio_pci模块,以处理所有virtio设备的共性部分。这样一来,所有的virtio设备,在系统层面看来,都是一个pci设备,其设备驱动都是virtio_pci。
但是,virtio_pci并不能完整的驱动任何一个设备。因此,virtio_pci在probe(接管)每一个设备时,根据每个pci设备的subsystem vendor/device id来识别出这具体是哪一种virtio设备,然后相应的向内核注册一个virtio设备。当然,在注册virtio设备之前,virtio_pci驱动已经为此设备做了诸多共性的操作。同时,还为设备提供了各种操作的适配接口,例如,一些常用的pci设备操作,还有申请buffer队列的操作。这些操作,都通过virtio_config_ops结构变量来适配。
三、virtio驱动
这里讲virtio驱动,指的是具体的各个设备的驱动了。例如,网卡或块设备。有了前面所述的各模块的工作,virtio各个设备的驱动实现,就相对简单了。大体来说,除了完成本设备特有的功能以外,剩下的基本就是buffer队列相关操作了。
那就是申请几个队列,并提供相应的回调函数。有数据要输出,往队列中送就行了。队列来数据了,自然会有中断产生,中断处理中,自然会触发回调来处理。
四、virtio_bus
内核中的各种对象,总是有秩序的。为了管理每种具体的virtio驱动及每个具体的virtio设备,干脆搞了一个virtio_bus出来。当然,这个bus并不存在实际的硬件电路,纯粹起个管理与适配作用。就这个管理与适配功能而言,他和pci总线是相似的。全部的virtio driver与virtio device,在virtio_bus中都能够找到。每当有新的virtio driver或者virtio device注册到系统中时,系统都会执行一次设备与驱动的匹配操作。