6.1.1 全虚拟化与半虚拟化
让我们先讨论一下两种类型完全不同的虚拟化模式:完全虚拟化和半虚拟化。在完全虚拟化中,Guest OS运行在位于物理机器上的 hypervisor 之上。Guest OS并不知道它已被虚拟化,并且不需要任何更改就可以在该配置下工作。相反,在半虚拟化 中,Guest OS不仅知道它运行在 hypervisor 之上,还包含让Guest OS更高效地过渡到 hypervisor 的代码。在完全虚拟化模式中,hypervisor 必须模拟设备硬件,它是在会话的最低级别进行模拟的(例如,网络驱动程序)。尽管在该抽象中模拟很干净,但它同时也是最低效、最复杂的。在半虚拟化模式中,Guest OS和 hypervisor 能够共同合作,让模拟更加高效。半虚拟化方法的缺点是操作系统知道它被虚拟化,并且需要修改才能工作。
上图左侧为全虚拟化;右侧为半虚拟化。他们的关键区别在于全虚拟化在guest os上不需要任何任何改动,而半虚拟化引入了para-drivers. 但全虚拟化由于vm-exit较多因而性能较差。从 2006 年开始,KVM 上设备 I/O 虚拟化的性能问题也显现了出来,此时由 Rusty Russell 开发的 virtio 引起了开发者们的注意并逐渐被 KVM 等虚拟化平台接纳并作为了其 I/O 虚拟化最主要的一个通用框架。virtio 是kvm对半虚拟化 hypervisor 中的一组通用模拟设备的抽象。其结构如下:
kvm hypervisor提供了一组通用模拟设备的抽象和一套API. GuestOs通过实现前段driver,并调用hypervisor提供的api,调用hypervisor back-end driver来完成相应的功能。
6.1.2Virtio架构与原理
virtio 还定义了GuestOS到 hypervisor的通信。在顶级(称为 virtio)的是虚拟队列接口,它在概念上将前端驱动程序附加到后端驱动程序。驱动程序可以使用 0 个或多个队列,具体数量取决于需求。例如,virtio 网络驱动程序使用两个虚拟队列(一个用于接收,另一个用于发送),而 virtio 块驱动程序仅使用一个虚拟队列.
本文将涉及到virtio-blk,virtio-balloon二种类别。
下面来看看virtio的几个基本概念:
(1) vendor_id, device_id用来标示设备,例如virt_blk vendor_id为VIRTIO_ID_BLOCK = 2;
(2) 配置空间: 在 device 特定的配置区域后会有一块区域存放 virtio header。最开始的 32bits 为设备的 feature bits,紧跟着的 32bits 为 Guest(driver) feature bits,然后依次为 QueueAddress(32 bits),Queue Size(16bits),Queue Select(16bits),QueueNotify(16bits),Device Status(8 bits),ISR status(8 bits)。
a) featurebits(32bits)来指定设备支持的功能和特性。
0~23:根据设备类型的不同而不同
24~31:保留位,用于 queue 和feature 协商机制的扩展
b) 设备状态(Device Status) :Device Status 域主要由 guest 来更新,表示当前 drive 的状态。状态包括:
0:写入 0 表示重启该设备
1:Acknowledge,表明 guest 已经发现了一个有效的 virtio 设备
2:Driver,表明 guest 已经可以驱动该设备,guest 已经成功注册了设备驱动
3:Driver_OK,表示 guest 已经正确安装了驱动,准备驱动设备
4:FAILED,在安装驱动过程中出错
每次试图重新初始化设备前,需要设置 Device Status 为 0。
(3) 设备专属配置:此配置空间包含了虚拟设备特殊的一些配置信息,可由 guest 读写virtio_config_ops 定义了config的函数指针
(4) virtqueue: 每个设备拥有多个 virtqueue 用于大块数据的传输。virtqueue 是一个简单的队列,guest 把 buffers 插入其中,每个 buffer 都是一个分散-聚集数组。驱动调用 find_vqs()来创建一个与 queue 关联的结构体。virtqueue 的数目根据设备的不同而不同,比如 block 设备有一个 virtqueue,network 设备有 2 个 virtqueue,一个用于发送数据包,一个用于接收数据包。Balloon 设备有 3 个virtqueue.
(5)virtio_ring 是 virtio 传输机制的实现,vring 引入 ring buffers 来作为我们数据传输的载体。virtio_ring 包含 3 部分:
描述符数组(descriptortable)用于存储一些关联的描述符,每个描述符都是一个对 buffer 的描述,包含一个 address/length 的配对。
可用的ring(available ring)用于 guest 端表示那些描述符链当前是可用的。
使用过的ring(used ring)用于表示 Host 端表示那些描述符已经使用。
Ring 的数目必须是 2 的次幂。
6.1.3 Guest Linux OSvirtio架构
本节将以virtio_blk为例,从上层到下层分析Linux Guest OS virtio架构。
6.1.3.1 virtio_blk
(1) 驱动初始化
major = register_blkdev(0,"virtblk");
error =register_virtio_driver(&virtio_blk);
static struct virtio_drivervirtio_blk = {
.feature_table =features,
.feature_table_size =ARRAY_SIZE(features),
.driver.name =KBUILD_MODNAME,
.driver.owner =THIS_MODULE,
.id_table = id_table,
.probe =virtblk_probe,
.remove =virtblk_remove,
.config_changed =virtblk_config_changed,
#ifdef CONFIG_PM_SLEEP
.freeze =virtblk_freeze,
.restore =virtblk_restore,
#endif
};
(2) probe
virtblk_probe(structvirtio_device *vdev)
a. 通过vdev->config.read获得设备配置信息
virtio_cread_feature ==》 virtio_cread ==》 vdev->config->get(vdev,offset, &ret, sizeof(ret));
b.为配置管理建立work queue:
INIT_WORK(&vblk->config_work, virtblk_config_changed_work);
c. 取得virtqueue :
init_vq ==>virtio_find_single_vq(vblk->vdev,virtblk_done, "requests"); ==> vdev->config->find_vqs
d. gendisk 的初始化,最重要的过程如下:
vblk->tag_set.ops =&virtio_mq_ops;
vblk->disk->queue =blk_mq_init_queue(&vblk->tag_set);
vblk->disk->fops =&virtblk_fops;
add_disk(vblk->disk);
queue的操作如下:
static struct blk_mq_opsvirtio_mq_ops = {
.queue_rq =virtio_queue_rq,
.map_queue =blk_mq_map_queue,
.complete =virtblk_request_done,
.init_request =virtblk_init_request,
};
(2) 配置管理
virtblk_config_changed ==》 queue_work(virtblk_wq,&vblk->config_work);
virtblk_config_changed_work 用virtio_cread检察容量是否发生变化,如果发生变化则:
set_capacity(vblk->disk, capacity);
revalidate_disk(vblk->disk);
kobject_uevent_env(&disk_to_dev(vblk->disk)->kobj,KOBJ_CHANGE, envp);
(3) 电源管理
s3/s4 enter : virtblk_freeze
a. vdev->config->reset(vdev); //重置config
vblk->config_enable = false;
b. flush_work(&vblk->config_work);
blk_mq_stop_hw_queues(vblk->disk->queue); //停止queue
c. vdev->config->del_vqs(vdev);
s3/s4 resume:
vblk->config_enable = true;
ret = init_vq(vdev->priv);
if (!ret)
blk_mq_start_stopped_hw_queues(vblk->disk->queue, true);
(4) RW 管理
virtio_queue_rq(structblk_mq_hw_ctx *hctx, struct request *req)
a.根据request类别将block层的命令转为virtblk_req,如REQ_TYPE_BLOCK_PC:
vbr->out_hdr.type = VIRTIO_BLK_T_SCSI_CMD;
vbr->out_hdr.sector = 0;
vbr->out_hdr.ioprio = req_get_ioprio(vbr->req);
b. 建立sglist 到virblk_req: sgblk_rq_map_sg(hctx->queue, vbr->req, vbr->sg);
c. 将virtblk_req加入到virtio的queue中
__virtblk_add_req ==》 virtqueue_add_sgs==》 virtqueue_add
d. 提交到virtqueue
virtqueue_kick_prepare /
virtqueue_notify ==》vq->notify
由以上分析有两大问题
(1) guest os的struct virtio_device *vdev是如何创建的
(2) virtqueue是如何工作的
6.1.3.2 virtio device的创建
有了virtio_device我们根据linux 设备对象模型需要分析bus driver. 代码在drivers/virtio中。
virtio_init ==>bus_register(&virtio_bus)
static struct bus_typevirtio_bus = {
.name ="virtio",
.match = virtio_dev_match,
.dev_groups = virtio_dev_groups,
.uevent = virtio_uevent,
.probe = virtio_dev_probe,
.remove = virtio_dev_remove,
};
virtio_dev_probe会调用drv->probe. 同时在virtio中提供了函数register_virtio_device, 用于注册virtio_device。
register_virtio_device, 主要代码如下:
dev->config->reset(dev);
add_status(dev, VIRTIO_CONFIG_S_ACKNOWLEDGE);
INIT_LIST_HEAD(&dev->vqs);
err = device_register(&dev->dev);
其调用者为virtio_mmio.c(为platform驱动) 和virtio_pci.c(为pci驱动), 这说明virtio_blk不是最低层的驱动; 我们这里分析virtio_pci.c. 对于pci设备是由qemu虚拟出来的;
int virtio_pci_probe(structpci_dev *pci_dev, const struct pci_device_id *id)
a. 建立struct virtio_pci_device *vp_dev;
vp_dev = kzalloc(sizeof(struct virtio_pci_device), GFP_KERNEL);
vp_dev->vdev.dev.parent = &pci_dev->dev;
vp_dev->vdev.dev.release = virtio_pci_release_dev;
vp_dev->vdev.config = &virtio_pci_config_ops;
vp_dev->pci_dev = pci_dev;
这里的config就是我们上一节在virtio_cread_feature中最终会调用到的。
b. pci 的基本操作
pci_msi_off(pci_dev);
err = pci_enable_device(pci_dev);
err = pci_request_regions(pci_dev, "virtio-pci");
vp_dev->ioaddr = pci_iomap(pci_dev, 0, 0);
pci_set_drvdata(pci_dev, vp_dev);
pci_set_master(pci_dev);
vp_dev->vdev.id.vendor = pci_dev->subsystem_vendor;
vp_dev->vdev.id.device = pci_dev->subsystem_device;
c. 调用register_virtio_device(&vp_dev->vdev);这样会触发virtio_driver->probe
static const structvirtio_config_ops virtio_pci_config_ops = {
.get = vp_get,
.set = vp_set,
.get_status =vp_get_status,
.set_status =vp_set_status,
.reset = vp_reset,
.find_vqs = vp_find_vqs,
.del_vqs = vp_del_vqs,
.get_features =vp_get_features,
.finalize_features = vp_finalize_features,
.bus_name = vp_bus_name,
.set_vq_affinity = vp_set_vq_affinity,
};
static void vp_get(structvirtio_device *vdev, unsigned offset,
void *buf, unsignedlen)
{
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
void __iomem *ioaddr = vp_dev->ioaddr +
VIRTIO_PCI_CONFIG(vp_dev) + offset;
u8 *ptr = buf;
int i;
for (i = 0; i < len; i++)
ptr[i] = ioread8(ioaddr + i); //ioaddr由virtio_pci_probe的pci_iomap分配
}
再来看看vp_find_vqs
上一节的init_vq中会调用:
vblk->vq =virtio_find_single_vq(vblk->vdev, virtblk_done,"requests");
==》vdev->config->find_vqs(vdev, 1, &vq, callbacks, names);//callbakc=virtblk_done
==> vp_try_to_find_vqs:
a. 分为两种case: initx 和msix,我们这里分msix. ==> request_irq为pci_dev注册中断处理函数vp_config_changed 和vp_vring_interrupt
b. setup_vq创建virtqueue,
info->queue = alloc_pages_exact(size, GFP_KERNEL|__GFP_ZERO);
/* activate the queue */
iowrite32(virt_to_phys(info->queue) >>VIRTIO_PCI_QUEUE_ADDR_SHIFT,
vp_dev->ioaddr +VIRTIO_PCI_QUEUE_PFN);
//设置后VMM就能知道queue的地址,这时guest与vmm通讯的关键点
/* create the vring */
vq = vring_new_virtqueue(index, info->num,VIRTIO_PCI_VRING_ALIGN, vdev,
true,info->queue, vp_notify, callback, name);
//其中vq->vq.callback= callback; vq->notify = notify; //为vp_notify
static irqreturn_tvp_vring_interrupt(int irq, void *opaque)
{
spin_lock_irqsave(&vp_dev->lock, flags);
list_for_each_entry(info, &vp_dev->virtqueues, node) {
if (vring_interrupt(irq, info->vq) == IRQ_HANDLED)
ret = IRQ_HANDLED;
}
spin_unlock_irqrestore(&vp_dev->lock, flags);
return ret;
}
vring_interrupt ==》 vq->vq.callback 调用传输完成函数
vp_config_changed ==》 drv->config_changed用于配置发生变化
6.1.3.3 virtio 数据传输
virtio_queue_rq ==》 __virtblk_add_req ==》 virtqueue_add_sgs ==》virtqueue_add
for (n = 0; n < out_sgs; n++) {
for (sg = sgs[n]; sg; sg = next(sg, &total_out)) {
vq->vring.desc[i].flags = VRING_DESC_F_NEXT;
vq->vring.desc[i].addr = sg_phys(sg);
vq->vring.desc[i].len = sg->length;
prev = i;
i = vq->vring.desc[i].next;
}
}
for (; n < (out_sgs + in_sgs); n++) {
for (sg = sgs[n]; sg; sg = next(sg, &total_in)) {
vq->vring.desc[i].flags =VRING_DESC_F_NEXT|VRING_DESC_F_WRITE;
vq->vring.desc[i].addr = sg_phys(sg);
vq->vring.desc[i].len = sg->length;
prev = i;
i = vq->vring.desc[i].next;
}
}
vq->data[head] = data;//data为vbr
将sglist存在vq->vring_desc中,vbr存于vq->data[head]中
virtqueue_notify ==》vq->notify = vp_notify
vp_notify :通知vmm启动队列
iowrite16(vq->index, vp_dev->ioaddr + VIRTIO_PCI_QUEUE_NOTIFY);