当前位置:   article > 正文

crosvm Hypervisor:VFIO

vfio

前言

背景。虚拟化环境中,为提高虚拟机性能,需要将主机设备直通给虚拟机(vm),即:设备透传技术(也叫设备直通技术),该技术需要主机支持Intel(VT-d)或AMD (AMD-Vi)或ARM(SMMU) 硬件虚拟化加速技术。
VFIO.简称虚拟功能I/O,VFIO是一套完整的用户态驱动(userspace driver)方案,它可以安全地把设备I/O、中断、DMA等能力呈现给用户空间。
目的。VFIO驱动程序框架旨在替换KVM PCI特定设备分配代码,并提供比UIO更安全、功能更强大的用户空间驱动程序环境,通过VFIO向用户态开放IOMMU的功能,编写用户态的驱动。
应用。如OVMF支持将GPU透传给qemu虚拟机
依赖。CONFIG_VFIO,CONFIG_VFIO_IOMMU_TYPE1,CONFIG_VFIO_PLATFORM
代码。内核及用户空间源码:
 git://github.com/awilliam/linux-vfio.git
 git://github.com/awilliam/qemu-vfio.git
商用。2020.3 Cloud Hypervisor 0.6能够为已经运行的客户机热(拔)直接分配的基于VFIO PCI的设备
商用。华为stratovirt VFIO未实现VFIO PLATFORM。https://gitee.com/openeuler/stratovirt/tree/master/vfio/src
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

VFIO基础

在这里插入图片描述

DMA. CPU把外设和内存地址空间合并,读写外设空间很慢,因此把数据放内存,让外设自己去读,这个动作称为DMA,由DMA控制器完成。设备和CPU通过存储控制器访问存储器。一个简单的case是CPU向存储器写数据,然后设备从存储器读数据,CPU和设备都会走缓存验证一遍以后,再落到存储器上,这样带上缓存以后大家的一致性都是一样的了。设备其实也可以和CPU一样有一层MMU,也就是地址到存储器物理地址的转换。注意,这里我用了地址,因为对CPU来说是虚拟地址,但是对设备来说是一个总线域的地址。这里要明确区分一下,一个是总线地址,是从设备的角度来看的,一个是CPU的虚拟地址,这是从CPU角度来看的,两个是不同的东西。将总线域地址转换成存储器物理地址的设备就叫IOMMU。
IOMMU与设备透传.IOMMU在不同架构上名字不太一样,AMD叫AMD-Vi,Intel叫VT-d,Arm叫SMMU。在VT-d中,dmar(DMA remapping)就是那个IOMMU设备,通过中断的方式实现类似page fault一样的内存分配行为。DMA传输是由CPU发起的:CPU会告诉DMA控制器,帮忙将xxx地方的数据搬到xxx地方。CPU发完指令之后,就当甩手掌柜了。IOMMU有点像MMU是一个将设备地址翻译到内存地址的页表体系,也会有对应的页表,这个东西在虚拟化中也非常有用,可以将原本有软件模拟的设备,用直接的硬件替代,而原本的隔离通过IOMMU来完成。如下图所示,原本需要通过软件模拟的驱动设备可以通过IOMMU以安全的方式来直接把硬件设备分配个用户态的Guest OS.
左图是没有IOMMU的情况,对于虚拟机无法实现设备的透传,原因主要有两个:1)没有IOMMU的情况下,设备必须访问真实的物理地址HPA,而虚机可见的是GPA;
2)如果让虚机填入真正的HPA,相当于虚机可以直接访问物理地址,有安全隐患。所以针对没有IOMMU的情况,不能用透传的方式,对于设备的访问都会有VMM接管,这样就不会对虚机暴露HPA。
右图是有IOMMU的情况,虚拟机可以将GPA直接写入到设备,当设备进行DMA传输时,设备请求地址GPA由IOMMU转换为HPA(硬件自动完成),进而DMA操作真实的物理空间。IOMMU的映射关系是由VMM维护的,HPA对虚机不可见,保障了安全问题,利用IOMMU可实现设备的透传。
  • 1
  • 2
  • 3
  • 4
  • 5

