VT-d 中斷重映射分析


 https://kernelgo.org/vtd_interrupt_remapping_code_analysis.html

 

本文中我們將一起來分析一下VT-d中斷重映射的代碼實現, 在看本文前建議先復習一下VT-d中斷重映射的原理,可以參考VT-D Interrupt Remapping這篇文章。 看完中斷重映射的原理我們必須明白:直通設備的中斷是無法直接投遞到Guest中的,需要先將其中斷映射到host的某個中斷上,然后再重定向(由VMM投遞)到Guest內部.

我們將從

  • 1.中斷重映射Enable
  • 2.中斷重映射實現
  • 3.中斷重映射下中斷處理流程

這3個層面去分析VT-d中斷重映射的代碼實現。

1.中斷重映射Enable

當BIOS開啟VT-d特性之后,操作系統初始化的時候會通過cpuid去檢測硬件平台是否支持VT-d Interrupt Remapping能力, 然后做一些初始化工作后將操作系統的中斷處理方式更改為Interrupt Remapping模式。

start_kernel --> late_time_init --> x86_late_time_init --> x86_init.irqs.intr_mode_init() --> apic_intr_mode_init --> default_setup_apic_routing --> enable_IR_x2apic --> irq_remapping_prepare # Step1:使能Interrupt Reampping --> intel_irq_remap_ops.prepare() --> remap_ops = &intel_irq_remap_ops --> irq_remapping_enable # Step2:做一些工作 --> remap_ops->enable() 

從代碼堆棧可以看到內核初始化的時候會初始化中斷,在default_setup_apic_routing中會分2個階段對Interrupt Remapping進行Enable。 這里涉及到一個關鍵的數據結構intel_irq_remap_ops,它是Intel提供的Intel CPU平台的中斷重映射驅動方法集。

struct irq_remap_ops intel_irq_remap_ops = { .prepare = intel_prepare_irq_remapping, .enable = intel_enable_irq_remapping, .disable = disable_irq_remapping, .reenable = reenable_irq_remapping, .enable_faulting = enable_drhd_fault_handling, .get_ir_irq_domain = intel_get_ir_irq_domain, .get_irq_domain = intel_get_irq_domain, }; 

階段1調用intel_irq_remap_ops的prepare方法。該方法主要做的事情有:

  • 調用了dmar_table_init從ACPI表中解析了DMAR Table關鍵信息。 關於VT-d相關的ACPI Table信息可以參考VT-D Spec Chapter 8 BIOS ConsiderationIntroduction to Intel IOMMU 這篇文章。
  • 遍歷每個iommu檢查是否支持中斷重映射功能(dmar_ir_support)。
  • 調用intel_setup_irq_remapping為每個IOMMU分配中斷重映射表(Interrupt Remapping Table)。
