IOMMU之Interrupt Remapping

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;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章