VFIO概念

先介绍VFIO中的几个重要概念,主要包括Group和Container。
Group:group 是IOMMU能够进行DMA隔离的最小硬件单元,一个group内可以有多个device,这取决于物理平台上硬件的IOMMU拓扑结构。设备直通的时候一个group里面的设备必须都直通给一个虚拟机。 不能够让一个group里的多个device分别从属于2个不同的VM,也不允许部分device在host上而另一部分被分配到guest里, 因为就这样一个guest中的device可以利用DMA攻击获取另外一个guest里的数据,就无法做到物理上的DMA隔离。
Container:对于虚机,Container 这里可以简单理解为一个VM Domain的物理内存空间。对于用户态驱动,Container可以是多个Group的集合。在iommu_group的层级上,VFIO封装了一层container class,这个的作用对应于希望能够在不同的iommu_group之间共享TLB和page tables
  • 1
  • 2
  • 3

VFIO PCI官方举例

Linux官方文档描述了如何使能VFIO和用户态程序如何透传设备。
1.获取PCI设备0000:06:0d.0的group_id (PCI命名的规则是domain:bus:slot.func)
  • 1
  • 2
$ readlink /sys/bus/pci/devices/0000:06:0d.0/iommu_group
../../../../kernel/iommu_groups/26 
  • 1
  • 2
2.使用之前需要确保已经加载了VFIO内核模块
modprobe vfio-pci 
3.解绑PCI设备,然后创建一个container id
  • 1
  • 2
  • 3
$ lspci -n -s 0000:06:0d.0
06:0d.0 0401: 1102:0002 (rev 08)
echo 0000:06:0d.0 > /sys/bus/pci/devices/0000:06:0d.0/driver/unbind
echo 1102 0002 > /sys/bus/pci/drivers/vfio-pci/new_id 
  • 1
  • 2
  • 3
  • 4
4.然后寻找其他同属于一个group的设备,需要都解绑。PCI桥0000:00:1e.0后面挂了两个设备,一个是刚才加进去的0000:06:0d.0,还有一个是0000:06:0d.1,通过上面的步奏加进去就可以。
  • 1
$ ls -l /sys/bus/pci/devices/0000:06:0d.0/iommu_group/devices
total 0
lrwxrwxrwx. 1 root root 0 Apr 23 16:13 0000:00:1e.0 ->
../../../../devices/pci0000:00/0000:00:1e.0
lrwxrwxrwx. 1 root root 0 Apr 23 16:13 0000:06:0d.0 ->
../../../../devices/pci0000:00/0000:00:1e.0/0000:06:0d.0
lrwxrwxrwx. 1 root root 0 Apr 23 16:13 0000:06:0d.1 ->
../../../../devices/pci0000:00/0000:00:1e.0/0000:06:0d.1 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
5.最后让用户有权限使用这个 group
chown user:user /dev/vfio/26 
6.下面就是一个样例,从用户态使用 VFIO,整个的使用方式是通过ioctl来获取中断相关信息,以及注册中断处理函数,然后也是通过ioctl来获取region信息,然后调用相应的mmap函数,让CPU可以访问内存。
  • 1
  • 2
  • 3
int container, group, device, i;
struct vfio_group_status group_status =
				{ .argsz = sizeof(group_status) };
struct vfio_iommu_type1_info iommu_info = { .argsz = sizeof(iommu_info) };
struct vfio_iommu_type1_dma_map dma_map = { .argsz = sizeof(dma_map) };
struct vfio_device_info device_info = { .argsz = sizeof(device_info) };

/* Create a new container */
container = open("/dev/vfio/vfio", O_RDWR);

if (ioctl(container, VFIO_GET_API_VERSION) != VFIO_API_VERSION)
	/* Unknown API version */