static int intel_setup_irq_remapping(struct intel_iommu *iommu) { ir_table = kzalloc(sizeof(struct ir_table), GFP_KERNEL); //為IOMMU申請一塊內存,存放ir_table和對應的bitmap 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); // 創建ir_domain和ir_msi_domain iommu->ir_domain = irq_domain_create_hierarchy(arch_get_ir_parent_domain(), 0, INTR_REMAP_TABLE_ENTRIES, fn, &intel_ir_domain_ops, iommu); irq_domain_free_fwnode(fn); iommu->ir_msi_domain = arch_create_remap_msi_irq_domain(iommu->ir_domain, "INTEL-IR-MSI", iommu->seq_id); ir_table->base = page_address(pages); ir_table->bitmap = bitmap; iommu->ir_table = ir_table; iommu_set_irq_remapping //最后將ir_table地址寫入到寄存器中並最后enable中斷重映射能力 } 

階段2調用irq_remapping_enable中判斷Interrupt Remapping是否Enable如果還沒有就Enable一下,然后set_irq_posting_cap設置Posted Interrupt Capability等。

2.中斷重映射實現

要使得直通設備的中斷能夠工作在Interrupt Remapping模式下VFIO中需要做很多的准備工作. 首先,QEMU會通過PCI配置空間向操作系統呈現直通設備的MSI/MSI-X Capability, 這樣Guest OS加載設備驅動程序時候會嘗試去Enable直通設備的MSI/MSI-X中斷. 為了方便分析代碼流程這里以MSI中斷為例。 Guest OS設備驅動嘗試寫配置空間來Enable設備中斷,這時候會訪問設備PCI配置空間發生VM Exit被QEMU截獲處理. 對於MSI中斷Enable會調用vfio_msi_enable函數.

從PCI Local Bus Spec 可以知道MSI中斷的PCI Capability為xxx

QEMU Code: static void vfio_msi_enable(VFIOPCIDevice *vdev) { int ret, i; vfio_disable_interrupts(vdev); #先disable設備中斷 vdev->nr_vectors = msi_nr_vectors_allocated(&vdev->pdev); #從設備配置空間讀取設備Enable的MSI中斷數目 vdev->msi_vectors = g_new0(VFIOMSIVector, vdev->nr_vectors); for (i = 0; i < vdev->nr_vectors; i++) { VFIOMSIVector *vector = &vdev->msi_vectors[i]; vector->vdev = vdev; vector->virq = -1; vector->use = true; if (event_notifier_init(&vector->interrupt, 0)) { error_report("vfio: Error: event_notifier_init failed"); } qemu_set_fd_handler(event_notifier_get_fd(&vector->interrupt), // 綁定irqfd的處理函數 vfio_msi_interrupt, NULL, vector); // 將中斷信息刷新到kvm irq routing table里,其實就是建立起gsi和irqfd的映射關系 vfio_add_kvm_msi_virq(vdev, vector, i, false);  } /* Set interrupt type prior to possible interrupts */ vdev->interrupt = VFIO_INT_MSI; // 使能msi中斷!!!重點分析 ret = vfio_enable_vectors(vdev, false);   .... } 

vfio_msi_enable的主要流程是:從配置空間查詢支持的中斷數目 --> 對每個MSI中斷進行初始化(分配一個irqfd) --> 將MSI gsi信息和irqfd綁定並刷新到中斷路由表中 --> 使能中斷(調用vfio-pci內核ioctl為MSI中斷申請irte並刷新中斷路由表表項)。

 vfio_pci_write_config --> vfio_msi_enable --> vfio_add_kvm_msi_virq --> kvm_irqchip_add_msi_route #為MSI中斷申請gsi,並更新irq routing tableQEMU里面有一份Copy--> kvm_irqchip_commit_routes --> kvm_irqchip_add_irqfd_notifier_gsi #將irqfd和gsi映射信息注冊到kvm內核模塊中fd = kvm_interrupt, gsi=virq, flags=0, rfd=NULL --> kvm_vm_ioctl(s, KVM_IRQFD, &irqfd) --> vfio_enable_vectors --> ioctl(vdev->vbasedev.fd, VFIO_DEVICE_SET_IRQS, irq_set) #調用vfio-pci內核接口使能中斷,重點分析! 

kvm_irqchip_commit_routes的邏輯比較簡單這里跳過,kvm_irqchip_add_irqfd_notifier_gsi的邏輯稍微有些復雜后面專門寫一篇來分析, 只需要知道這里是將gsi和irqfd信息注冊到內核模塊中(KVM irqfd提供了一種中斷注入機制)並且可以在這個fd上監聽事件來達到中斷注入的目的。 這里重點分析vfio_enable_vectors的代碼流程。

Kernel Code: vfio_enable_vectors --> vfio_pci_ioctl // irq_set 傳入了一個irqfd數組 --> vfio_pci_set_irqs_ioctl --> vfio_pci_set_msi_trigger --> vfio_msi_enable #Step1:為MSI中斷申請Host IRQ,這里會一直調用到Interrupt Remapping框架分配IRTE --> pci_alloc_irq_vectors --> vfio_msi_set_block #Step2:這里綁定irqfd,建立好中斷注入通道 --> vfio_msi_set_vector_signal 

vfio_pci_set_msi_trigger函數中主要有2個關鍵步驟。

vfio_msi_enable

vfio_msi_enable -> pci_alloc_irq_vectors -> pci_alloc_irq_vectors_affinity -> __pci_enable_msi_range -> msi_capability_init -> pci_msi_setup_msi_irqs -> arch_setup_msi_irqs -> x86_msi.setup_msi_irqs -> native_setup_msi_irqs -> msi_domain_alloc_irqs -> __irq_domain_alloc_irqs,irq_domain_activate_irq

這里內核調用棧比較深,我們只需要知道vfio_msi_enable最終調用到了__irq_domain_alloc_irqs -> intel_irq_remapping_alloc. 在intel_irq_remapping_alloc中申請這個中斷對應的IRTE。這里先調用的alloc_irte函數返回irte在中斷重映射表中的index號(即中斷重映射表的索引號), 再調用intel_irq_remapping_prepare_irte去填充irte。

static int intel_irq_remapping_alloc(struct irq_domain *domain, unsigned int virq, unsigned int nr_irqs, void *arg) { index = alloc_irte(iommu, virq, &data->irq_2_iommu, nr_irqs); #向Interrupt Remapping Table申請index for (i = 0; i < nr_irqs; i++) { irq_data = irq_domain_get_irq_data(domain, virq + i); irq_cfg = irqd_cfg(irq_data); irq_data->hwirq = (index << 16) + i; irq_data->chip_data = ird; irq_data->chip = &intel_ir_chip; intel_irq_remapping_prepare_irte(ird, irq_cfg, info, index, i); } } 

irq_domain_activate_irq最終會調用到:intel_irq_remapping_activate -> intel_ir_reconfigure_irte -> modify_irte 。 modify_irte中會將新的irte刷新到中斷重定向表中。

vfio_msi_set_block

vfio_msi_set_block中調用vfio_msi_set_vector_signal為每個msi中斷安排其Host IRQ的信號處理鈎子,用來完成中斷注入。 其內核調用棧為:

vfio_pci_ioctl vfio_pci_set_irqs_ioctl vfio_pci_set_msi_trigger vfio_msi_set_block irq_bypass_register_producer __connect kvm_arch_irq_bypass_add_producer vmx_update_pi_irte #在Posted Interrupt模式下在這里刷新irte為Posted Interrupt 模式 irq_set_vcpu_affinity intel_ir_set_vcpu_affinity modify_irte 

再看下一vfio_msi_set_vector_signal的代碼主要流程。 可以看出vfio_msi_set_vector_signal中為設備MSI中斷申請了一個ISR,即vfio_msihandler, 然后注冊了一個producer。

static int vfio_msi_set_vector_signal(struct vfio_pci_device *vdev, int vector, int fd, bool msix) { irq = pci_irq_vector(pdev, vector); #獲得每個MSI中斷的irq號trigger = eventfd_ctx_fdget(fd); if (msix) { struct msi_msg msg; get_cached_msi_msg(irq, &msg); pci_write_msi_msg(irq, &msg); } #在host上申請中斷處理函數 ret = request_irq(irq, vfio_msihandler, 0, vdev->ctx[vector].name, trigger); vdev->ctx[vector].producer.token = trigger; # irqfd對應的event_ctx vdev->ctx[vector].producer.irq = irq; ret = irq_bypass_register_producer(&vdev->ctx[vector].producer); vdev->ctx[vector].trigger = trigger; } 

這樣直通設備的中斷會觸發Host上的vfio_msihandler這個中斷處理函數。在這個函數中向這個irqfd發送了一個信號通知中斷到來, 如此一來KVM irqfd機制在poll這個irqfd的時候會受到這個事件,隨后調用事件的處理函數注入中斷。 irqfd_wakeup -> EPOLLIN -> schedule_work(&irqfd->inject) -> irqfd_inject -> kvm_set_irq這樣就把中斷注入到虛擬機了。

static irqreturn_t vfio_msihandler(int irq, void *arg) { struct eventfd_ctx *trigger = arg; eventfd_signal(trigger, 1); return IRQ_HANDLED; }
__u64 eventfd_signal(struct eventfd_ctx *ctx, __u64 n)
{
    unsigned long flags;
    spin_lock_irqsave(&ctx->wqh.lock, flags);
    if (ULLONG_MAX - ctx->count < n)
        n = ULLONG_MAX - ctx->count;
    ctx->count += n;
    if (waitqueue_active(&ctx->wqh))
        wake_up_locked_poll(&ctx->wqh, POLLIN);
    spin_unlock_irqrestore(&ctx->wqh.lock, flags);
    return n;
}

 

思考一下:為什么觸發這個irqfd的寫事件后,直通設備的中斷就能夠被重映射到虛擬機內部呢?

原因在於,前面我們提到的直通設備MSI中斷的GSI和irqfd是一對一綁定的, 所以直通設備在向Guest vCPU投遞MSI中斷的時候首先會被IOMMU截獲, 中斷被重定向到Host IRQ上,然后通過irqfd注入MSI中斷到虛擬機內部。

3.中斷重映射下中斷處理流程

為了方便理解,我花了點時間畫了下面這張圖,方便讀者理解中斷重映射場景下直通設備的中斷處理流程:

device passthrough interrupt handling

總結一下中斷重映射Enable和處理流程:

QEMU向虛擬機呈現設備的PCI配置空間信息  -> 設備驅動加載,讀寫PCI配置空間Enable MSI  -> VM Exit到QEMU中處理vfio_pci_write_config  -> QEMU調用vfio_msi_enable使能MSI中斷  -> kvm_set_irq_routing更新中斷路由表PRT,  kvm_irqfd_assign注冊irqfd和gsi的映射關系,  vfio_pci_set_msi_trigger分配Host irq並分配對應的IRTE和刷新中斷重映射表,  vfio_msi_set_vector_signal注冊Host irq的中斷處理函數vfio_msihandler  -> vfio_msihandler寫了irqfd這樣就觸發了EPOLLIN事件  -> irqfd接受到EPOLLIN事件,調用irqfd_wakeup  -> kvm_arch_set_irq_inatomic 嘗試直接注入中斷,如果被BLOCK了(vCPU沒有退出?)就調用 schedule_work(&irqfd->inject),讓kworker延后處理  -> irqfd_inject向虛擬機注入中斷  -> 虛擬機退出的時候寫對應VCPU的vAPIC Page IRR字段注入中斷到Guest內部。 

Done!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM