首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

KVM 虚拟化技术在 AMD 平台上的实现(7)

KVM 虚拟化技术在 AMD 平台上的实现(7)

IOMMU   采用 virtIO 设备比仿真物理设备的方式会急剧的减小因 MMIO 和 PIO 截取而导致的 VMEXIT 数量,因此客操作系统性能会有明显的提升。 但 virtIO 方式仍然没有摆脱这种由 VMM 层来实现 IO 设备行为的架构,客操作系统和 VMM 之间仍然存在必不可少的交互操作,让 virtIO 的性能难以提升到真实物理设备所能提供的能力。让客操作系统直接使用 PCI 设备 ( 或称 PCI-passthrough) 是最大化其 IO 性能的方式。
   然而,如果没有特殊的硬件支持,让客操作系统直接使用 PCI 设备会带来问题。 第一个问题是,PCI 设备通过 DMA 方式访问内存,最终需要向 DRAM 控制器发出一个有效的物理地址,即 hpa, 但是运行在客操作系统上的设备驱动,其向设备的 DMA 控制寄存器写人的必然是一个 gpa, 这种地址的差别如何解决?第二个问题是对 DMA 的地址和设备的中断控制的问题,如何防止客操作系统恶意地设置用于 DMA 的地址和或通过 PCI 配置空间发起恶意的中断。
  在 AMD 平台上,IOMMU 就是解决虚拟机直接使用物理设备问题的技术。 在物理上,IOMMU 就是一个针对于外围设备的内存管理单元,相似于 CPU 上的 MMU。 AMD IOMMU 提供如下功能:
  • 地址翻译。
  • 隔离和访问控制。
   在 IOMMU 出现之前,x86 上的 GART 硬件实际上已经提供了一定的地址翻译功能。GART 和 IOMMU 相比,在地址翻译上局限性比较大,第一个局限是 GART 只能将其 Aperture 空间的地址翻译成随意的物理地址, 每个使用 GART 的设备必须从 Aperture 申请空间,由于 Aperture 空间的局限性,GART 一般只是用于由 AGP 卡独立使用,进行数据拷贝的情形; 第二个局限就是 GART 不具备地址空间隔离能力,所有的设备只能通过一个翻译表将 Aperture 地址空间翻译成其他物理地址区域,无法阻止一个设备的驱动使用另外一个设备的 Aperture 范围。
   AMD-V 提供的 Device Exclusion Vector 扩展能提供地址空间隔离和访问控制, DEV 通过一个查询表来为每一个 PCI 设备指定一个保护域,并且为每一个保护域提供了一个访问控制位图。 IO 设备执行内存访问时,DEV 机制根据其 HyperTransport 链路及 PCI Id 确定其保护域,然后以该设备访问的地址为索引检测保护域相关的控制位图的相应位,以确定设备能否成功访问相应的物理页。DEV 每个保护域的访问控制位图是存放在系统物理内存中的。
   IOMMU 的功能是在 GART 和 DEV 结合的基础上形成的, 其主要包括如下功能组件 :
  • 设备表。 即一个按设备 ID 索引的数组,类似于 DEV 的查询表。 每个表项为其设备 ID 指定一个 IO 页表根指针及中断转换表的物理地址。 不同的设备 ID 可通过其设备表项中的指针共享 IO 页表或中断转换表。系统的全部 IOMMU 可共享一个设备表。 ( 在 AMD 平台上一般一个 NUMA Node 或北桥控制器会带一个独立的 IOMMU)
  • IO 页表。 不同于 DEV 的位图,IO 页表是层次性的。 和 CPU MMU 使用的页表完全兼容。 在 Nested Paging 情况下,对于某个被分配到虚拟机的物理设备,其 IO 页表完全可以共享其虚拟机的第二维页表。  IO 页表能翻译的设备地址的范围完全取决于页表目录本身的有效 entries 覆盖的范围,不受类似于 GART Aperture 空间的限制。
  • 中断转换表。 将一个中断向量号转换成另一个向量号,并确定该中断的目标 APIC 控制器。 利用中断转换表,VMM 能控制被独占设备产生的中断。
  • 命令缓冲区。用于向 IOMMU 发送命令的环型队列结构, 每个 IOMMU 单独有一个。 发送的命令一般是操作设备表或 IO 页表项的命令。
  • 事件日志缓冲区。用于 IOMMU 记录事件的环形队列结构, 每个 IOMMU 单独有一个。
