设备穿透之IOMMU分组

10 篇文章 2 订阅
1 篇文章 0 订阅

深入了解iommu系列二:iommu 工作原理解析之dma remapping - 知乎

Bazinga

一、 IOMMU所处的位置

IOMMU驱动所处的位置位于DMA驱动之上,其上又封装了一层VFIO驱动框架,便于用户空间编写设备驱动直接调用底层的api操作设备。

内核在启动的时候就会初始化好设备的信息,给设备分配相应的iommu group,其过程如下。

二、第一步IOMMU驱动初始化

系统启动的时候会执行iommu_init函数进行内核对象iommu的初始化。主要是调用了kset_create_and_add函数创建了iommu_groups对象。在/sys/kernel目录下会出现iommu_groups目录。
可以使用内核调试方法,在iommu_init函数处打断点,查看系统对iommu初始化的过程。

注意需要内核打开相应的配置开关才能使得IOMMU功能生效。

三、第二步是完成设备分组

这里主要分析arm下的情况,在iommu驱动加载的时候都会调用接口arm_smmu_device_probe。
2.1 在armV3下该函数会首先分配一个struct arm_smmu_device *smmu;对象。然后将smmu->dev指向入参struct platform_device对象保存的dev对象。

smmu = devm_kzalloc(dev, sizeof(*smmu), GFP_KERNEL);
if (!smmu) {
    dev_err(dev, "failed to allocate arm_smmu_device\n");
    return -ENOMEM;
}
smmu->dev = dev;

2.2 调用arm_smmu_device_hw_probe给smmu对象赋值其具有的硬件特性。
2.3 调用iommu_device_sysfs_add初始化iommu对象中的dev对象。并给iommu对象ops赋值为arm_smmu_ops,fwnode赋值为dev->fwnode。

/* And we're up. Go go go! */
ret = iommu_device_sysfs_add(&smmu->iommu, dev, NULL, "smmu3.%pa", &ioaddr);
if (ret)
    return ret;

iommu_device_set_ops(&smmu->iommu, &arm_smmu_ops);
iommu_device_set_fwnode(&smmu->iommu, dev->fwnode);

2.4 注册iommu设备。即将全局变量iommu_device_list添加到链表尾部。

static LIST_HEAD(iommu_device_list);
static DEFINE_SPINLOCK(iommu_device_lock)

int iommu_device_register(struct iommu_device *iommu)
{
    spin_lock(&iommu_device_lock);
    list_add_tail(&iommu->list, &iommu_device_list);
    spin_unlock(&iommu_device_lock);

    return 0;
}

2.5 最后调用bus_set_iommu设置总线iommu的回调操作函数以及为该总线类型的iommu做一些特别的设定。这里传入的iommu_ops值为arm_smmu_ops。

int bus_set_iommu(struct bus_type *bus, const struct iommu_ops *ops)
{
	int err;

	if (bus->iommu_ops != NULL)
		return -EBUSY;

	bus->iommu_ops = ops;

	/* Do IOMMU specific setup for this bus-type */
	err = iommu_bus_init(bus, ops);
	if (err)
		bus->iommu_ops = NULL;

	return err;
}

上面几步的数据结构关系如下:

2.6 iommu_bus_init中会遍历总线下的设备,给每个设备调用回调函数add_iommu_group,将设备添加到iommu group中。

err = bus_for_each_dev(bus, NULL, &cb, add_iommu_group);

2.7 add_iommu_group只是简单调用了iommu_probe_device。iommu_probe_device是一个封装的接口函数,它可以适配不同的体系结构。如在intel下这里的add_device调动的就是intel_iommu_add_device,在arm下调用的就是arm_smmu_add_device。

ret = ops->add_device(dev);

2.8 然后调用iommu_group_get_for_dev给设备添加iommu group,不同体系结构最后实际都是调的该函数接口。

/**
 * iommu_group_get_for_dev - Find or create the IOMMU group for a device
 * @dev: target device
 *
 * This function is intended to be called by IOMMU drivers and extended to
 * support common, bus-defined algorithms when determining or creating the
 * IOMMU group for a device.  On success, the caller will hold a reference
 * to the returned IOMMU group, which will already include the provided
 * device.  The reference should be released with iommu_group_put().
 */
struct iommu_group *iommu_group_get_for_dev(struct device *dev)
{
	const struct iommu_ops *ops = dev->bus->iommu_ops;
	struct iommu_group *group;
	int ret;

	group = iommu_group_get(dev);
	if (group)
		return group;

	if (!ops)
		return ERR_PTR(-EINVAL);

	group = ops->device_group(dev);
	if (WARN_ON_ONCE(group == NULL))
		return ERR_PTR(-EINVAL);

	if (IS_ERR(group))
		return group;

	/*
	 * Try to allocate a default domain - needs support from the
	 * IOMMU driver.
	 */
	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) {
			dev_warn(dev,
				 "failed to allocate default IOMMU domain of type %u; falling back to IOMMU_DOMAIN_DMA",
				 iommu_def_domain_type);
			dom = __iommu_domain_alloc(dev->bus, IOMMU_DOMAIN_DMA);
		}

		group->default_domain = dom;
		if (!group->domain)
			group->domain = dom;
	}

	ret = iommu_group_add_device(group, dev);
	if (ret) {
		iommu_group_put(group);
		return ERR_PTR(ret);
	}

	return group;
}

该函数首先判断设备是否已经有iommu group,有的话直接返回。然后调用device_group分配一个group对象。arm下调用的就是arm_smmu_device_group,然后根据设备是否是PCI,分别调用generic_device_group或pci_device_group。最终调用的是iommu_group_alloc分配内存空间。
然后给刚创建的iommu_group对象分配domain空间,调用__iommu_domain_alloc。该函数会将domain结构中包含的的ops赋值为bus->iommu_ops;

最后重点是调用iommu_group_add_device将设备添加到iommu_group中去。 该函数首先创建一个struct group_device对象。

struct group_device *device;

device = kzalloc(sizeof(*device), GFP_KERNEL);
if (!device)
	return -ENOMEM;

然后将设备添加到组中,并创建软链接,将/sys/kernel/iommu_groups/xx/devices/设备PCI号,链接到/sys/devices/pci总线ID/设备号上。

device->dev = dev;

ret = sysfs_create_link(&dev->kobj, &group->kobj, "iommu_group");
if (ret)
		goto err_free_device;

同时设备结构中包含的iommu_group对象会指向其所在的组。

dev->iommu_group = group;

然后会给设备做DMA映射,iommu_group_create_direct_mapping主要是将设备对应的虚拟机地址空间段映射到物理地址空间。其会遍历设备映射的地址段,然后调用iommu_map给每段虚拟地址空间都映射到相应的物理内存页上。

iommu_group_create_direct_mappings(group, dev);

iommu_get_resv_regions(dev, &mappings);

/* We need to consider overlapping regions for different devices */
list_for_each_entry(entry, &mappings, list) {
		dma_addr_t start, end, addr;

		if (domain->ops->apply_resv_region)
			domain->ops->apply_resv_region(dev, domain, entry);

		start = ALIGN(entry->start, pg_size);
		end   = ALIGN(entry->start + entry->length, pg_size);

		if (entry->type != IOMMU_RESV_DIRECT)
			continue;

		for (addr = start; addr < end; addr += pg_size) {
			phys_addr_t phys_addr;

			phys_addr = iommu_iova_to_phys(domain, addr);
			if (phys_addr)
				continue;

			ret = iommu_map(domain, addr, addr, pg_size, entry->prot);
			if (ret)
				goto out;
	}

iommu_map会调用domain->ops->map即调用的struct iommu_ops arm_smmu_ops中定义的.map函数,即arm_smmu_map。arm_smmu_map会调用pgtbl_ops->map。pgtbl_ops是在函数arm_smmu_domain_finalise中调用函数alloc_io_pgtable_ops分配的对象。最终调用的map函数为arm_lpae_map。

static int arm_smmu_map(struct iommu_domain *domain, unsigned long iova,
			phys_addr_t paddr, size_t size, int prot)
{
	struct io_pgtable_ops *ops = to_smmu_domain(domain)->pgtbl_ops;

	if (!ops)
		return -ENODEV;

	return ops->map(ops, iova, paddr, size, prot);
}

然后会执行以下一连串的调用,最终调用到底层的dma 驱动api。
arm_smmu_domain_finalise -> alloc_io_pgtable_ops -> io_pgtable_init_table -> io_pgtable_arm_64_lpae_s1_init_fns -> arm_64_lpae_alloc_pgtable_s1 -> __arm_lpae_alloc_pages -> dma_map_single.
dma_map_single函数的作用就是接收一个虚拟地址,然后建立起需要做的IOMMU映射,然后返回设备访问的总线地址Z。然后通知设备对Z地址做DMA映射,得到物理地址。
arm_lpae_map -> __arm_lpae_map -> arm_lpae_install_table -> __arm_lpae_sync_pte -> dma_sync_single_for_device。
dma_sync_single_for_device的作用是在让设备能够再次访问DMA区域前调用,将之前CPU访问过的状态进行同步。

四、IOMMU的作用和效果

iommu实现了类似MMU的功能,主要是做地址翻译。其主要通过底层的DMA api来实现设备地址空间的映射。其最终效果类似于下图:

这里存在三种地址,虚拟地址空间,物理地址空间,总线地址空间。从CPU的视角看到的是虚拟机地址空间,如我们调用kmalloc、vmalloc分配的都是虚拟地址空间。然后通过MMU转换成ram或寄存器中的物理地址。如C->B的过程。物理地址可以在/proc/iomem中查看。
IO设备通常使用的是总线地址,在一些系统上,总线地址就等于物理地址。但是通常有iommu和南桥的设备则会做总线地址到物理地址之间的映射。
如上图A->C未进行DMA的过程,内核首先扫到IO设备和他们的MMIO空间以及将设备挂到系统上的host bridge信息。假如设备有一个BAR,内核从BAR中读取总线地址A,然后转换成物理地址B。物理地址B可以在/proc/iomem中查看到。当驱动挂载到设备上时,调用ioremap( )将物理地址B转换成虚拟地址C。之后就可以通过读写C的地址来访问到最终的设备寄存器A的地址。
如上图X->Y,Z->Y使用DMA的过程,驱动首先使用kmalloc或类似接口分配一块缓存空间即地址X,内存管理系统会将X映射到ram中的物理地址Y。设备本身不能使用类似的方式映射到同一块物理地址空间,因为DMA没有使用MMU内存管理系统。在一些系统上,设备可以直接DMA到物理地址Y,但是通常有IOMMU硬件的系统会DMA设备到地址Z,然后通过IOMMU转换到物理地址Y。这是因为调用DMA API的时候,驱动将虚拟地址X传给类似dma_map_single( )的接口后,会建立起必须的IOMMU映射关系,然后返回DMA需要映射的地址Z。驱动然后通知设备做DMA映射到地址Z,最后IOMMU映射地址Z到ram中的缓存地址Y。
从上面的两个例子中客户看出,IOMMU硬件的存在会加速地址的转换速度,从X->Y是通过CPU完成的,而Z->Y则是通过IOMMU硬件完成。同时能够做到安全,IOMMU会校验Y->Z地址的合法性,避免了直接DMA到Y可能带来的安全问题。

参考:

[1] https://www.kernel.org/doc/Document

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值