深入了解iommu系列二:iommu 工作原理解析之dma remapping - 知乎
一、 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可能带来的安全问题。
参考: