内核的iommu驱动在drivers/iommu下,因为虚拟化相关的设计与硬件体系架构强相关,所以iommu的驱动包含软件提取的通用框架层代码和各体系结构相关代码,包括intel/amd/arm等。
本文简单梳理了intel的iommu主线流程,内核基于linux 5.14.13。
一、驱动框架的相关入口一般都是各种initcall函数,在内核初始化的最后阶段调用。intel的iommu的入口在IOMMU_INIT_POST(detect_intel_iommu);
在这个接口里,会检测当前的硬件是否是intel的iommu,如果是则执行后续的初始化。
int __init detect_intel_iommu(void)
{
int ret;
struct dmar_res_callback validate_drhd_cb = {
.cb[ACPI_DMAR_TYPE_HARDWARE_UNIT] = &dmar_validate_one_drhd,
.ignore_unhandled = true,
};
down_write(&dmar_global_lock);
ret = dmar_table_detect();
if (!ret)
ret = dmar_walk_dmar_table((struct acpi_table_dmar *)dmar_tbl,
&validate_drhd_cb);
if (!ret && !no_iommu && !iommu_detected &&
(!dmar_disabled || dmar_platform_optin())) {
iommu_detected = 1;
/* Make sure ACS will be enabled */
pci_request_acs();
}
#ifdef CONFIG_X86
if (!ret) {
x86_init.iommu.iommu_init = intel_iommu_init;
x86_platform.iommu_shutdown = intel_iommu_shutdown;
}
#endif
if (dmar_tbl) {
acpi_put_table(dmar_tbl);
dmar_tbl = NULL;
}
up_write(&dmar_global_lock);
return ret ? ret : 1;
}
可以看到,内核阶段是根据acpi表的内容进行判断的,如果acpi初始化了相关内容,且内核配置打开iommu(!no_iommu),则配置x86.init_iommu.iommu_init = intel_iommu_init.
这里只是配置了intel的iommu_init接口,下面看调用它的位置以及这个接口具体做了什么。
二、在arch/x86/kernel/pci-dma.c中,调用了x86.init_iommu_iommu_init()接口。
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);
这也是一个initcall函数,想必IOMMU_INIT_POST的优先级比rootfs_initcall的优先级要高了。
三、intel_iommu_init()接口做了什么,如何为每个pci设备创建了iommu_group和iommu_domain.
int __init intel_iommu_init(void)
{
/* ............ */
ret = init_dmars();
/* ............ */
bus_set_iommu(&pci_bus_type, &intel_iommu_ops);
/* ............ */
}
其实为每个pci设备创建iommu_group和iommu_domain的过程就在bus_set_iommu接口里。内核启动过程中会有相关日志打印,每个设备都会有"Adding device to iommu_group 12"这样的日志信息。
bus_set_iommu()接口又继续调用了iommu_bus_init()接口,一共做了三件重要的事:
1、pci_bus_type->iommu_ops = intel_iommu_ops; iommu_ops是一组核心接口,不同体系架构的都会注册不同的iommu_ops。
2、为pci bus注册了一个notifier,notifier_call是iommu_bus_notifier。(有效事件是BUS_NOTIFY_ADD_DEVICE,为热插入设备执行iommu_probe_device)。
3、bus_iommu_probe接口,遍历当前pci bus挂载的冷启动设备,执行probe_iommu_group:1)创建iommu_group并添加到group_list中;2)然后为group_list中的所有group申请domain,创建direct mapping。
实际分析代码,iommu_probe_device接口的操作与probe_iommu_group接口中为每个设备做的操作是一致的。这也是符合预期的,热插拔和冷启动的设备执行同样的动作。
这两个接口都会调用__iommu_probe_device()。
四、__iommu_probe_device做了什么
static int __iommu_probe_device(struct device *dev, struct list_head *group_list)
{
const struct iommu_ops *ops = dev->bus->iommu_ops;
/* ....... */
iommu_dev = ops->probe_device(dev); //intel_iommu_probe_device
if (IS_ERR(iommu_dev)) {
ret = PTR_ERR(iommu_dev);
goto out_module_put;
}
dev->iommu->iommu_dev = iommu_dev;
group = iommu_group_get_for_dev(dev); //创建iommu_group, Adding to iommu_group **,是在这里打印的
if (IS_ERR(group)) {
ret = PTR_ERR(group);
goto out_release;
}
iommu_group_put(group);
if (group_list && !group->default_domain && list_empty(&group->entry))
list_add_tail(&group->entry, group_list); //添加到group_list链表中
iommu_device_link(iommu_dev, dev); //创建sys目录下的链接关系
/* ...... */
}
iommu_group_get_for_dev() ----> ops->device_group() = intel_iommu_device_group() ----> pci_device_group()
在pci_device_group()接口里,会牵涉到一个重要的特性ACS。如果设备不支持ACS,那么挂在同一个总线下的所有设备都必须使用一个iommu_group,这是从安全性考虑的。只有通过ACS禁止掉了P2P,主机才会认为设备是安全的,可以分配给不通的虚机或地址空间使用,看起来这部分工作是内核的软件来控制的。
这部分代码还有一些其他的alias逻辑,作者只是分析了ACS相关的内容。
#define REQ_ACS_FLAGS (PCI_ACS_SV | PCI_ACS_RR | PCI_ACS_CR | PCI_ACS_UF)
因此设备侧如果需要扩展总线和多级设备,也需要配置每一级port和device设备的ACS特性,具体使能的字段如上定义。
struct iommu_group *pci_device_group(struct device *dev)
{
struct pci_dev *pdev = to_pci_dev(dev);
struct group_for_pci_data data;
struct pci_bus *bus;
/* ...... */
/*
* Continue upstream from the point of minimum IOMMU granularity
* due to aliases to the point where devices are protected from
* peer-to-peer DMA by PCI ACS. Again, if we find an existing
* group, use it.
*/
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)) //当前总线port使能了REQ_ACS_FLAGS,查找失败,break退出
break;
pdev = bus->self;
group = iommu_group_get(&pdev->dev); //当前总线已经分配了group,使用该group并立即返回
if (group)
return group;
}
/* ...... */
return iommu_group_alloc();
}
EXPORT_SYMBOL_GPL(pci_device_group);