VIRTIO PCI 设备

Virtio的代码主要分两个部分:QEMU和内核驱动程序。Virtio设备的模拟就是通过QEMU完成的,QEMU代码在虚拟机启动之前,创建虚拟设备。虚拟机启动后检测到设备,调用内核的virtio设备驱动程序来加载这个virtio设备。

对于KVM虚拟机,都是通过QEMU这个用户空间程序创建的,每个KVM虚拟机都是一个QEMU进程,虚拟机的virtio设备是QEMU进程模拟的,虚拟机的内存也是从QEMU进程的地址空间内分配的。

VRING是由虚拟机virtio设备驱动创建的用于数据传输的共享内存,QEMU进程通过这块共享内存获取前端设备递交的IO请求。

如下图所示,虚拟机IO请求的整个流程:

1) 虚拟机产生的IO请求会被前端的virtio设备接收,并存放在virtio设备散列表scatterlist里;

2) Virtio设备的virtqueue提供add_buf将散列表中的数据映射至前后端数据共享区域Vring中;

3) Virtqueue通过kick函数来通知后端qemu进程。Kick通过写pci配置空间的寄存器产生kvm_exit;

4) Qemu端注册ioport_write/read函数监听PCI配置空间的改变,获取前端的通知消息;

5) Qemu端维护的virtqueue队列从数据共享区vring中获取数据

6)  Qemu将数据封装成virtioreq;

7)  Qemu进程将请求发送至硬件层。

前后端主要通过PCI配置空间的寄存器完成前后端的通信,而IO请求的数据地址则存在vring中,并通过共享vring这个区域来实现IO请求数据的共享。

从上图中可以看到,Virtio设备的驱动分为前端与后端:前端是虚拟机的设备驱动程序,后端是host上的QEMU用户态程序。为了实现虚拟机中的IO请求从前端设备驱动传递到后端QEMU进程中,Virtio框架提供了两个核心机制:前后端消息通知机制和数据共享机制。

消息通知机制,前端驱动设备产生IO请求后,可以通知后端QEMU进程去获取这些IO请求,递交给硬件。

数据共享机制,前端驱动设备在虚拟机内申请一块内存区域,将这个内存区域共享给后端QEMU进程,前端的IO请求数据就放入这块共享内存区域,QEMU接收到通知消息后,直接从共享内存取数据。由于KVM虚拟机就是一个QEMU进程,虚拟机的内存都是QEMU申请和分配的,属于QEMU进程的线性地址的一部分,因此虚拟机只需将这块内存共享区域的地址传递给QEMU进程,QEMU就能直接从共享区域存取数据。

PCI配置空间

由整体流程图可知,guest和host交互传送信息的两个重要结构分别的PCI config和vring,本节重点分析实现消息通知机制的PCI配置空间。

虚拟机是如何获取PCI配置空间的?

首先,我们为虚拟机创建的virtio设备都是PCI设备,它们挂在PCI总线上,遵循通用PCI设备的发现、挂载等机制。

当虚拟机启动发现virtio PCI设备时,只有配置空间可以被访问,配置空间内保存着该设备工作所需的信息,如厂家、功能、资源要求等,通过对这个空间信息的读取,完成对PCI设备的配置。同时配置空间上有一块存储器空间,里面包含了一些寄存器和IO空间。

前后端的通知消息就是写在这些存储空间的寄存器,virtio会为它的PCI设备注册一个PCI BAR来访问这块寄存器空间。配置空间如下图所示:

虚拟机系统在启动过程中在PCI总线上发现virtio-pci设备,就会调用virtio-pci的probe函数。该函数会将PCI配置空间上的寄存器映射到内存空间,并将这个地址赋值给virtio_pci_device的 ioaddr 变量。之后要对PCI配置空间上的寄存器操作时,只需要ioaddr+偏移量。

pci_iomap函数完成PCI BAR的映射,第一个参数是pci设备的指针,第二个参数指定我们要映射的是0号BAR,第三个参数确定要映射的BAR空间多大,当第三个参数为0时,就将整个BAR空间都映射到内存空间上。VirtioPCI设备的0号BAR指向的就是配置空间的寄存器空间,也就是配置空间上用于消息通知的寄存器。

通过pci_iomap之后,我们就可以像操作普通内存一样(调用ioread和iowrite)来读写pci硬件设备上的寄存器。

虚拟机是如何操作这个配置空间的?

1. kick
当前端设备的驱动程序需要通知后端QEMU程序执行某些操作的时候,就会调用kcik函数,来触发读写PCI配置空间寄存器的动作。
2. 读写PCI寄存器
ioread/iowrite实现了对配置空间寄存器的读写,例如:

vp_dev->ioaddr + VIRTIO_PCI_QUEUE_NOTIFY 表示写notify这个寄存器,位置如图 2 1所示。

ioread读取QEMU端在配置空间寄存器上写下的值。
在读写PCI设备配置空间的操作中,我们可以看到都是通过iodaar+偏移,来指向某个寄存器,ioaddr这个变量是我们在Virtio-pci设备初始化的时候对它赋值,并指向配置空间寄存器的首地址位置。

QEMU如何感知虚拟机的操作的?

虚拟机内调用kick函数实现通知之后,会产生KVM_EXIT。Host端的kvm模块捕获到这个EXIT之后,根据它退出的原因来做处理。如果是一个IO_EXIT,kvm会将这个退出交给用户态的QEMU程序来完成IO操作。
QEMU为kvm虚拟机模拟了virtio设备,因此后端的virtio-pci设备也是在QEMU进程中模拟生成的。QEMU对模拟的PCI设备的配置空间注册了回调函数,当虚拟机产生IO_EXIT,就调用这些函数来处理事件。

这里只分析legacy模式,其实在初始化阶段guest会判断设备是否支持modern模式,如果支持,回调函数会发生一些变化。挖个坑有时间以后补。
1. 监听PCI寄存器
virtio_ioport_write/read就是QEMU进程监听PCI配置空间上寄存器消息的函数,针对前端iowrite/ioread读写了哪个PCI寄存器,来决定下一步操作:

2. 监听函数的注册
PCI寄存器的这些监听函数,都是在QEMU为虚拟机创建虚拟设备的时候注册。

QEMU先为虚拟机的virtio-pci设备创建PCI配置空间,配置空间内包含了设备的一些基本信息;在配置空间的存储空间位置注册了一个PCI BAR,并为这个BAR注册了回调函数监听寄存器的改变。

这部分代码是初始化配置空间的基本信息。  

给PCI设备注册了PCI BAR,指定起始地址为PCI_BASE_ADDRESS_SPACE_IO(即PCI配置空间中存储空间到配置空间首地址的偏移值);

指定这个BAR的大小为size,回调函数为virtio_pci_config_ops中的读写函数。

 这里的read/write最终都会调用virtio_ioport_write(virtio_ioport_write处理前端写寄存器时触发的事件,virtio_ioport_read处理前端要读寄存器时触发的事件)来统一的管理。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值