这些数据结构都是分配在系统物理内存中的,其物理地址由 IOMMU 的控制寄存器所指向。 另外在 IOMMU 内还会有地址翻译的缓存,即 IOTLB, 其运行机制和 CPU MMU 的 TLB 一致。
  在 Linux 上支持 AMD IOMMU 的代码是 arch/x86/kernel/ 目录下的 amd_iommu.c 和 amd_iommu_init.c 两个文件。 amd_iommu_init.c 中主要是实现 IOMMU 检测和控制寄存器操作的代码。 IOMMU 的检测是通过分析 ACPI IVRS 表进行的。 文件 amd_iommu.c 中对 AMD IOMMU 进行了抽象并为 IOMMU 通用层实现了一组操作接口。一个数据结构 struct protect_domain 用来抽象一个保护域,其中有个字段是该保护域的 IO 页表的顶层目录的物理地址,所有关联到这个域的设备都只能使用该页表。设备到域的关联当然是通过 dev_table 实现的。 除硬件需要的 dev_table 外,软件还使用了 rlookup_table 和 pd_table。rlookup_table 是用来确定一个具体的 PCI 设备是物理上连接到哪个 IOMMU 上的,这在采用多个 IOMMU 的结构中当然是有必要的。 pd_table 是用来确定一个设备对应的 protect_domain 数据结构。 这三个表都是以设备的 ID 为索引的。目前在 RHEL6.2 的实现中,似乎还没有对 IOMMU 的中断转换功能的支持。 在 amd_iommu.c 中实现的操作接口主要包括几个函数 : amd_iommu_domain_init, amd_iommu_domain_destroy, amd_iommu_attach_device, amd_iommu_detach_device, amd_iommu_map_range, amd_iommu_unmap_range,  amd_iommu_iova_to_phys 及 amd_iommu_domain_has_cap。 读者不难通过函数名和代码看出这几个函数的功能。
Linux 内核 IOMMU 通用层定义的 API 在 driver/base/iommu.c 中, 有八个函数 : iommu_domain_alloc, iommu_domain_free,  iommu_attach_device,  iommu_detach_device, iommu_map_range,  iommu_unmap_range,  iommu_iova_to_phys 及 iommu_domain_has_cap, 分别调用 AMD IOMMU 层的功能实现函数。
  KMM 支持 PCI-Passthrough 的代码在 virt/kvm/iommu.c 中,这里可通过其中的代码简单描述 PCI-Passthrough 的实现过程 :
  • Qemu-kvm 会根据管理软件或命令的指定执行 “Assign Device”的操作。为执行该操作,Qemu-kvm 在创建完一个 VM 的数据结构后,通过 ioctl() 向 KVM 内核发起 “Assign Device”的操作,执行 virt/kvm/kvm_main.c 中的 kvm_vm_ioctl_assign_device 函数。
  • kvm_vm_ioctl_assign_device 调用 virt/kvm/iommu.c 中的 kvm_iommu_map_guest, 该函数首先会调用 iommu_domain_alloc 分配一个 domain( 对于一个虚拟机来说,所有分配给它的设备共享一个域 ), 然后该函数调用 virt/kvm/iommu.c 中的 kvm_iommu_map_memslots()。
  • kvm_iommu_map_memslots 会遍历该虚拟机的全部物理内存区域对应的 mem_slots, 为每一个 mem_slot, 调用 virt/kvm/iommu.c 中的 kvm_iommu_map_pages 函数。对于每个 mem_slot,我们知道其代表一个连续的 gpa 区域, 所以 kvm_iommu_map_pages 能遍历该区域的所有页面,调用 iommu_iova_to_phys 获取其物理页面号,然后调用 iommu_map_range() 建立该 guest 页在 IO 页表中的映射。
  • kvm_vm_ioctl_assign_device 中在调用 kvm_iommu_map_guest 为该虚拟机建立保护域和 IO 页表后,立即调用 virt/kvm/iommu.c 中的 kvm_assign_device, 该函数会调用 iommu_attach_device 将被 assigned 的 PCI 设备加入到该 guest 的域中。
  根据上面的代码路径,我们可以理解,被 Assigned 的设备,其 DMA 访问的空间,会被完全控制在属主的客操作系统的物理内存页范围之内。“Deassign Device” 的执行过程类似,在内核中的路径由 kvm_vm_ioctl_deassign_device 开始,具体这里就不用描述了。
AMD-V 支持的增强特征AMD-V 提供不少增强的特征为 VMM 的实现提供优化,KVM 利用这些特征能提高系统的性能或解决特定环境的客操作系统的问题。
VMCB 状态缓存及 Clean 控制位比较新的 AMD-V 处理器中存在 VMCB 状态缓存硬件, 用来在 VMEXIT 至 VMRUN 操作之间缓存虚拟机的寄存器的状态。 VMRUN 在加载虚拟机的寄存器状态时,可以选择从 VMCB 中加载,或者从状态缓存中加载, 当然从状态缓存中加载寄存器要快很多。VMCB.CONTROL 的 “Clean Bits” 字段,可被 VMM 用来控制寄存器的状态加载方式,当相关的 Clean Bit 为 1 时,从状态缓存加载该寄存器的状态,否则从 VMCB 加载该寄存器的状态。一般来说 VMM 软件在虚拟机 VMEXIT 到 VMRUN 之间如果修改了 VMCB 中某字段的值,则需要将 VMCB.CONTROL.CLEAN_BITS 字段相关位清除,让 VMRUN 直接从 VMCB 加载该寄存器。并不是所有的寄存器在 VMCB 状态缓存中都有位置的,具体参考 AMD 的系统编程手册。另外,一个 CPU 的硬件中可存在多个虚拟机的 VMCB 状态缓存,VMRUN 可根据 VMCB 的物理地址来识别特定的状态缓存。目前 RHEL6.2 的 KVM 实现已很好的利用了 VMCB.CONTROL 的 “Clean Bits” 字段做 VMRUN 状态加载的优化。
ASID即 Address Space ID, 是较新的 AMD-V 处理器支持的特征。 ASID 就是在 TLB 的 entries 中增加一个 ASID 字段用于区分不同地址空间上下文的 entries, 以便多个地址空间的 TLB entries 可以共存在一个 TLB 中,减少地址空间切换时不必要的 TLB Flush 操作。 为支持对 ASID 特征的使用,AMD-V 在 VMCS.CONTROL 中增加了两个字段 G_ASID 和 TLB_CTRL。 G_ASID 用于指定 VMRUN 所运行的虚拟机的 ASID。 TLB_CTRL 用于控制 VMRUN 在重新加载虚拟机状态时怎样 Flush TLB。 TLB_CTRL 可有 000,001,011,111 四个值,其中 000 表示不做 TLB Flush, 001 表示刷全部的 TLB Entries, 011 表示 Flush 本 ASID 的全部 TLB Entries, 111 表示 Flush 本 ASID 的非全局的 TLB Entries。 目前 RHEL6.2 的 KVM 代码已经利用 ASID 和 VMCB.TLB_CTRL 做了 TLB Flush 方面的优化。  另外 AMD-V 中还有一个和 ASID 直接相关的指令 invlpga, 执行该指令只 Flush 指定线性地址和 ASID 对应的 TLB entries。 Invlpga 能为 VMM 对 TLB 的管理提供更多的优化空间,如主机上 Linux 的物理页回收代码在释放掉一个物理页时,可根据该页所属的 ASID, 用 Invlpga 而非 Invlpg 来做 TLB Flush, 目前的 KVM 还没有有效的利用 Invlpga。
Pause Filter DetectPAUSE 指令在 Linux 内核等代码中广泛被用于自旋锁的 SPIN 循环中,用于标识一个 CPU 的代码处于自旋锁等待状态。 在虚拟化环境中存在一个问题是,我们并不希望一个虚拟机的 CPU 太长时间处于自旋锁等待状态,因为在主机上有其他的虚拟机或计算任务可以利用该 CPU 的计算资源。 解决该问题的一个比较好的思想是计算某虚拟 CPU 连续调用 PAUSE 指令的次数,当超过了规定的值,就截取该虚拟 CPU,调度其他 VMM 上的进程,或想法加快持有自旋锁的 VCPU 线程的调度执行。 为了实现这一思想,较近的 AMD-V 处理器的 VMCB.CONTROL 中增加了一 PAUSE_FILTER_COUNT 字段及一个 INTERCEPT_PAUSE 控制位,用于设定对 PAUSE 的截取,即当 INTERCEPT_PAUSE 为 1 时,用 PAUSE_FILTER_COUNT 初始化 CPU 内某寄存器,每次 PAUSE 执行时,该寄存器的值减 1,当减到 0 时产生截取。 PAUSE_FILTER_COUNT 完全按 PAUSE 的次数控制是否截取 PAUSE 指令,没有考虑不同 PAUSE 指令之间的执行间隔,如俩个 PAUSE 指令属于不同代码或 PAUSE 指令间有中断的情况,这些情况不应该被误判为自旋锁 SPIN 循环的存在。为弥补 PAUSE_FILTER_COUNT 机制的不足,更新的 AMD-V 处理器在 VMCB.CONTROL 中增加了一个 PAUSE_FILTER_THRESHOLD 字段, 新的字段用来规定一个 CPU Cycles 的长度,当最近一次 PAUSE 过去的 CPU Cycles 超过了该字段规定的值时,PAUSE_FILTER_COUNT 对应的 CPU 内部计数器会重新设置为 PAUSE_FILTER_COUNT;如果过去的 CPU Cycles 没超过 PAUSE_FILTER_THRESHOLD 字段规定的值,则内部计数器按正常的方式递减,直到其值为 0,产生截取。 目前 RHEL6.2 上 KVM 的实现已经支持 PAUSE_FILTER_COUNT,但尚不支持 PAUSE_FILTER_THRESHOLD。
返回列表