6.1.Virtio 原理与Guest OS driver


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);

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值