怀德维宁

大邦维屏,大宗维翰。怀德维宁,宗子维城。

0%

Virtio-设备模拟详解

什么是virtio

virtio 是一种 I/O 半虚拟化解决方案,是一套通用 I/O 设备虚拟化的程序,是对半虚拟化 Hypervisor 中的一组通用 I/O 设备的抽象。提供了一套上层应用与各 Hypervisor 虚拟化设备(KVM,Xen,VMware等)之间的通信框架和编程接口,减少跨平台所带来的兼容性问题,大大提高驱动程序开发效率。
virtio 协议定义了各类设备与驱动,定义了它们如何初始化,如何通信,如何通知等。其中,最核心的是设备与驱动的通信机制,避免了每次访问外设寄存器都要 vm_exit/vm_enter 的问题。

Virtio架构

从总体上看,virtio 可以分为四层,包括前端 guest 中各种驱动程序模块,后端 Hypervisor (实现在Qemu上)上的处理程序模块,中间用于前后端通信的 virtio 层和 virtio-ring 层,virtio 这一层实现的是虚拟队列接口,算是前后端通信的桥梁,而 virtio-ring 则是该桥梁的具体实现,它实现了两个环形缓冲区,分别用于保存前端驱动程序和后端处理程序执行的信息。

严格来说,virtio 和 virtio-ring 可以看做是一层,virtio-ring 实现了 virtio 的具体通信机制和数据流程。或者这么理解可能更好,virtio 层属于控制层,负责前后端之间的通知机制(kick,notify)和控制流程,而 virtio-vring 则负责具体数据流转发。

vring共享内存基本原理

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

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

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

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

总体看有三种情况:

  1. 通过 qemu 模拟的设备,GPA 位于 qemu 的进程地址空间,qemu 天然可以访问。
  2. qemu 外部模拟的设备,比如 vhost-net/vhost-user,需要建立新的内存映射。
  3. 对于一个真实的硬件设备,需要使用 IOMMU 辅助完成地址转换。

以 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 进程上下文的虚拟地址。

PCI设备概述

PCI即Peripheral Component Interconnect,中文意思是“外围器件互联”,是由PCISIG (PCI Special Interest Group)推出的一种局部并行总线标准。PCI总线是由ISA(Industy Standard Architecture)总线发展而来的,是一种同步的独立于处理器的32位或64位局部总线。从结构上看,PCI是在CPU的供应商和原来的系统总线之间插入的一级总线,具体由一个桥接电路实现对这一层的管理,并实现上下之间的接口以协调数据的传送。

PCI总线是一种共享总线,所以需要特定的仲裁器(Arbiter)来决定当前时刻的总线的控制权。一般该仲裁器位于北桥中,而仲裁器(主机)则通过一对引脚,REQ#(request) 和GNT# (grant)来与各个从机连接。
CPU可以直接通过load/store指令来访问PCI设备,PCI设备有如下三种不同内存:

  • MMIO
  • PCI IO space
  • PCI configuration space

guest前端驱动程序操作接口

驱动程序对PCI 配置的操作可以分成以下几个部分:

读写 feature bits
定义了 Guest 和 Host 支持的功能,例如 VIRTIO_NET_F_CSUM bit 表示网络设备是否支持 checksum offload。feature bits 机制提供了未来扩充功能的灵活性,以及兼容旧设备的能力。

读写配置空间

一般通过一个数据结构和一个虚拟设备关联,Guest 可以读写此空间。

读写 status bits

这是一个8bits的长度,Guest用来标识device probe的状态,当 VIRIO_CONFIG_S_DRIVE_OK被设置,那么Guest已经完成了feature协商,可以跟host进行数据交互了。

Device reset

重置设备,配置status bits。

Virtqueue的创建和销毁

提供了分配virtqueue内存和Host的IO空间的初始化操作。

对应的代码如下:

//代码路径virtio-win/VirtIO/windows/VirtIOPCIModern.c
static const struct virtio_device_ops virtio_pci_device_ops = {
.get_config = vio_modern_get_config,
.set_config = vio_modern_set_config,
.get_config_generation = vio_modern_get_generation,
.get_status = vio_modern_get_status,
.set_status = vio_modern_set_status,
.reset = vio_modern_reset,
.get_features = vio_modern_get_features,
.set_features = vio_modern_set_features,
.set_config_vector = vio_modern_set_config_vector,
.set_queue_vector = vio_modern_set_queue_vector,
.query_queue_alloc = vio_modern_query_vq_alloc,
.setup_queue = vio_modern_setup_vq,
.delete_queue = vio_modern_del_vq,
};
//代码路径virtio-win/virtio/virtio_pci.h
struct virtio_device_ops
{
// read/write device config and read config generation counter
void (*get_config)(VirtIODevice *vdev, unsigned offset, void *buf, unsigned len);
void (*set_config)(VirtIODevice *vdev, unsigned offset, const void *buf, unsigned len);
u32 (*get_config_generation)(VirtIODevice *vdev);
// read/write device status byte and reset the device
u8 (*get_status)(VirtIODevice *vdev);
void (*set_status)(VirtIODevice *vdev, u8 status);
void (*reset)(VirtIODevice *vdev);
// get/set device feature bits
u64 (*get_features)(VirtIODevice *vdev);
NTSTATUS (*set_features)(VirtIODevice *vdev, u64 features);
// set config/queue MSI interrupt vector, returns the new vector
u16 (*set_config_vector)(VirtIODevice *vdev, u16 vector);
u16 (*set_queue_vector)(struct virtqueue *vq, u16 vector);
// query virtual queue size and memory requirements
NTSTATUS (*query_queue_alloc)(VirtIODevice *vdev,
unsigned index, unsigned short *pNumEntries,
unsigned long *pRingSize,
unsigned long *pHeapSize);
// allocate and initialize a queue
NTSTATUS (*setup_queue)(struct virtqueue **queue,
VirtIODevice *vdev, VirtIOQueueInfo *info,
unsigned idx, u16 msix_vec);
// tear down and deallocate a queue
void (*delete_queue)(VirtIOQueueInfo *info);
};

virtio 数据流交互机制:virtqueue

Virtio 使用 virtqueue 来实现 I/O 机制,每个 virtqueue 就是一个承载大量数据的队列,具体使用多少个队列取决于需求,例如,virtio 网络驱动程序(virtio-net)使用两个队列(一个用于接受,另一个用于发送),而 virtio 块驱动程序(virtio-blk)仅使用一个队列。

//VirtIO.h
struct virtqueue {
VirtIODevice *vdev;
struct vring vring;
struct {
u16 flags;
u16 idx;
} master_vring_avail;
unsigned int index;
unsigned int num_unused;
unsigned int num_added_since_kick;
u16 first_unused;
u16 last_used;
void *notification_addr;
void (*notification_cb)(struct virtqueue *vq);
void *opaque[];
};

针对Virtqueue的具体操作包含:

1.向queue中添加一个新的buffer,opaque为一个非空的令牌,用于识别buffer,当buffer内容被消耗后,opaque会返回。

//VirtIORing.c
int virtqueue_add_buf(
struct virtqueue *vq,/* 虚拟化队列 */
struct scatterlist sg[],   /* 缓存区描述符数组,长度为驱动程序->设备缓冲区描述符(in)+设备->驱动程序缓冲区描述符(out)*/
unsigned int out,/* sg中驱动程序到设备缓冲区描述符数量 */
unsigned int in, /* sg中设备到驱动程序缓冲区描述符数量 */
void *opaque,/* virtqueue_get_buf 函数返回值(used ring缓冲区起始描述符指针)*/
void *va_indirect,   /* 间接页的虚拟地址或空指针*/
ULONGLONG phys_indirect) /*间接页的物理地址或空指针*/

2.Guest 通知 host 单个或者多个 buffer 已经添加到 queue 中,调用 virtqueue_notify(),notify 函数会向 queue notify(VIRTIO_PCI_QUEUE_NOTIFY)寄存器写入 queue index 来通知 host。

 //VirtIOPCICommon.c
