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;
#endif
dma_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);
else
ret = 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