1. 程式人生 > 實用技巧 >VT-d 中斷重對映分析

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

思考一下:為什麼觸發這個irqfd的寫事件後,直通裝置的中斷就能夠被重對映到虛擬機器內部呢?

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

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

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

總結一下中斷重對映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!