void virtqueue_kick(struct virtqueue *vq)
{
if (virtqueue_kick_prepare(vq)) {
virtqueue_notify(vq);
}
}

3.返回使用过的 buffer,len 为写入到 buffer 中数据的长度。获取数据,释放 buffer,更新 vring 描述符表格中的 index。

//VirtIORing.c
void *virtqueue_get_buf(
struct virtqueue *vq, /* the queue */
unsigned int *len)/* number of bytes returned by the device */

4.示意 guest 不再需要再知道一个 buffer 已经使用了,也就是关闭 device 的中断。驱动会在初始化时注册一个回调函数,disable_cb()通常在这个 virtqueue 回调函数中使用,用于关闭再次的回调发生。

//VirtIORing.c
void virtqueue_disable_cb(struct virtqueue *vq)

5.与 disable_cb()刚好相反,用于重新开启设备中断的上报。

//VirtIORing.c
bool virtqueue_enable_cb(struct virtqueue *vq) 

virtio 的核心机制就是通过共享内存在前端驱动与后端实现间进行数据传输,共享内存区域被称作 vring。

virtio 传输机制:vring的构成与实现

vring 是 virtio 传输机制的实现,vring 引入 ring buffers 来作为数据传输的载体,包含三个部分:

// virtio_ring.h
struct vring {
unsigned int num;
struct vring_desc *desc;
struct vring_avail *avail;
struct vring_used *used;
};

Descriptor Table: 描述内存 buffer,主要包括 addr/len 等信息。

// virtio_ring.h
/* This marks a buffer as continuing via the next field. */
#define VIRTQ_DESC_F_NEXT	1
/* This marks a buffer as write-only (otherwise read-only). */
#define VIRTQ_DESC_F_WRITE	2
/* This means the buffer contains a list of buffer descriptors. */
#define VIRTQ_DESC_F_INDIRECT	4
/* Virtio ring: 16 bytes.  These can chain together via "next". */
struct vring_desc {
/* Address (guest-physical). */
__virtio64 addr;
/* Length. */
__virtio32 len;
/* The flags as indicated above. */
__virtio16 flags;
/* We chain unused descriptors via this, too */
__virtio16 next;
};

Available Ring: 用于驱动通知设备有新的可用的描述符。比如,通知后端设备,有一个待发送的报文描述符。

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

// virtio_ring.h
#define VIRTQ_AVAIL_F_NO_INTERRUPT	1
struct vring_avail {
    //控制信息,比如 VIRTQ_AVAIL_F_NO_INTERRUPT 表示驱动侧不想接收通知
__virtio16 flags;
//idx:驱动将把下一个描述符放在哪里,即 ring 数组的下标
__virtio16 idx;
    //ring[]:avail 描述符在 Descriptor Table 中的 id
__virtio16 ring[];
};

Used Ring: 用于通知驱动设备侧已用的描述符。比如,后端设备收到一个报文,需要将报文数据放入可用的描述符,并更新Used Ring,同时通知前端驱动。

// virtio_ring.h
#define VIRTQ_USED_F_NO_NOTIFY	1
struct vring_used_elem {
/* Index of start of used descriptor chain. */
__virtio32 id;
/* Total length of the descriptor chain which was used (written to) */
__virtio32 len;
};

struct vring_used {
__virtio16 flags;
__virtio16 idx;
struct vring_used_elem ring[];
};

注意:相比 avail ring 结构多了 len 字段,用于表示设备侧写入的数据长度。对于只读数据类型,不改变 len 长度。

vring 主要通过两个环形缓冲区来完成数据流的转发。

当 guest 向 virtqueue 中写数据时,实际上是向 desc 结构指向的 buffer 中填充数据,完了会更新 available ring,然后再通知 host。
当 host 收到接收数据的通知时,首先从 desc 指向的 buffer 中找到 available ring 中添加的 buffer,映射内存,同时更新 used ring,并通知 guest 接收数据完毕。

qemu后端处理模块

下面以Virtio Network Device设备的初始化为例对qemu中virtio的实现进行说明。

