您的位置:首页 > 产品设计 > UI/UE

6.1.Virtio 原理与Guest OS driver

2015-07-26 16:58 537 查看
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
; 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
; 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);
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: