1、原理介绍
使用iommu,可以改变虚拟机外设中断的投递方式。以msi中断为例,msi msg里不再需要填写相关的中断信息,而是转换成interrput index的方式。中断的管理信息(投递方式、目标cpu信息、vector信息)存放在一个叫irte的内存区域里,每个iommu最多可以有64k个irte,iommu通过interrupt index找到对应的irte,iommu的irte基址信息存放在iommu的IRTA(Interrupt Remapping Table Address)寄存器里。
中断请求的格式有两种(Compatibility Format和Remappable Format),如下所示,在Compatibility Format格式下,address需要包含中断的Destination ID信息,Data需要包含中断的delivery Mode、Trigger Mode等信息,并且bit 4的interrupt Format需要清0。
而在Remappable Format模式下,addess主要保存的是Handle信息,并且bit 4置1,data保存subhandle信息,iommu通过Handle及subhandle找到对应的irte。
Remappable Format时,不同的中断号可以共享同一个handle(最终对应同一个irte),比如同一个网卡设备的多个队列中断;也可设置每个中断使用独立的irte,这通过address的bit 3(SHV)位来标识(默认置1,使用共享模式)。iommu根据SHV以及handle值来查找interrupte_index。
if (address.SHV == 0) {
interrupt_index = address.handle;
} else {
interrupt_index = (address.handle + data.subhandle);
}
2、初始化过程
系统启动时,会根据设置的内核参数intel_iommu决定是否使用iommu,当设置了该参数后,会去解析相关的iommu信息。可以同时存在多个iommu硬件,每个iommu能管理的pci设备是固定的,iommu的相关信息bios会放在table里通知os,os通过读取table相关信息初始化iommu。
kernel_init
kernel_init_freeable
smp_prepare_cpus
native_smp_prepare_cpus
default_setup_apic_routing
enable_IR_x2apic
irq_remapping_prepare
intel_prepare_irq_remapping
dmar_table_init
parse_dmar_table
dmar_parse_one_drhd
alloc_iommu
map_iommu
dmar_register_drhd_unit(将dmar注册到到全局链表中dmar_drhd_units)
这里的流程主要是os启动时解析iommu的过程,包括bios提供的iommu table信息,映射iommu的io地址空间等。
static int map_iommu(struct intel_iommu *iommu, u64 phys_addr)
{
int map_size, err=0;
iommu->reg_phys = phys_addr;
iommu->reg_size = VTD_PAGE_SIZE;
if (!request_mem_region(iommu->reg_phys, iommu->reg_size, iommu->name)) {
pr_err("Can't reserve memory\n");
err = -EBUSY;
goto out;
}
//映射iommu的io地址空间
iommu->reg = ioremap(iommu->reg_phys, iommu->reg_size);
if (!iommu->reg) {
pr_err("Can't map the region\n");
err = -ENOMEM;
goto release;
}
//获取iommu的capility
iommu->cap = dmar_readq(iommu->reg + DMAR_CAP_REG);
iommu->ecap = dmar_readq(iommu->reg + DMAR_ECAP_REG);
}
接下来主要是iommu的remapping初始化,第一步先分配好irte空间,并将地址信息设置到IRTA寄存器。
static int intel_setup_irq_remapping(struct intel_iommu *iommu)
{
ir_table = kzalloc(sizeof(struct ir_table), GFP_KERNEL);
if (!ir_table)
return -ENOMEM;
pages = alloc_pages_node(iommu->node, GFP_KERNEL | __GFP_ZERO,
INTR_REMAP_PAGE_ORDER);
bitmap = kcalloc(BITS_TO_LONGS(INTR_REMAP_TABLE_ENTRIES),
sizeof(long), GFP_ATOMIC);
if (bitmap == NULL) {
pr_err("IR%d: failed to allocate bitmap\n", iommu->seq_id);
goto out_free_pages;
}
//设置iommu的irte地址信息
ir_table->base = page_address(pages);
ir_table->bitmap = bitmap;
iommu->ir_table = ir_table;
iommu_set_irq_remapping(iommu, eim_mode);
return 0;
}
static void iommu_set_irq_remapping(struct intel_iommu *iommu, int mode)
{
addr = virt_to_phys((void *)iommu->ir_table->base);
//将irte地址信息通知给硬件
dmar_writeq(iommu->reg + DMAR_IRTA_REG,
(addr) | IR_X2APIC_MODE(mode) | INTR_REMAP_TABLE_REG_SIZE);
/* Set interrupt-remapping table pointer */
writel(iommu->gcmd | DMA_GCMD_SIRTP, iommu->reg + DMAR_GCMD_REG);
}
然后是使能iommu的remapping模式:
ry_to_enable_IR
irq_remapping_enable
intel_enable_irq_remapping
iommu_enable_irq_remapping
/* Enable interrupt-remapping */
iommu->gcmd |= DMA_GCMD_IRE;
iommu->gcmd &= ~DMA_GCMD_CFI; /* Block compatibility-format MSIs */
writel(iommu->gcmd, iommu->reg + DMAR_GCMD_REG);
开启iommu的remapping模式后,还会进一步检查iommu是有有interrupt post能力,如果有,则将intel_irq_remap_ops.capabiliy或上IRQ_POSTING_CAP,后续vcpu启动时会检查该标志。
static inline void set_irq_posting_cap(void)
{
struct dmar_drhd_unit *drhd;
struct intel_iommu *iommu;
//以下几个条件同时成立时,会将intel_irq_remap_ops.capability与上IRQ_POSTING_CAP标志
//位,后续vcpu创建的时候会关注该标志位
//1)disable_irq_post变量没有置1;
//2)CPU支持CMPXCHG16B原子指令(一般支持);
//3)iommu的cap具有post能力。
if (!disable_irq_post) {
/*
* If IRTE is in posted format, the 'pda' field goes across the
* 64-bit boundary, we need use cmpxchg16b to atomically update
* it. We only expose posted-interrupt when X86_FEATURE_CX16
* is supported. Actually, hardware platforms supporting PI
* should have X86_FEATURE_CX16 support, this has been confirmed
* with Intel hardware guys.
*/
if ( cpu_has_cx16 )
intel_irq_remap_ops.capability |= 1 << IRQ_POSTING_CAP;
for_each_iommu(iommu, drhd)
if (!cap_pi_support(iommu->cap)) {
intel_irq_remap_ops.capability &=
~(1 << IRQ_POSTING_CAP);
break;
}
}
}
最后重新安装irq remapping的操作函数:
static void __init irq_remapping_modify_x86_ops(void)
{
x86_io_apic_ops.disable = irq_remapping_disable_io_apic;
x86_io_apic_ops.set_affinity = set_remapped_irq_affinity;
x86_io_apic_ops.setup_entry = setup_ioapic_remapped_entry;
x86_io_apic_ops.eoi_ioapic_pin = eoi_ioapic_pin_remapped;
x86_msi.setup_msi_irqs = irq_remapping_setup_msi_irqs;
x86_msi.setup_hpet_msi = setup_hpet_msi_remapped;
x86_msi.compose_msi_msg = compose_remapped_msi_msg;
}
3、Guest虚拟机启动后,会为直通设备申请msi中断,然后enable_msi,这个过程被Qemu捕获,Qemu通过ioctl,通知vfio-pci模式设置enable。
vfio_pci_ioctl
vfio_pci_set_irqs_ioctl
vfio_pci_set_msi_trigger
vfio_msi_enable
pci_enable_msix_range
pci_enable_msix
msix_capability_init
msix_setup_entries
static int msix_setup_entries(struct pci_dev *dev, void __iomem *base,
struct msix_entry *entries, int nvec)
{
struct msi_desc *entry;
int i;
for (i = 0; i < nvec; i++) {
entry = alloc_msi_entry(dev);
if (!entry) {
if (!i)
iounmap(base);
else
free_msi_irqs(dev);
/* No enough memory. Don't try again */
return -ENOMEM;
}
entry->msi_attrib.is_msix = 1;
entry->msi_attrib.is_64 = 1;
entry->msi_attrib.entry_nr = entries[i].entry;
entry->msi_attrib.default_irq = dev->irq;
//设置entry的io地址信息,base为msix_map_region(dev, msix_table_size(control))
//映射到的msix io地址空间,后续需要使用该地址mask、unmask riq,以及设置msi的msg
//地址信息等
entry->mask_base = base;
entry->nvec_used = 1;
//将所有的entry都存放到msi_list里
list_add_tail(&entry->list, &dev->msi_list);
}
return 0;
}
再接下来主要是setup每个entry
arch_setup_msi_irqs
irq_remapping_setup_msi_irqs
do_setup_msix_irqs(遍历mst_list链表,为每个msi_entry设置中断信息)、
1)、如果是第一个msi_entry:
msi_alloc_remapped_irq
intel_msi_alloc_irq
alloc_irte(从iommu->ir_table分配一个可用的irte表索引号)
static int alloc_irte(struct intel_iommu *iommu, int irq, u16 count)
{
//从bitmap里获取一个可用的irte index
index = bitmap_find_free_region(table->bitmap,
INTR_REMAP_TABLE_ENTRIES, mask);
if (index < 0) {
pr_warn("IR%d: can't allocate an IRTE\n", iommu->seq_id);
} else {
cfg->remapped = 1;
irq_iommu->iommu = iommu;
//设置irte index
irq_iommu->irte_index = index;
//第一个中断信息sub_handle为0
irq_iommu->sub_handle = 0;
irq_iommu->irte_mask = mask;
irq_iommu->mode = IRQ_REMAPPING;
}
return index;
}
分配完irte后,就需要去设置irte信息:
setup_msi_irq
msi_compose_msg
intel_compose_msi_msg
static void intel_compose_msi_msg(struct pci_dev *pdev,
unsigned int irq, unsigned int dest,
struct msi_msg *msg, u8 hpet_id)
{
cfg = irq_get_chip_data(irq);
//获取sub_handle及irte index信息
ir_index = map_irq_to_irte_handle(irq, &sub_handle);
BUG_ON(ir_index == -1);
//获取irte
irte = get_irte(irq_2_iommu(irq));
//设置irte的trigger_mode、vector、及irq的dest信息
prepare_irte(irte, cfg->vector, dest);
/* Set source-id of interrupt request */
//根据pci信息设置irte的source-id
if (pdev)
set_msi_sid(irte, pdev);
else
set_hpet_sid(irte, hpet_id);
modify_irte(irq, irte);
//设置irte的address及data信息
msg->address_hi = MSI_ADDR_BASE_HI;
msg->data = sub_handle;
//设置irte为共享模式,多个中断可以共用一个irte
msg->address_lo = MSI_ADDR_BASE_LO | MSI_ADDR_IR_EXT_INT |
MSI_ADDR_IR_SHV |
MSI_ADDR_IR_INDEX1(ir_index) |
MSI_ADDR_IR_INDEX2(ir_index);
}
2)、如果非第1个msi_entry,则不重新分配irte,而是与第一个共享同一个irte,因此这里的流程主要是使用第一个msi_entry分配的irte,并修改其中的subhandle信息,然后通知给硬件。
4、Remapping模式下,iommu会将发往Guest的中断先转发给宿主机,因此需要在宿主机安装中断处理函数。
vfio_msi_set_block
vfio_msi_set_vector_signal
request_irq(为irq分配action处理函数vfio_msihandler)
vfio_msihandler主要功能就是通过eventfd的方式通知kvm,kvm进一步将中断信息注入到Guest。
/*
* MSI/MSI-X
*/
static irqreturn_t vfio_msihandler(int irq, void *arg)
{
struct eventfd_ctx *trigger = arg;
eventfd_signal(trigger, 1);
return IRQ_HANDLED;
}