预定义结构体

//代码路径:QEMU/qom/object.c
static TypeInfo object_info = {
.name = TYPE_OBJECT,
.instance_size = sizeof(Object),
.instance_init = object_instance_init,
.abstract = true,
};
//代码路径:QEMU/hw/core/qdev.c
static const TypeInfo device_type_info = {
.name = TYPE_DEVICE,
.parent = TYPE_OBJECT,
.instance_size = sizeof(DeviceState),
.instance_init = device_initfn,
.instance_post_init = device_post_init,
.instance_finalize = device_finalize,
.class_base_init = device_class_base_init,
.class_init = device_class_init,
.abstract = true,
.class_size = sizeof(DeviceClass),
};
//代码路径:QEMU/hw/virtio/virtio.c
static const TypeInfo virtio_device_info = {
.name = TYPE_VIRTIO_DEVICE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(VirtIODevice),
.class_init = virtio_device_class_init,
.instance_finalize = virtio_device_instance_finalize,
.abstract = true,
.class_size = sizeof(VirtioDeviceClass),
};
//代码路径:QEMU/hw/net/virtio-net.c
static const TypeInfo virtio_net_info = {
.name = TYPE_VIRTIO_NET,
.parent = TYPE_VIRTIO_DEVICE,
.instance_size = sizeof(VirtIONet),
.instance_init = virtio_net_instance_init,
.class_init = virtio_net_class_init,
};
static void virtio_register_types(void)
{
type_register_static(&virtio_net_info);
}
type_init(virtio_register_types)

Virtio Network Device这种类的定义是有多层继承关系的,TYPE_VIRTIO_NET的父类是TYPE_VIRTIO_DEVICE,TYPE_VIRTIO_DEVICE的父类是TYPE_DEVICE,TYPE_DEVICE的父类是TYPE_OBJECT,继承关系就到头了。type_init用于注册这种类,这里面每一层都有class_init,用于从TypeImpl生成xxxClass,也有instance_init,会将xxxClass初始化为实例。

创建VirtQueue

TYPE_VIRTIO_NET层的class_init函数是virtio_net_class_init,它定义了DeviceClass的realize函数为virtio_net_device_realize,如下所示:

//代码路径:QEMU/hw/net/virtio-net.c
static void virtio_net_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);
dc->props = virtio_net_properties;
set_bit(DEVICE_CATEGORY_NETWORK, dc->categories);
vdc->realize = virtio_net_device_realize;
vdc->unrealize = virtio_net_device_unrealize;
vdc->get_config = virtio_net_get_config;
vdc->set_config = virtio_net_set_config;
vdc->get_features = virtio_net_get_features;
vdc->set_features = virtio_net_set_features;
vdc->bad_features = virtio_net_bad_features;
vdc->reset = virtio_net_reset;
vdc->set_status = virtio_net_set_status;
vdc->preset_dma_map = virtio_net_preset_dma_map;
vdc->set_host_notifier = virtio_net_set_host_notifier;
vdc->unset_host_notifier = virtio_net_unset_host_notifier;
vdc->guest_notifier_mask = virtio_net_guest_notifier_mask;
vdc->guest_notifier_pending = virtio_net_guest_notifier_pending;
vdc->load = virtio_net_load_device;
vdc->save = virtio_net_save_device;
}
static void virtio_net_device_realize(DeviceState *dev, Error **errp)
{
VirtIODevice *vdev = VIRTIO_DEVICE(dev);
VirtIONet *n = VIRTIO_NET(dev);
NetClientState *nc;
int i;
         … …
n->max_queues = MAX(n->nic_conf.peers.queues, 1);
… …
for (i = 0; i < n->max_queues; i++) {
virtio_net_add_queue(n, i);
}
n->ctrl_vq = virtio_add_queue(vdev, 64, virtio_net_handle_ctrl);
qemu_macaddr_default_if_unset(&n->nic_conf.macaddr);
memcpy(&n->mac[0], &n->nic_conf.macaddr, sizeof(n->mac));
n->status = VIRTIO_NET_S_LINK_UP;
n->announce_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
 virtio_net_announce_timer, n);
if (n->netclient_type) {
n->nic = qemu_new_nic(&net_virtio_info, &n->nic_conf,
  n->netclient_type, n->netclient_name, n);
} else {
n->nic = qemu_new_nic(&net_virtio_info, &n->nic_conf,
  object_get_typename(OBJECT(dev)), dev->id, n);
}
… …
}

上述代码创建了一个VirtIODevice,而virtio_init用来初始化这个设备。VirtIODevice结构里面有一个VirtQueue数组,这就是virtio前端和后端互相传数据的队列,最多有VIRTIO_QUEUE_MAX(1024)个。

但是net设备也有与其他设备不一样的地方,即代码中存在这样的语句n->max_queues * 2 + 1 > VIRTIO_QUEUE_MAX。为什么要乘以2呢?这是因为对于网络设备来讲,应该分发送队列和接收队列两个方向。

VirtQueue队列初始化

接下来调用virtio_net_add_queue来初始化队列,可以看出这里面就有发送tx_vq和接收rx_vq两个队列,如下所示:

//代码路径:QEMU/hw/net/virtio-net.c
static void virtio_net_add_queue(VirtIONet *n, int index)
{
VirtIODevice *vdev = VIRTIO_DEVICE(n);
n->vqs[index].rx_vq = virtio_add_queue(vdev, n->net_conf.rx_queue_size,
   virtio_net_handle_rx);
if (n->net_conf.tx && !strcmp(n->net_conf.tx, "timer")) {
n->vqs[index].tx_vq =
virtio_add_queue(vdev, n->net_conf.tx_queue_size,
 virtio_net_handle_tx_timer);
n->vqs[index].tx_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,
  virtio_net_tx_timer,
  &n->vqs[index]);
} else {
n->vqs[index].tx_vq =
virtio_add_queue(vdev, n->net_conf.tx_queue_size,
 virtio_net_handle_tx_bh);
n->vqs[index].tx_bh = qemu_bh_new(virtio_net_tx_bh, &n->vqs[index]);
}
n->vqs[index].tx_waiting = 0;
n->vqs[index].n = n;
}

每个VirtQueue中,都有一个vring用来维护这个队列里面的数据;另外还有函数virtio_net_handle_rx用于处理网络包的接收;函数virtio_net_handle_tx_bh用于网络包的发送。

创建虚拟机网卡

qemu_new_nic会创建一个虚拟机里面的网卡,如下所示:

//代码路径:QEMU/net/net.c
NICState *qemu_new_nic(NetClientInfo *info,
   NICConf *conf,
   const char *model,
   const char *name,
   void *opaque)
{
NetClientState **peers = conf->peers.ncs;
NICState *nic;
int i, queues = MAX(1, conf->peers.queues);
assert(info->type == NET_CLIENT_OPTIONS_KIND_NIC);
assert(info->size >= sizeof(NICState));
nic = g_malloc0(info->size + sizeof(NetClientState) * queues);
nic->ncs = (void *)nic + info->size;
nic->conf = conf;
nic->opaque = opaque;
for (i = 0; i < queues; i++) {
qemu_net_client_setup(&nic->ncs[i], info, peers[i], model, name,
  NULL);
nic->ncs[i].queue_index = i;
}
return nic;
}
static void qemu_net_client_setup(NetClientState *nc,
  NetClientInfo *info,
  NetClientState *peer,
  const char *model,
  const char *name,
  NetClientDestructor *destructor)
{
nc->info = info;
nc->model = g_strdup(model);
if (name) {
nc->name = g_strdup(name);
} else {
nc->name = assign_name(nc, model);
}
if (peer) {
assert(!peer->peer);
nc->peer = peer;
peer->peer = nc;
}
QTAILQ_INSERT_TAIL(&net_clients, nc, next);
nc->incoming_queue = qemu_new_net_queue(qemu_deliver_packet_iov, nc);
nc->destructor = destructor;
QTAILQ_INIT(&nc->filters);
}

kernel内核驱动程序

在虚拟机里面的进程发送一个网络包,通过文件系统和Socket调用网络协议栈到达网络设备层,只不过这个不是普通的网络设备,而是virtio_net的驱动。virtio_net的驱动程序代码在Linux操作系统的源代码里面,文件名为linux/drivers/net/virtio_net.c,如下所示:

预定义结构体

static struct virtio_driver virtio_net_driver = {
    .feature_table = features,
    .feature_table_size = ARRAY_SIZE(features),
    .driver.name =	KBUILD_MODNAME,
    .driver.owner =	THIS_MODULE,
    .id_table =	id_table,
    .probe =	virtnet_probe,
    .remove =	virtnet_remove,
    .config_changed = virtnet_config_changed,
#ifdef CONFIG_PM_SLEEP
    .freeze =	virtnet_freeze,
    .restore =	virtnet_restore,
#endif
};
module_virtio_driver(virtio_net_driver);
MODULE_DEVICE_TABLE(virtio, id_table);
MODULE_DESCRIPTION("Virtio network driver");
MODULE_LICENSE("GPL");

模块初始化

在virtio_net的驱动程序的初始化代码中,需要注册一个驱动函数virtio_net_driver。当一个设备驱动作为一个内核模块被初始化的时候,probe函数会被调用,因而来看一下virtnet_probe:

static int virtnet_probe(struct virtio_device *vdev)
{
    int i, err;
    struct net_device *dev;
    struct virtnet_info *vi;
    u16 max_queue_pairs;
    int mtu;
    … …
    dev = alloc_etherdev_mq(sizeof(struct virtnet_info), max_queue_pairs);
    if (!dev)
        return -ENOMEM;
    /* Set up network device as normal. */
    dev->priv_flags |= IFF_UNICAST_FLT | IFF_LIVE_ADDR_CHANGE;
    dev->netdev_ops = &virtnet_netdev;
    dev->features = NETIF_F_HIGHDMA;
    SET_ETHTOOL_OPS(dev, &virtnet_ethtool_ops);
    SET_NETDEV_DEV(dev, &vdev->dev);
    … …
    /* Set up our device-specific information */
    vi = netdev_priv(dev);
    vi->dev = dev;
    vi->vdev = vdev;
    vdev->priv = vi;
    vi->stats = alloc_percpu(struct virtnet_stats);
    err = -ENOMEM;
    … …
    err = init_vqs(vi);
    if (err)
        goto free_index;
    netif_set_real_num_tx_queues(dev, vi->curr_queue_pairs);
    netif_set_real_num_rx_queues(dev, vi->curr_queue_pairs);
    virtnet_init_settings(dev);
    err = register_netdev(dev);
    if (err) {
        pr_debug("virtio_net: registering device failed\n");
        goto free_vqs;
    }
    virtio_device_ready(vdev);
    … …
    virtnet_set_queues(vi, vi->curr_queue_pairs);
    … …
}

在virtnet_probe中会创建struct net_device,并且通过register_netdev注册这个网络设备,这样在客户机里面就能看到这个网卡了。

初始化virtqueue

在virtnet_probe中,还有一件重要的事情就是,init_vqs会初始化发送和接收的virtqueue,如下所示:

static int init_vqs(struct virtnet_info *vi)
{
    int ret;
    /* Allocate send & receive queues */
    ret = virtnet_alloc_queues(vi);
    if (ret)
        goto err;
    ret = virtnet_find_vqs(vi);
    if (ret)
        goto err_free;
    get_online_cpus();
    virtnet_set_affinity(vi);
    put_online_cpus();
    return 0;
err_free:
    virtnet_free_queues(vi);
err:
    return ret;
}

Virtqueue实体查找

按照之前的virtio原理,virtqueue是一个介于客户机前端和qemu后端的一个结构,用于在这两端之间传递数据,对于网络设备来讲有发送和接收两个方向的队列。这里建立的struct virtqueue是客户机前端对于队列管理的数据结构。队列的实体需要通过函数virtnet_find_vqs查找或者生成,这里还会指定接收队列的callback函数为skb_recv_done,发送队列的callback函数为skb_xmit_done。当buffer使用发生变化的时候,可以调用这个callback函数进行通知,如下所示:

static int virtnet_find_vqs(struct virtnet_info *vi)
{
    vq_callback_t **callbacks;
    struct virtqueue **vqs;
    int ret = -ENOMEM;
    int i, total_vqs;
    const char **names;
    /* We expect 1 RX virtqueue followed by 1 TX virtqueue, followed by
     * possible N-1 RX/TX queue pairs used in multiqueue mode, followed by
     * possible control vq.
     */
    total_vqs = vi->max_queue_pairs * 2 +
        virtio_has_feature(vi->vdev, VIRTIO_NET_F_CTRL_VQ);
    /* Allocate space for find_vqs parameters */
    vqs = kzalloc(total_vqs * sizeof(*vqs), GFP_KERNEL);
    if (!vqs)
        goto err_vq;
    callbacks = kmalloc(total_vqs * sizeof(*callbacks), GFP_KERNEL);
    if (!callbacks)
        goto err_callback;
    names = kmalloc(total_vqs * sizeof(*names), GFP_KERNEL);
    if (!names)
        goto err_names;
    /* Parameters for control virtqueue, if any */
    if (vi->has_cvq) {
        callbacks[total_vqs - 1] = NULL;
        names[total_vqs - 1] = "control";
    }
    /* Allocate/initialize parameters for send/receive virtqueues */
    for (i = 0; i < vi->max_queue_pairs; i++) {
        callbacks[rxq2vq(i)] = skb_recv_done;
        callbacks[txq2vq(i)] = skb_xmit_done;
        sprintf(vi->rq[i].name, "input.%d", i);
        sprintf(vi->sq[i].name, "output.%d", i);
        names[rxq2vq(i)] = vi->rq[i].name;
        names[txq2vq(i)] = vi->sq[i].name;
    }
    ret = vi->vdev->config->find_vqs(vi->vdev, total_vqs, vqs, callbacks,
                     names);
    if (ret)
        goto err_find;
    if (vi->has_cvq) {
        vi->cvq = vqs[total_vqs - 1];
        if (virtio_has_feature(vi->vdev, VIRTIO_NET_F_CTRL_VLAN))
            vi->dev->features |= NETIF_F_HW_VLAN_CTAG_FILTER;
    }
    for (i = 0; i < vi->max_queue_pairs; i++) {
        vi->rq[i].vq = vqs[rxq2vq(i)];
        vi->sq[i].vq = vqs[txq2vq(i)];
    }
    kfree(names);
    kfree(callbacks);
    kfree(vqs);
    return 0;
err_find:
    kfree(names);
err_names:
    kfree(callbacks);
err_callback:
    kfree(vqs);
err_vq:
    return ret;
}

这里的find_vqs是在struct virtnet_info里的struct virtio_device里的struct virtio_config_ops *config里面定义的。

根据virtio_config_ops的定义,find_vqs会调用vp_modern_find_vqs。在vp_modern_find_vqs 中,vp_find_vqs会调用vp_find_vqs_intx。在vp_find_vqs_intx 中,通过request_irq注册一个中断处理函数vp_interrupt,当设备向队列中写入信息时会产生一个中断,也就是vq中断。中断处理函数需要调用相应队列的回调函数,然后根据队列的数目,依次调用vp_setup_vq完成virtqueue、vring的分配和初始化。

同样,这些数据结构会和virtio后端的VirtIODevice、VirtQueue、vring对应起来,都应该指向刚才创建的那一段内存。客户机同样会通过调用专门给外部设备发送指令的函数iowrite告诉外部的pci设备,这些共享内存的地址。至此前端设备驱动和后端设备驱动之间的两个收发队列就关联好了。

总结

virtio 是 guest 与 host 之间通信的润滑剂,提供了一套通用框架和标准接口或协议来完成两者之间的交互过程,极大地解决了各种驱动程序和不同虚拟化解决方案之间的适配问题。