if (!ioctl(container, VFIO_CHECK_EXTENSION, VFIO_TYPE1_IOMMU))
	/* Doesn't support the IOMMU driver we want. */

/* Open the group */
group = open("/dev/vfio/26", O_RDWR);

/* Test the group is viable and available */
ioctl(group, VFIO_GROUP_GET_STATUS, &group_status);

if (!(group_status.flags & VFIO_GROUP_FLAGS_VIABLE))
	/* Group is not viable (ie, not all devices bound for vfio) */

/* Add the group to the container */
ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);

/* Enable the IOMMU model we want */
ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);

/* Get addition IOMMU info */
ioctl(container, VFIO_IOMMU_GET_INFO, &iommu_info);

/* Allocate some space and setup a DMA mapping */
dma_map.vaddr = mmap(0, 1024 * 1024, PROT_READ | PROT_WRITE,
		     MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
dma_map.size = 1024 * 1024;
dma_map.iova = 0; /* 1MB starting at 0x0 from device view */
dma_map.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE;

ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);

/* Get a file descriptor for the device */
device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:06:0d.0");

/* Test and setup the device */
ioctl(device, VFIO_DEVICE_GET_INFO, &device_info);

for (i = 0; i < device_info.num_regions; i++) {
	struct vfio_region_info reg = { .argsz = sizeof(reg) };

	reg.index = i;

	ioctl(device, VFIO_DEVICE_GET_REGION_INFO, &reg);

	/* Setup mappings... read/write offsets, mmaps
	 * For PCI devices, config space is a region */
}

for (i = 0; i < device_info.num_irqs; i++) {
	struct vfio_irq_info irq = { .argsz = sizeof(irq) };

	irq.index = i;

	ioctl(device, VFIO_DEVICE_GET_IRQ_INFO, &irq);

	/* Setup IRQs... eventfds, VFIO_DEVICE_SET_IRQS */
}

/* Gratuitous device reset and go... */
ioctl(device, VFIO_DEVICE_RESET); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
7.VFIO_IOMMU_MAP_DMA. 首先,利用mmap映射出1MB字节的虚拟空间,因为物理地址对于用户态不可见,只能通过虚拟地址访问物理空间。然后执行ioctl的VFIO_IOMMU_MAP_DMA命令,传入参数主要包含vaddr及iova,其中iova代表的是设备发起DMA请求时要访问的地址,也就是IOMMU映射前的地址,vaddr就是mmap的地址。VFIO_IOMMU_MAP_DMA命令会为虚拟地址vaddr找到物理页并pin住(因为设备DMA是异步的,随时可能发生,物理页面不能交换出去),然后找到Group对应的Contex Entry,建立页表项,页表项能够将iova地址映射成上面pin住的物理页对应的物理地址上去,这样对用户态程序完全屏蔽了物理地址,实现了用户空间驱动。IOVA地址的0x0~0x100000对应DRAM地址0x10000000~0x10100000,size为1024 * 1024。一句话概述,VFIO_IOMMU_MAP_DMA这个命令就是将iova通过IOMMU映射到vaddr对应的物理地址上去。
8.内核VFIO-PCI
a.首先是作为 PCI 设备的 probe。主要是通过 vfio_iommu_group_get 分配 iommu_group,然后调用vfio_add_group_dev初始化设备回调接口vfio_pci_ops,而remove就是反过来把对应的结构释放掉就可以。然后再看注册的回调函数结构体。
  • 1
  • 2
  • 3
