Linux虚拟化之IOMMU

  • 引言
  • 1 IOMMU初始化
    • 1.1 初始化函数集
    • 1.2 初始化流程
  • 2 Intel-IOMMU
    • 2.1 技术原理
    • 2.2 数据结构
    • 2.3 核心流程
      • 2.3.1 intel_iommu_init
      • 2.3.2 iommu_group_get_for_dev
      • 2.3.3 intel_iommu_add_device
      • 2.3.4 iommu_domain_alloc
      • 2.3.5 iommu_attach_device
      • 2.3.6 iommu_map

引言

DMA操作允许设备直接访问内存,但是也带来了诸多问题:

  • 当设备需要大量连续物理内存时,OS未必能满足;
  • 当某些设备不能访问高端内存时,必须采用反弹缓冲区搬移,影响性能;
  • 在虚拟化环境中,存在多设备访问同一物理内存的冲突;

IOMMU与cpu的MMU类似,给设备提供一套虚拟地址空间,设备发出虚拟总线地址空间的访问请求、送到IOMMU单元翻译成物理地址的方式间接访问物理内存。

1 IOMMU初始化

1.1 初始化函数集

以x86环境为例,在文件arch/x86/include/asm/iommu_table.h文件中定义了每种IOMMU的初始化函数集结构体:

struct iommu_table_entry {initcall_t detect; //IOMMU探测函数,返回非0表示该类IOMMU开启,返回0表示关闭。initcall_t    depend; //另一类IOMMU的detect函数,用于多种IOMMU的初始化排序。void     (*early_init)(void); /* No memory allocate available. */void        (*late_init)(void); /* Yes, can allocate memory. */
//当flags的IOMMU_FINISH_IF_DETECTED置位,detect函数返回1后不再扫描后续的IOMMU
#define IOMMU_FINISH_IF_DETECTED (1<<0)
//当flags的IOMMU_DETECTED置位,表示该类IOMMU已经成功探测到开启
#define IOMMU_DETECTED       (1<<1)int        flags;
};

此外,在该文件中还定义了生成iommu_table_entry全局变量的宏:

//将全局变量编译到".iommu_table"段中
#define __IOMMU_INIT(_detect, _depend, _early_init, _late_init, _finish)\static const struct iommu_table_entry              \__iommu_entry_##_detect __used             \__attribute__ ((unused, __section__(".iommu_table"),     \aligned((sizeof(void *)))))    \= {_detect, _depend, _early_init, _late_init,         \_finish ? IOMMU_FINISH_IF_DETECTED : 0}

其他的宏IOMMU_INIT_POST/IOMMU_INIT_POST_FINISH,和IOMMU_INIT_FINISH/IOMMU_INIT都是基于__IOMMU_INIT的封装。

1.2 初始化流程

内核加载后,通过start_kernel->mm_init->mem_init->pci_iommu_alloc,执行IOMMU初始化。

pci_iommu_alloc函数定义在arch/x86/kernel/pci-dma.c文件中:

void __init pci_iommu_alloc(void)
{struct iommu_table_entry *p;//首先,对所有类型的IOMMU函数集合进行排序sort_iommu_table(__iommu_table, __iommu_table_end);//发现depend等于自己的detect,将其置NULL;如果发现排序后的某个IOMMU在其被依赖的IOMMU前面,报错!!!check_iommu_entries(__iommu_table, __iommu_table_end);for (p = __iommu_table; p < __iommu_table_end; p++) {if (p && p->detect && p->detect() > 0) {p->flags |= IOMMU_DETECTED;if (p->early_init)p->early_init();if (p->flags & IOMMU_FINISH_IF_DETECTED)break;}}
}

最后,在pci_iommu_init函数中调用late_init完成最后的IOMMU初始化操作。该函数也在arch/x86/kernel/pci-dma.c文件中

static int __init pci_iommu_init(void)
{struct iommu_table_entry *p;x86_init.iommu.iommu_init();for (p = __iommu_table; p < __iommu_table_end; p++) {if (p && (p->flags & IOMMU_DETECTED) && p->late_init)p->late_init();}return 0;
}
/* Must execute after PCI subsystem */
rootfs_initcall(pci_iommu_init);

2 Intel-IOMMU

相关概念如下:

  • DRHD:DMA Remapping Hardware Unit Definition,用于描述DMA Remapping Unit的信息;

2.1 技术原理

下图是x86物理服务器视图:

