本文结合vt-directed-io-spec和内核的实现,对IOMMU的地址映射机制有了基础的认识,在此记录。最开始的原因是之前看ATS时对于first-stage和second-stage的困惑,因为近几年引入了pasid模式,IOMMU的映射机制也看起来复杂了很多,所以对整个映射机制进行了梳理。
一、基础知识
非pasid机制下,传统的IOMMU根据BDF号索引到设备页表的基地址,然后根据设备页表找到HPA,完成一次translation,这称为legacy模式。legacy模式的经典映射流程大家比较熟悉。
Scalable-iov引入了新的模式称为scalable模式,与legacy模式相对应。scalable-iov的目的也是针对虚机和容器场景,在SR-IOV的基础上进一步扩展了可供主机使用的安全隔离实体的数量。SRIOV是在PF的基础上扩展了VF,每个VF还是有独立的BDF号。而scalable-iov直接修改了TLP报文的格式,TLP报文中增加了pasid字段(最多20位),所以同一个BDF号,可以使用不同的pasid字段区分。对于主机而言,一个PCIE设备,可以分出多达2^20个独立的实体给虚机和容器使用。显然,scalable模式依赖设备侧和主机侧RC硬件的同步支持。
scalable模式的映射机制如下图。
legacy模式里根据BDF号的映射还是在的,也就是说首先会根据BDF号索引,只是BDF号索引到的并非是页表基地址,而是scalable mode pasid directory的基地址。
由于最多支持65536个pasid,从节省空间的角度考虑,这个pasid表也做成了两级,第一级称为pasid目录,负责PASID【19:6】的一级映射;第二级称为pasid table,负责PASID【5:0】的二级映射。
PASID Table里的PASID ENTRY指向的就是【BDF+PASID】完整索引到的PASID表项。
看最右侧的页表,看到每个PASID表项同时包含了First-Stage Page Table和Second-Stage Page Table两个页表,这是为了支持虚机场景下nested页表使用的,类似于MMU和EPT两级页表。对于这两级页表的应用场景,在下一章会做详细解释。
本章介绍了IOMMU的legacy mode和scalable mode,其中scalable mode是新一代的intel cpu架构才支持。
二、Scalable Mode Translation
legacy mode一级页表用于GPA/HVA到HPA的转换,这种模式不在详细介绍。本章重点介绍scalable mode的页表结构。
大家可能会疑惑:我用VFIO透传设备用legacy模式的一级页表就够了呀。那是在guest自身没有开启iommu的情形下,所有设备都只能使用GPA,所以只需要GPA->HPA的转换;一旦guest开启了iommu(我们称作v-iommu),如果guest内部想使用VFIO在用户空间使用某个透传设备,因为IOMMU无法像MMU一样支持2级页表,所以每次在guest内部配置v-iommu页表项,都要做vm-exit,然后将v-iommu的GVA->GPA转换成GVA->HPA,写到真实的IOMMU页表中,这就像是Guest CPU访问场景中尚未支持EPT页表二级页表时的影子页表机制。整个映射流程如下图所示。
Scalable Mode引入First-Stage Page Table和Second-Stage Page Table两级页表,IOMMU硬件就可以支持Guest也开启v-iommu的场景。类似于MMU/EPT两级页表,IOMMU的一级页表用于GPA到HPA的转换,二级页表用于GVA到GPA的转换。
IOMMU采用了灵活的使用方式,每个独立的[BDF+pasid]指定的PASID ENTRY表项里,都可以指定当前采用Pass-through/ First-Stage Translation Only/ Second-Stage Translation Only/ Nested Translation,有4种模式可选。
1、Pasid Table Entry
我们看一下PASID Table entry的字段,在VT-d手册里都有详细的描述。
每个表项有512b=64B大小,目前只使用了低32B。其中
PGTT: PASID Granular Translation Type 用于标识当前的页表模式,
001b: First-stage Translation only
010b: Second-stage Translation only
011b: Nested Translation
100b: Pass-through
FSPTPTR和SSPTPTR分别表示First-Stage Table和Second-Stage Table的基地址。
2、Passthrough
Pass-through模式显而易见就是对于IOVA=HPA的场景,比如host 内核空间使用的设备;
3、First-stage/Second-stage Translation
而First-Stage Table Only和Second-Stage Table Only都是针对一级页表转换场景,有什么区别?实际从内核的使用方式来看,如果判断当前IOMMU支持scalable模式+内核开启了iommu选项,则iommu的capability寄存器只要支持flts(first-stage ***),就优先使用first-stage模式,否则使用second-stage模式;
梳理一下内核相应的代码流程。
在另一篇文章《IOMMU_GROUP创建流程》中,对iommu遍历pcie设备创建iommu_group的流程做了梳理。事实上,不仅会遍历每个pcie设备申请iommu_group,还会为每个设备执行一个重要的函数,__iommu_attach_device();
static int __iommu_attach_device(struct iommu_domain *domain,
struct device *dev)
{
int ret;
if (unlikely(domain->ops->attach_dev == NULL))
return -ENODEV;
ret = domain->ops->attach_dev(domain, dev);
if (!ret)
trace_attach_device_to_domain(dev);
return ret;
}
intel_iommu_ops->attach_dev = intel_iommu_attach_device;
intel_iommu_attach_device() -> domain_add_dev_info() -> dmar_insert_one_dev_info()
//dmar_insert_one_dev_info()的相关代码
{
/* PASID table is mandatory for a PCI device in scalable mode. */
if (dev && dev_is_pci(dev) && sm_supported(iommu)) {
ret = intel_pasid_alloc_table(dev);
if (ret) {
dev_err(dev, "PASID table allocation failed\n");
dmar_remove_one_dev_info(dev);
return NULL;
}
/* Setup the PASID entry for requests without PASID: */
spin_lock_irqsave(&iommu->lock, flags);
if (hw_pass_through && domain_type_is_si(domain))
ret = intel_pasid_setup_pass_through(iommu, domain,
dev, PASID_RID2PASID);
else if (domain_use_first_level(domain))
ret = domain_setup_first_level(iommu, domain, dev,
PASID_RID2PASID); //-->intel_pasid_setup_first_level
else
ret = intel_pasid_setup_second_level(iommu, domain,
dev, PASID_RID2PASID);
spin_unlock_irqrestore(&iommu->lock, flags);
if (ret) {
dev_err(dev, "Setup RID2PASID failed\n");
dmar_remove_one_dev_info(dev);
return NULL;
}
}
}
//上面的函数列出了默认的三种选项
intel_pasid_setup_pass_through() //passthrough模式
intel_pasid_setup_first_level() //first-stage模式
intel_pasid_setup_second_level() //second-stage模式
4、Nested Translation
Nested translation就是针对透传到GUEST中的pcie设备,guest开启v-iommu的场景。在我的5.14.13内核里,对nested页表的支持并不完整,看到了这个接口,但是并没有调用的位置,看到有使用这个接口的VFIO 驱动patch,不知道是否合入了最新的内核。总体来看,内核对于scalable mode的两级页表模式短期内不会很完善,可能云厂商们会进行定制化的修改。
iommu_uapi_sva_bind_gpasid() -->
domain->ops->sva_bind_gpasid() = intel_svm_bind_gpasid() -->
intel_pasid_setup_nested()
intel_pasid_setup_nested() //nested模式
iommu_uapi_sva_bind_gpasid这个接口就是iommu驱动对外开放的nested页表配置接口。这个接口会传入一个gpgd地址,用于写入给first-stage table的首地址。
pasid_set_flptr(pte, (uintptr_t)gpgd); //First-stage页表首地址,使用gpgd
pgd = domain->pgd;
pgd_val = virt_to_phys(pgd);
pasid_set_slptr(pte, pgd_val); //Second-Stage页表首地址,使用domain->pgd
所以,Second-Stage是GPA到HPA的转换(能否复用EPT页表?),First-Stage是GVA到GPA的转换(能否复用进程页表?)。
文章来源地址https://uudwc.com/A/X3R2Z
后面可以查阅和补充一下Nested Translation和VFIO透传设备结合的具体使用场景,IOVA在Nested Translation时的完整翻译流程。文章来源:https://uudwc.com/A/X3R2Z