static const struct vfio_device_ops vfio_pci_ops = {
	.name		= "vfio-pci",
	.open		= vfio_pci_open,
	.release	= vfio_pci_release,
	.ioctl		= vfio_pci_ioctl,
	.read		= vfio_pci_read,
	.write		= vfio_pci_write,
	.mmap		= vfio_pci_mmap,
	.request	= vfio_pci_request,
}; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
b.这里分析几个关键的函数,他们会通过file_operations vfio_fops被间接的调用。
c.首先是 mmap,就是在调用vfio_pci_mmap的时候最终调用remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, req_len, vma->vm_page_prot); 来将物理内存映射到用户态空间,这就是上面的栗子中 mmap 系统调用的入口,而具体要映射的物理内存是通过一系列pci_resource_xxx宏从 PCI bar 空间读出来的配置。
d.然后是 ioctl 接口,这个接口比较丰富,也简单的看一下。如VFIO_DEVICE_SET_IRQS会通过使用用户态传进来的结构体,调用vfio_pci_set_irqs_ioctl注册中断处理函数。而通过vfio_ioctl_set_iommu会设置 container 的 iommu_group 以及对应的 driver。read/write接都是用于修改 PCI 配置信息的。
简单的来说,VFIO 的主要工作是把设备通过 IOMMU 映射的 DMA 物理内存地址映射到用户态中,让用户态程序可以自行操纵设备的传输,并且可以保证一定程度的安全,另外可以自行注册中断处理函数,从而在用户态实现设备的驱动程序,通过这样的框架,可以在 DPDK 中充分发挥用户态协议栈的威力。
  • 1
  • 2
  • 3
  • 4

VFIO Platform举例

网上没有VFIO-PLATFORM官方举例,此处以瑞萨RCAR虚拟化为基础,运用到IMX8
  • 1
/* 以imx8 agl透传ethernet为例 */
/* 0. 查看哪些设备被加入到iommu,只有iommu中的设备才能透传,sata/eth/usb/mmc*/
dmesg | grep iommu
/* 1. Host unbind driver */
echo 5b040000.ethernet > /sys/bus/platform/drivers/fec/5b040000.ethernet/driver/unbind
/* 2. Host rebind device to vfio-platform driver */
echo vfio-platform > /sys/bus/platform/devices/5b040000.ethernet/driver_override
echo 5b040000.ethernet > /sys/bus/platform/drivers/vfio-platform/bind 
/* 或者透传sata设备 */ 
echo 5f020000.sata > /sys/bus/platform/drivers/ahci-imx/5f020000.sata/driver/unbind;
echo vfio-platform > /sys/bus/platform/devices/5f020000.sata/driver_override;
echo 5f020000.sata > /sys/bus/platform/drivers/vfio-platform/bind 
/* 3. qemu侧,增加该设备透传 */
-device vfio-platform,host=5b040000.ethernet 
/* 4. 或者Crosvm侧,透传设备 */
./crosvm run --vfio-platform=/sys/bus/platform/devices/5b040000.ethernet  --disable-sandbox --rwroot ./rootfs-ext4-5m.img ./Image
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

crosvm VFIO Platform

crosvm VFIO Platform代码分层
从接口来看,可以分为VFIO平台无关的接口层,VFIO平台设备实现层
从流程来看,可以分为创建VFIO平台设备,创建VFIO平台总线
  • 1
  • 2
  • 3

在这里插入图片描述

VFIO平台无关接口层

VFIO平台无关的接口层,用于向用户态提供访问硬件设备的接口(VfioDevice),向用户态提供配置IOMMU的接口(VfioContainer).
  • 1
VFIO设备接口
VfioDevice #创建VfioDevice,guest对设备的read/write可以传到Kernel VFIO
    new #创建Vfio设备
    get_group_id #readlink /sysfs/device/iommu_group获取group id
    get_group  #检查和设置iommu版本,把VM地址空间加到iommu 
    get_device #获取5b040000.ethernet设备文件描述符
    get_region  #获取设备信息,设备BAR地址空间
irq_enable #使能vfio设备irq和关联irqfd
resample_virq_enable #开启重采用中断
irq_disable
irq_unmask
irq_mask
validate_dev_info
get_irqs #查询中断信息
get_regions #查询中断region
get_region_flags  #获取区域信息,是否支持read/write/mmap/caps
get_region_offset #获取vfio设备描述符偏移
get_region_size #获取区域尺寸
get_region_mmap #获取区域映射
region_read  #将vfio设备指定区域读到buf
region_write #将buf写到vfio设备指定区域
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
VFIO容器接口
VfioContainer #实现了VFIO容器API,用于操作IOMMU 
    new #新建一个VfioContainer
    is_group_set #检查group是否被设置
check_extension #检查内核支持的IOMMU driver版本,当前支持
                   #VFIO_TYPE1_IOMMU和VFIO_TYPE1v2_IOMMU
set_iommu     #使能IOMMU模块
vfio_dma_map #将指定区域设置为DMA映射,将iova通过IOMMU映射到vaddr
                #对应的物理地址上去,pin住,vaddr物理页不被交换出去
    vfio_get_iommu_page_size_mask #获取直通设备信息
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

VFIO平台设备实现层

VFIO平台设备实现层,分为VFIO-PCI设备实现和VFIO-PLATFORM实现,分别实现了该平台的特定方法。
  • 1
VFIO-PCI设备实现层
VfioPciDevice #模拟vfio pci设备的各种操作
    get_bar_configuration
    allocate_device_bars
    allocate_io_bars
    allocate_address
    read_config_register
write_config_register
read_bar
write_bar
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
VFIO-PLATFORM设备实现层
VfioPlatformDevice #模拟vfio platform设备的各种操作
    assign_platform_irq #设置设备irq route
    find_region 
    allocate_irq
    region_mmap
disable_irq
read_mmio  #将数据从VFIO设备读到buf
write_mmio #将数据从buf写到VFIO设备
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

创建VFIO平台设备

根据传入的透传设备,获取group,映射,创建VfioPlatformDevice
  • 1
run_vm --> create_devices src/linux.rs:1764
create_vfio_platform_device #Arm创建VFIO设备
    vfio_get_container        #创建container,IOMMU关闭
        VfioContainer::new    #打开/dev/vfio/vfio
            -> VFIO_GET_API_VERSION #检查API版本是否为0
    VfioDevice::new #创建VFIO设备,Guest read/write会转发到内核VFIO
        get_group #group相关准备工作
            VfioGroup::new #打开/dev/vfio/$group
                -> VFIO_GROUP_GET_STATUS     #检查group是否可用
                -> VFIO_GROUP_SET_CONTAINER #将group设到container
            VfioContainer::init #初始化容器,设置IOMMU,Guest内存加到vfio
                                   #容器IOMMU table
                set_iommu #设置IOMMU类型VFIO_TYPE1v2_IOMMU
                    -> VFIO_SET_IOMMU 
                GuestMemory::with_regions #DMA控制器可以访问VM物理内存
                    vfio_dma_map #把所有guest 256M内存都加到VFIO iommu
                                    #表做DMA映射,VFIO kernel可以访问所有
                                    #guest内存iova(0x80000000)guest_addr
                                    #(0xFFFFE7CFF000)size(256M)
                        -> VFIO_IOMMU_MAP_DMA #将iova通过IOMMU映射到
                                                  #vaddr对应的物理地址
            create_device #创建VFIO模拟设备
                             #kvm_device_type_KVM_DEV_TYPE_VFIO
                -> KVM_CREATE_DEVICE 
            kvm_device_set_group #设置设备GROUP属性
                -> KVM_SET_DEVICE_ATTR 
        get_device #获取5b040000.ethernet设备文件描述符
            -> VFIO_GROUP_GET_DEVICE_FD
        get_regions #获取VFIO设备region信息
            -> VFIO_DEVICE_GET_INFO #获取设备信息
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

在这里插入图片描述

                                #flags:4 表示platform device
                                #num_regions:1 表示只有一个region
                                #num_irqs:4 表示由4个IRQ Space
    -> VFIO_DEVICE_GET_REGION_INFO #进一步查询region信息
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述

                             #flags:7 region支持read/write/mmap
                             #index:0 region索引0
                             #size:65536 region尺寸64K
                             #offset:0 region相对device fd偏移
  • 1
  • 2
  • 3
  • 4