如上图,在主桥中有多个DMA Remapping Unit。每个单元管理相关设备的DMA请求,负责将它们的设备虚拟地址转为设备物理地址。图中,DMA Remapping Unit #1管理PCIe Root Port Dev[14:0]及其下属设备; 依次类推,DMA Remapping Unit #4管理PCIe Root Port Dev[30:0]及其下属设备和南桥设备。

注:BIOS通过在ACPI表中的DMA Remapping Reporting Structure 信息来描述这些管理信息。

2.2 数据结构

linux中关于intel IOMMU的相关数据结构如下:

  • linux通过全局变量dmar_drhd_units将所有的struct dmar_drhd_unit串成一个链表,该结构对应于上面的DMA Remapping Unit。每个struct dmar_drhd_unit指向一个struct intel_iommu。;
  • 每个struct intel_iommu包含一个root entry,用于建立设备与域的映射关系。通过总线号、设备号、功能号索引IOMMU映射的页表基地址.
  • 每个struct intel_iommu包含多个domain,一般一个虚拟机对应一个域。一个域内共享相同的虚拟IO地址空间;
  • 每个struct intel_iommu也嵌套一个struct iommu_device,一个DMA Remapping Unit也是一个硬件设备,从linux设备模型看属于一个特定功能的设备,其中还包括一个iommu_ops的函数集合,该函数集合用于域的构建、释放、域内设备管理等功能;
  • 每个struct dmar_domain嵌套一个struct iommu_domain,其中也包括一个iommu_ops的函数集合;
  • 每个struct dmar_domain还包括一个IOMMU映射的页表基地址;
  • 一个struct iommu_group代表设备透传的最小单元,其中的设备不能在不同的域,也就是不能透传给不同的guest,也不能一部分在host、一部分在guest。所以,每个struct iommu_group属于一个域;

2.3 核心流程

2.3.1 intel_iommu_init

通过IOMMU_INIT_POST(detect_intel_iommu)定义了用于intel-iommu的detect,同时该函数会在pci_swiotlb_detect_4gb后执行。在detect_intel_iommu函数中对全局回调x86_init.iommu.iommu_init赋值为intel_iommu_init,最后会在pci_iommu_init函数中调用该回调(即intel_iommu_init函数)。

int __init intel_iommu_init(void)
{# 略略略... ...if (iommu_init_mempool()) {if (force_on)panic("tboot: Failed to initialize iommu memory\n");return -ENOMEM;}down_write(&dmar_global_lock);if (dmar_table_init()) {if (force_on)panic("tboot: Failed to initialize DMAR table\n");goto out_free_dmar;}if (dmar_dev_scope_init() < 0) {if (force_on)panic("tboot: Failed to initialize DMAR device scope\n");goto out_free_dmar;}up_write(&dmar_global_lock);dmar_register_bus_notifier();down_write(&dmar_global_lock);# 略略略... ...if (dmar_init_reserved_ranges()) {if (force_on)panic("tboot: Failed to reserve iommu ranges\n");goto out_free_reserved_range;}if (dmar_map_gfx)intel_iommu_gfx_mapped = 1;init_no_remapping_devices();ret = init_dmars();if (ret) {if (force_on)panic("tboot: Failed to initialize DMARs\n");pr_err("Initialization failed\n");goto out_free_reserved_range;}up_write(&dmar_global_lock);#if defined(CONFIG_X86) && defined(CONFIG_SWIOTLB)if (!has_untrusted_dev() || intel_no_bounce)swiotlb = 0;
#endifdma_ops = &intel_dma_ops;init_iommu_pm_ops();down_read(&dmar_global_lock);for_each_active_iommu(iommu, drhd) {iommu_device_sysfs_add(&iommu->iommu, NULL,intel_iommu_groups,"%s", iommu->name);iommu_device_set_ops(&iommu->iommu, &intel_iommu_ops);iommu_device_register(&iommu->iommu);}up_read(&dmar_global_lock);bus_set_iommu(&pci_bus_type, &intel_iommu_ops);if (si_domain && !hw_pass_through)register_memory_notifier(&intel_iommu_memory_nb);cpuhp_setup_state(CPUHP_IOMMU_INTEL_DEAD, "iommu/intel:dead", NULL,intel_iommu_cpu_dead);down_read(&dmar_global_lock);if (probe_acpi_namespace_devices())pr_warn("ACPI name space devices didn't probe correctly\n");/* Finally, we enable the DMA remapping hardware. */for_each_iommu(iommu, drhd) {if (!drhd->ignored && !translation_pre_enabled(iommu))iommu_enable_translation(iommu);iommu_disable_protect_mem_regions(iommu);}up_read(&dmar_global_lock);pr_info("Intel(R) Virtualization Technology for Directed I/O\n");intel_iommu_enabled = 1;return 0;out_free_reserved_range:# 略略略... ...
out_free_dmar:# 略略略... ...
}

核心流程如下:

  • 首先,通过iommu_init_mempool函数创建slob缓存池;
  • 然后,通过dmar_table_init函数解析ACPI表中与DMA Remapping Unit相关的信息;
  • 继续,通过dmar_dev_scope_init将解析每个PCI设备的bus链,通过bus链将设备添加到struct dmar_drhd_unit中;
  • 继续,通过dmar_init_reserved_ranges函数保留IOAPIC和每个PCI设备的bar空间,这些区域不能用于内存分配;
  • 继续,通过init_no_remapping_devices函数忽略掉相关的struct dmar_drhd_unit,具体是:若struct dmar_drhd_unit下挂设备只有gfx设备,且dmar_map_gfx为0(因为很多图形设备将内存集成在显卡内部);或者struct dmar_drhd_unit下没有挂设备;
  • 通过init_dmars对每个struct dmar_drhd_unit做正式初始化,包括设置root entry;
  • 继续,设置全局回调函数集dma_ops为intel_dma_ops,该函数集用于dma_alloc_*系列函数;
  • 继续,设置每个struct iommu_device的ops为intel_iommu_ops;
  • 继续,设置pci_bus_type的iommu_ops为intel_iommu_ops;
  • 继续,通过probe_acpi_namespace_devices将acpi namespace设备建立iommu_group;
  • 最后,使能每个struct dmar_drhd_unit,并将intel_iommu_enabled置1;

2.3.2 iommu_group_get_for_dev

iommu_group_get_for_dev函数负责获取iommu_group,若不存在就创建新的iommu_group并将设备添加到iommu_group之中:

struct iommu_group *iommu_group_get_for_dev(struct device *dev)
{const struct iommu_ops *ops = dev->bus->iommu_ops;# 略略略... ...group = iommu_group_get(dev);if (group)return group;# 略略略... ...group = ops->device_group(dev);# 略略略... ...if (!group->default_domain) {struct iommu_domain *dom;dom = __iommu_domain_alloc(dev->bus, iommu_def_domain_type);if (!dom && iommu_def_domain_type != IOMMU_DOMAIN_DMA) {dom = __iommu_domain_alloc(dev->bus, IOMMU_DOMAIN_DMA);# 略略略... ...}group->default_domain = dom;if (!group->domain)group->domain = dom;if (dom && !iommu_dma_strict) {int attr = 1;iommu_domain_set_attr(dom,DOMAIN_ATTR_DMA_USE_FLUSH_QUEUE,&attr);}}ret = iommu_group_add_device(group, dev);if (ret) {iommu_group_put(group);return ERR_PTR(ret);}return group;
}

函数中通过ops->device_group创建iommu_group,并创建默认的struct iommu_domain赋值给iommu_group的default_domain和domain字段。当iommu_group透传到虚拟机的时候,其domain会指向虚拟机的域。
在intel-iommu实现中,ops->device_group回调的实现函数为intel_iommu_device_group。在该函数中,对于PCI设备会执行pci_device_group;反之,则执行generic_device_group。

pci_device_group函数如下:

struct iommu_group *pci_device_group(struct device *dev)
{# 略略略... ...u64 devfns[4] = { 0 };# 略略略... ...if (pci_for_each_dma_alias(pdev, get_pci_alias_or_group, &data))return data.group;pdev = data.pdev;for (bus = pdev->bus; !pci_is_root_bus(bus); bus = bus->parent) {if (!bus->self)continue;if (pci_acs_path_enabled(bus->self, NULL, REQ_ACS_FLAGS))break;pdev = bus->self;group = iommu_group_get(&pdev->dev);if (group)return group;}group = get_pci_alias_group(pdev, (unsigned long *)devfns);if (group)return group;group = get_pci_function_alias_group(pdev, (unsigned long *)devfns);if (group)return group;/* No shared group found, allocate new */return iommu_group_alloc();
}