VfioPlatformDevice::new #创建平台设备

创建VFIO平台总线

build_vm #arch/src/lib.rs:415
generate_platform_bus #为VM创建平台总线,涉及MMIO,IRQ
        allocate_regions   #分配vfio设备platform mmio区域
                              #申请从0x90000000开始的64K匿名共享内存
        get_platform_irqs  #查询中断信息
            get_irqs         #获取设备IRQ信息
                -> VFIO_DEVICE_GET_INFO #获取VFIO设备信息
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述

        #flags:4 表示platform device
        #num_regions:1 表示只有一个region
        #num_irqs:4 表示由4个IRQ Space
-> VFIO_DEVICE_GET_IRQ_INFO #获取每个IRQ Space信息
                                  #循环4次,index 0->3
  • 1
  • 2
  • 3
  • 4
  • 5

在这里插入图片描述

                #flags:7 表示IRQ Space支持eventfd方式报告中断,可
                #以对其中的IRQ进行mask和unmask操作,
                #当IRQ上触发一次中断后,IRQ会自动被mask.
                #index:0 表示irq index
                #count:1 表示这个IRQ Space中IRQ个数为1
#为每一个IRQ Space创建irqfd和resample_fd,后面流程都会循环4次
allocate_irq #从系统分配器为每一个irq Space申请中断号,
                #如index0申请irq_num 7
register_irq_event #向KVM VGIC注册可以触发7号GSI中断的eventfd
    register_irqfd #[gsi=7,8,9,10, irqfd],vmm向VM注入中断
        -> KVM_IRQFD
assign_platform_irq #使能vfio设备中断 [irqfd,index]和配置重采样
    irq_enable #使能vfio设备的irq和关联irqfd事件
        -> VFIO_DEVICE_SET_IRQS
    irq_mask #由于中断电平触发,为设置重采样,先mask irq
        -> VFIO_DEVICE_SET_IRQS
    resample_virq_enable #Guest EOI重采样irqfd,通过resamplefd
                             #将取消屏蔽从KVM发到VFIO
        -> VFIO_DEVICE_SET_IRQS #EOI(end of interrupt)信号
    irq_unmask #设置完毕,unmask irq
#硬件中断 --> Kernel VFIO irq_handler --> irqfd 注入中断 --> Guest重采样irqfd --> Kernel VFIO可以再次接受硬件中断
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

三个关键点

VFIO Platform设备被透传到虚拟机,从设备角度看,必须要实现以下三个关键点:
1.MMIO Direct Access。映射设备MMIO空间到虚拟机,虚拟机访问MMIO无需陷出
2.IRQ Route。物理设备中断能路由到虚拟机
3.DMA Remapping。设备DMA控制器能访问虚拟机地址空间
下面将接着讲解这三个点:
  • 1
  • 2
  • 3
  • 4
  • 5
MMIO Direct Access
MMIO基础
  • 1
1.内存映射I/O. 内存和I/O设备共享同一个地址空间。CPU使用相同的地址总线来处理内存和I/O设备,I/O设备的内存和寄存器被映射到与之相关联的地址。当CPU访问某个内存地址时,它可能是物理内存,也可以是某个I/O设备的内存。
ioremap+mmap. 在Linux中,内核使用ioremap将IO设备的物理内存地址映射到内核空间的虚拟地址上;用户空间程序使用mmap将IO设备的物理内存地址映射到用户空间的虚拟内存地址上,一旦映射完成,用户空间的一段内存就与IO设备的内存关联起来,当用户访问用户空间的这段内存地址范围时,实际上会转化为对IO设备的访问。
MMIO Direct Access
1.MMIO透传。VM能直接访问设备MMIO空间,而无需陷出到VMM模拟MMIO读写,提升性能。
crosvm与MMIO。crosvm对设备MMIO是模拟(第一次)+透传(后续所有),VM第一次Read/Write操作先VmExit陷出到crosvm进行MMIO读写模拟,模拟完成后将MMIO区域mmap到VM地址空间,后续VM对MMIO访问就不再陷出。
  • 1
  • 2
  • 3
  • 4
  • 5

crosvm实现流程

1.虚拟机陷出,crosvm设备进程模拟MMIO操作
a.模拟mmio_read
mmio_read #将某一地址内容从VFIO设备region读到buf
regions_mmap -> RegisterMmapMemory #通过tube通知crosvm主线程映射MMIO区域
b.模拟mmio_write
mmio_write #将buf写到VFIO设备region
regions_mmap -> RegisterMmapMemory #通过tube通知crosvm主线程映射MMIO
c.MMIO区域为VFIO_DEVICE_GET_REGION_INFO获取设备的64K MMIO区域

2.crosvm主线程向VM添加MMIO区域
a.RegisterMmapMemory
vm.add_memory_region #将MappedRegion插入到VM地址空间
    set_user_memory_region #通知KVM建立MMIO的GPA<-->HPA映射关系
            -> ioctl(KVM_SET_USER_MEMORY_REGION)
b.通知crosvm设备进程映射完成

3.crosvm设备进程
a.收到响应后,只是VM的主进程映射了该区域,设备进程再做一次映射.用GPA当作IOVA,这样设备DMA控制器也能访问新加的MMIO空间
    vfio_dma_map (iova:guest_map_start, mmap_size, host, true)
#把64K MMIO区域加到VFIO iommu表做DMA映射,VFIO Kernel可以访问guest
#内存MMIO区域
    -> VFIO_IOMMU_MAP_DMA #将iova通过IOMMU映射到vaddr对应的物理地址
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
IRQ路由

KVM虚拟化外设中断基础

Irqfd. 基于eventfd机制,VMM中将一个gsi(全局系统中断号)与eventfd捆绑后,向kvm发送注册irqfd请求,kvm收到请求后将带有gsi信息的eventfd加入到与irqfd有关的等待队列中,一旦有进程向该eventfd写入,等待队列中的元素就会唤醒,并调用相应唤醒函数(irqfd_wakeup)向Guest注入中断。
事件源. 可以是虚拟设备,比如VFIO框架等
  • 1
  • 2
crosvm IRQ路由概述。硬件中断 --> Kernel VFIO irq_handler --> 将中断中继到crosvm向KVM注册的eventfd,irqfd 注入中断 --> VM中断处理 --> Guest EOI重采样irqfd --> Kernel VFIO可以再次接受硬件中断
  • 1

crosvm实现流程

1.向KVM设置IRQFD:
        register_irq_event #为VFIO设备每个IRQ SPACE向KVM VGIC注册可
                               #以触发GSI中断的eventfd
            register_irqfd  #[irq_region:0-3,gsi:7-10,irqfd]
                -> ioctl(KVM_IRQFD)
2.向Kernel VFIO设置中断:
        assign_platform_irq #设置vfio设备中断 [irqfd,index]
            irq_enable #使能vfio设备的irq和关联irqfd事件
                -> ioctl(VFIO_DEVICE_SET_IRQS)
            resample_virq_enable #Guest EOI重采样irqfd
                -> ioctl(VFIO_DEVICE_SET_IRQS)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
DMA Remapping

无虚拟化场景VFIO DMA原理

1.Mmap。利用mmap映射出1MB字节的虚拟空间,因为物理地址对于用户态不可见,只能通过虚拟地址访问物理空间。
VFIO_IOMMU_MAP_DMA。执行ioctl的VFIO_IOMMU_MAP_DMA命令,传入参数主要包含vaddr及iova,其中iova代表的是设备发起DMA请求时要访问的地址,也就是IOMMU映射前的地址,vaddr就是mmap的地址。VFIO_IOMMU_MAP_DMA命令会为虚拟地址vaddr找到物理页并pin住(因为设备DMA是异步的,随时可能发生,物理页面不能交换出去),然后找到Group对应的Contex Entry,建立页表项,页表项能够将iova地址映射成上面pin住的物理页对应的物理地址上去,这样对用户态程序完全屏蔽了物理地址,实现了用户空间驱动。IOVA地址的0x0~0x100000对应DRAM地址0x10000000~0x10100000,size为1024 * 1024。一句话概述,VFIO_IOMMU_MAP_DMA这个命令就是将iova通过IOMMU映射到vaddr对应的物理地址上去。
  • 1
  • 2

虚拟化寻址原理

1.VM自动寻址。从上图虚机寻址的映射过程来看,硬件自动完成GVA->GPA->HPA转换。
2.KVM建立GVA->GPA->HPA。VMM利用ioctl控制KVM实现EPT的映射,映射的过程中必然要申请物理页面。crosvm是应用程序,唯一可见的只是HVA,这时候又需要借助mmap了,crosvm会根据虚机的ram大小,即GPA大小范围,然后mmap出与之对应的大小,即HVA。通过KVM_SET_USER_MEMORY_REGION命令控制KVM,与这个命令一起传入的参数主要包括两个值,guest_phys_addr代表虚机GPA地址起始,userspace_addr代表上面mmap得到的首地址(HVA)。传入进去后,KVM就会为当前虚机GPA建立EPT映射表实现GPA->HPA,同时会为VMM建立HVA->HPA映射。
Qemu维护GPA<->HPA.当vm_exit发生时,VMM需要对异常进行处理,异常发生时VMM能够获取到GPA,有时VMM需要访问虚机GPA对应的HPA,VMM的映射和虚机的映射方式不同,是通过VMM完成HVA->HPA,且只能通过HVA才能访问HPA。
  • 1
  • 2
  • 3

虚拟化场景DMA原理

1.设备的DMA透传的工作流程。一旦设备透传给了虚机,虚机在配置设备DMA时直接使用GPA(VM中驱动也可以使用GVA,SMMUV3支持2级页面转换,此处为了简单讲解,只说1级)。此时GPA经由EPT会映射成HPA1,GPA经由IOMMU映射的地址为HPA2,此时的HPA1和HPA2必须相等,设备的透传才有意义。下面介绍在配置IOMMU时如何保证HPA1和HPA2相等:
2.VFIO_IOMMU_MAP_DMA。前面讲到了VFIO_IOMMU_MAP_DMA这个命令就是将iova通过IOMMU映射到vaddr对应的物理地址上去。对于IOMMU来讲,此时的GPA就是iova,我们知道GPA经由EPT会映射为HPA1,对于VMM来讲,这个HPA1对应的虚机地址为HVA,那样的话在传入VFIO_IOMMU_MAP_DMA命令时将hva作为vaddr,IOMMU就会将GPA映射为HVA对应的物理地址及HPA1,即HPA1和HPA2相等。
3.映射尺寸。前面提到了VMM维护了GPA和HVA的关系,在映射IOMMU的时候也可以派上用场。OMMU的映射在虚机启动时就已经建立好了,映射要涵盖整个GPA地址范围,同时虚机的HPA对应的物理页都不会交换出去(设备DMA交换是异步的)。
  • 1
  • 2
  • 3

crosvm与DMA。外设的DMA控制器通过IOMMU可以访问整个VM的256M GPA物理地址空间
crosvm实现逻辑

VfioDevice::new
    VfioContainer::init
            GuestMemory::with_regions #DMA控制器可以访问VM物理内存
                vfio_dma_map #把所有guest 256M内存都加到VFIO iommu
                                #表做DMA映射,VFIO kernel可以访问所有
                                #guest内存iova(0x80000000)guest_addr
                                #(0xFFFFE7CFF000)size(256M)
                     -> VFIO_IOMMU_MAP_DMA #将iova通过IOMMU映射到
                                                #vaddr对应的物理地址
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/365503
推荐阅读
相关标签
  

闽ICP备14008679号