执行流程如下:

  • 首先,通过pci_for_each_dma_alias函数迭代,若成功迭代到group,则返回;否则,将pdev更新到最后迭代的pdev,继续往后执行;
  • 通过for循环,从pdev开始往上遍历总线。每次,若bus开启了ACS特性(access controller service,该特性会关闭PCIE的点对点传输特性,确保下属设备的访问直通到DMA Remapping Unit),退出循环;否则,总线绑定了iommu_group,就将其return;
  • 然后,通过get_pci_alias_group函数先查询pdev是否绑定iommu_group,若绑定就将其返回;否则,查询同一个bus上,被pdev设置为dma alias或将pdev设置为dma alias的直接关联设备以及间接关联设备(间接关联只两个设备和某其他相同设备有直接关联或间接关联)。若查询成功,将其group返回,如果group没有,遍历结束返回NULL;
  • 继续在get_pci_function_alias_group中,查询和pdev在同一总线上,有相同设备号、不同功能号的设备,如果pdev和找到的设备都没有开启ACS特性,则将找到设备的group返回(前提是group存在,不存在继续查询,直到返回NULL)

pci_for_each_dma_alias迭代规则如下:

int pci_for_each_dma_alias(struct pci_dev *pdev,int (*fn)(struct pci_dev *pdev,u16 alias, void *data), void *data)
{# 略略略... ...ret = fn(pdev, pci_dev_id(pdev), data);if (ret)return ret;if (unlikely(pdev->dma_alias_mask)) {unsigned int devfn;for_each_set_bit(devfn, pdev->dma_alias_mask, MAX_NR_DEVFNS) {ret = fn(pdev, PCI_DEVID(pdev->bus->number, devfn),data);if (ret)return ret;}}for (bus = pdev->bus; !pci_is_root_bus(bus); bus = bus->parent) {struct pci_dev *tmp;/* Skip virtual buses */if (!bus->self)continue;tmp = bus->self;/* stop at bridge where translation unit is associated */if (tmp->dev_flags & PCI_DEV_FLAGS_BRIDGE_XLATE_ROOT)return ret;if (pci_is_pcie(tmp)) {switch (pci_pcie_type(tmp)) {case PCI_EXP_TYPE_ROOT_PORT:case PCI_EXP_TYPE_UPSTREAM:case PCI_EXP_TYPE_DOWNSTREAM:continue;case PCI_EXP_TYPE_PCI_BRIDGE:ret = fn(tmp,PCI_DEVID(tmp->subordinate->number,PCI_DEVFN(0, 0)), data);if (ret)return ret;continue;case PCI_EXP_TYPE_PCIE_BRIDGE:ret = fn(tmp, pci_dev_id(tmp), data);if (ret)return ret;continue;}} else {if (tmp->dev_flags & PCI_DEV_FLAG_PCIE_BRIDGE_ALIAS)ret = fn(tmp,PCI_DEVID(tmp->subordinate->number,PCI_DEVFN(0, 0)), data);elseret = fn(tmp, pci_dev_id(tmp), data);if (ret)return ret;}}return ret;
}

执行流程如下:

  • 首先,通过回调查询pdev是否绑定了iommu_group,存在就将其返回,否则继续下一步;
  • unlikely的条件可以忽略,因为fn回调get_pci_alias_or_group,其实现与PCI_DEVID(pdev->bus->number, devfn)没有影响;
  • 在for循环的迭代核心思想是:从pdev所属总线往上遍历,如果遇到bus具备PCI_DEV_FLAGS_BRIDGE_XLATE_ROOT特性,迭代结束;否则,若设备是PCIE设备,且是PCIE-PCI桥或PCI-PCIE桥,查询其iommu_group,存在就将其返回。若设备不是PCIE设备,就查询其iommu_group;

注:对于PCIE总线,之所以需要查询PCIE-PCI桥或PCI-PCIE桥,是因为PCI接入到PCIE其下属设备均共享source identifier(使用桥设备的bus、device、func)。

2.3.3 intel_iommu_add_device

2.3.4 iommu_domain_alloc

2.3.5 iommu_attach_device

2.3.6 iommu_map

Linux虚拟化之IOMMU相关推荐

  1. Linux虚拟化KVM-Qemu分析(十一)之virtqueue

    目录 1. 概述 2. 数据结构 3. 流程分析 3.1 发送 3.2 接收 3.3 代码分析 3.3.1 virtqueue创建 3.3.2 virtio-net驱动发送 3.3.3 Qemu vi ...

  2. Linux虚拟化KVM-Qemu分析(十)之virtio驱动

    目录 1. 概述 2. 数据结构 3. 流程分析 3.1 virtio总线创建 3.2 virtio驱动调用流程 参考 <Linux PCI驱动框架分析:(Peripheral Componen ...

  3. Linux虚拟化KVM-Qemu分析(九)之virtio设备

    目录 1. 概述 2. 流程分析 3. tap创建 - 网卡后端设备 4. virtio-net创建 4.1 数据结构 4.2 流程分析 4.2.1 class_init 4.2.2 instance ...

  4. Linux虚拟化KVM-Qemu分析(八)之virtio初探

    目录 概述 1. 网卡 1.1 网卡工作原理 1.2 Linux网卡驱动 2. 网卡全虚拟化 2.1 全虚拟化方案 2.2 弊端 3. 网卡半虚拟化 3.1 virtio 3.2 半虚拟化方案 参考 ...

  5. Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

    <Linux虚拟化KVM-Qemu分析(一)> <Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化> <Linux虚拟化KVM-Qemu分析(三)之KVM源码 ...

  6. Linux虚拟化KVM-Qemu分析(六)之中断虚拟化

    <Linux虚拟化KVM-Qemu分析(一)> <Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化> <Linux虚拟化KVM-Qemu分析(三)之KVM源码 ...

  7. Linux虚拟化KVM-Qemu分析(五)之内存虚拟化

    <Linux虚拟化KVM-Qemu分析(一)> <Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化> <Linux虚拟化KVM-Qemu分析(三)之KVM源码 ...

  8. Linux虚拟化KVM-Qemu分析(四)之CPU虚拟化(2)

    Table of Contents 1. 概述 1.1 CPU工作原理 1.2 guest模式 2. 流程分析 2.1 vcpu的创建 2.1.1 qemu中vcpu创建 2.1.2 kvm中vcpu ...

  9. Linux虚拟化KVM-Qemu分析(三)之KVM源码(1)kvm_init

    Table of Contents 1. 概述 2. KVM初始化 2.1 kvm_arch_init 2.1.1 init_hyp_mode 2.1.2 init_subsystems 2.2 mi ...

  10. Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化

    Table of Contents 1. 概述 2. ARMv8虚拟化 2.1 Exception Level 2.2 Stage 2 translation 2.2.1 内存映射 2.2.2 MMI ...

最新文章

  1. tf.keras.losses.MeanAbsolutePercentageError 损失函数 示例
  2. transparentblt函数实现透明贴图 (上)
  3. python 项目中的requirements.txt文件
  4. 【渝粤题库】广东开放大学 人力资源管理 形成性考核
  5. Ubuntu 添加、删除ppa源
  6. 概率图模型更进一步的知识点
  7. Python程序:求最小公倍数
  8. VB 实现UTF-8 与GB2312互转
  9. Ubuntu下安装opencv3.2.0,解决anaconda下的Python和安装opencv3.2.0冲突的问题
  10. 用于开启php绘图扩展配置为,女儿墙屋面排水列项应选择()。A.雨水管B.雨水斗C.雨水口D.出水弯管E.檐沟...
  11. linux CFI接口,Linux系统下的MTD/CFI驱动介绍
  12. HTML5 弹性布局
  13. 最新瑞芯微四核芯片RK3288开源开发板
  14. 热插拔+远程控制,同为(TOWE)智能桌面PDU产品APZ-1026HR
  15. Web应用程序与桌面应用程序的区别
  16. 迅捷PDF文件转换Excel
  17. 渗透辅助工具-Dude
  18. 12点的idft c语言,IDFT的公式及C语言实现.doc
  19. 【调剂】上海海事大学2022年硕士研究生调剂公告
  20. openstack修改密码

热门文章

  1. 财务模块的一些基础概念
  2. DevExpress ChartControl 绘制圆滑曲线
  3. I2C驱动12864液晶模块程序分享ARDUINO UNO
  4. 有关Altera 的CPLD工程编译报错的解决方法
  5. 基于J2EE的门诊挂号收费系统设计与实现.rar(毕业论文+程序源码+数据库文件)
  6. HeadFirstJava——2_类与对象
  7. windows内核驱动
  8. Linux多线程编程入门
  9. 闪迪内存卡软件测试,闪迪存储卡怎么样
  10. 24. cc.Director与资源加载策略