天天看點

KVM中斷控制器模拟QEMU裝置到vGICvGIC到CPU interfaceCPU interface到CPU(Deliver)

文章目錄

  • QEMU裝置到vGIC
  • vGIC到CPU interface
    • Distributor
      • 資料結構
      • 初始化
    • 路由表
      • 路由表項
      • 預設配置
        • ARM
        • X86
      • 配置
    • CPU interface
      • ARM
      • X86
  • CPU interface到CPU(Deliver)

QEMU裝置到vGIC

  • kvm子產品通過ioctl指令字接收使用者态程式的指令,QEMU kvm_vm_ioctl下發的指令字,最終調用kvm_vm_fops檔案操作的回調
static struct file_operations kvm_vm_fops = {
    .release        = kvm_vm_release,
    .unlocked_ioctl = kvm_vm_ioctl,
    .llseek     = noop_llseek,
};
kvm_vm_ioctl
	switch (ioctl) 
		case KVM_IRQ_LINE_STATUS:
    	case KVM_IRQ_LINE: 
    		/*體系結構相關,arm/x86/powerpc */
    		kvm_vm_ioctl_irq_line(kvm, &irq_event, ioctl == KVM_IRQ_LINE_STATUS)
           
  • 單獨分析arm的kvm_vm_ioctl_irq_line
int kvm_vm_ioctl_irq_line(struct kvm *kvm, struct kvm_irq_level *irq_level,
              bool line_status)
{             
	......
    irq_type = (irq >> KVM_ARM_IRQ_TYPE_SHIFT) & KVM_ARM_IRQ_TYPE_MASK;
    vcpu_idx = (irq >> KVM_ARM_IRQ_VCPU_SHIFT) & KVM_ARM_IRQ_VCPU_MASK;
    irq_num = (irq >> KVM_ARM_IRQ_NUM_SHIFT) & KVM_ARM_IRQ_NUM_MASK;
	
    switch (irq_type) {
	......
    case KVM_ARM_IRQ_TYPE_SPI:
        if (!irqchip_in_kernel(kvm))
            return -ENXIO;
		/* 外部中斷号不能小于ARM規定的前32個中斷号*/
        if (irq_num < VGIC_NR_PRIVATE_IRQS)
            return -EINVAL;
        /* 将中斷資訊注入到vGIC */
        return kvm_vgic_inject_irq(kvm, 0, irq_num, level, NULL);
    }
    return -EINVAL;
}
           

代碼根據和QEMU的約定,從傳遞傳遞下來的中斷資訊中解析三個東西:中斷類型,CPU号和中斷号;前面提到GIC中斷控制器可以在核心态實作實作,也可以在使用者态實作,如果不是核心模拟的中斷控制器,直接傳回,SPI類型中斷隻支援核心模拟的中斷控制器。

vGIC到CPU interface

Distributor

資料結構

  • GIC中主要資料結構是Distributor,他模拟nr_spis個spis中斷的輸入引腳,控制輸入中斷應該到達的target_cpu
struct vgic_dist {
    bool            in_kernel;
    bool            ready;
    bool            initialized;
    /* vGIC model the kernel emulates for the guest (GICv2 or GICv3) */
    u32         vgic_model;
    /* Do injected MSIs require an additional device ID? */
    bool            msis_require_devid;
    int         nr_spis;
    /* TODO: Consider moving to global state */
    /* Virtual control interface mapping */
    void __iomem        *vctrl_base;
    /* base addresses in guest physical address space: */
    gpa_t           vgic_dist_base;     /* distributor */
    union {
        /* either a GICv2 CPU interface */
        gpa_t           vgic_cpu_base;
        /* or a number of GICv3 redistributor regions */
        struct {
            gpa_t       vgic_redist_base;
            gpa_t       vgic_redist_free_offset;
        };
    };
    /* distributor enabled */
    bool            enabled;
    struct vgic_irq     *spis;
    struct vgic_io_device   dist_iodev;
    bool            has_its;
    /*
     * Contains the attributes and gpa of the LPI configuration table.
     * Since we report GICR_TYPER.CommonLPIAff as 0b00, we can share
     * one address across all redistributors.
     * GICv3 spec: 6.1.2 "LPI Configuration tables"
     */
    u64         propbaser;
    /* Protects the lpi_list and the count value below. */
    spinlock_t      lpi_list_lock;
    struct list_head    lpi_list_head;
    int         lpi_list_count;
    /* used by vgic-debug */
    struct vgic_state_iter *iter;
};
           
  • Distributor的spis數組中,每個vgic_irq模拟一個spis中斷輸入引腳,每個vgic_irq指向一個目标CPU
struct vgic_irq {
    struct list_head ap_list; 	/* 1 */
    struct kvm_vcpu *vcpu;      /* 2: SGIs and PPIs: The VCPU
                     			 * SPIs and LPIs: The VCPU whose ap_list
                     		     * this is queued on.
                                 */
    struct kvm_vcpu *target_vcpu;   /* 3: The VCPU that this interrupt should
                     				 * be sent to, as a result of the
                     				 * targets reg (v2) or the
                     			     * affinity reg (v3).
                     			     */
    u32 intid;          /* 4: Guest visible INTID */
    bool line_level;        /* 5: Level only */
    bool active;            /*6: not used for LPIs */
    bool enabled;
};
           
1. 中斷挂入的CPU連結清單
2. 該中斷最終投遞的CPU,所有需要被CPU處理的中斷都挂接到ap_list連結清單,該連結清單由每個CPU維護
3. GIC根據硬體寄存器或者親和性寄存器擷取的使用者配置資訊,得到的中斷目标CPU。在使用者沒有配置的情況下,有一個預設目标CPU,就是CPU0。
4. 中斷号,因為是SPI中斷,是以大于32
5. 中斷是否電平觸發
6. 中斷狀态位,标記中斷是否正在被處理
           
  • GIC是以隻處理SPI類型的中斷,原因是其它兩類中斷的輸入就是針對特定一個CPU的,不需要Distributor控制其中斷信号的deliver行為;而SPI的目标CPU,是可以使用者配置的,是以需要模拟一個Distributor來控制中斷deliver的目标,并将Distributor的控制接口暴露給使用者。
  • Q&A

    Q:為什麼一個中斷結構裡面設計了兩個vCPU結構,理論上一個中斷投遞到一個vGPU就夠了呀?

    A:target_vcpu結構用來存放使用者設定的GIC中斷路由資訊,如果使用者沒有設定,那target_vcpu就使用預設的CPU0,後續GIC可能會根據負載均衡政策将中斷分發到其它目标CPU上。換句話說,target_vcpu可能不是中斷最終投遞的CPU,隻是一個初始值,而vcpu才是中斷最終投遞的CPU

初始化

  • Distributor的初始化在GIC的初始化完成
vgic_init
	if (!dist->nr_spis)						/* 1 */
		dist->nr_spis = VGIC_NR_IRQS_LEGACY - VGIC_NR_PRIVATE_IRQS
	kvm_vgic_dist_init(kvm, dist->nr_spis)	/* 2 */
		dist->spis = kcalloc(nr_spis, sizeof(struct vgic_irq), GFP_KERNEL)		/* 3 */
		for (i = 0; i < nr_spis; i++) {											/* 4 */
        	struct vgic_irq *irq = &dist->spis[i];
        	/* u32 intid;          /* Guest visible INTID */
        	irq->intid = i + VGIC_NR_PRIVATE_IRQS;
        	INIT_LIST_HEAD(&irq->ap_list);
        	spin_lock_init(&irq->irq_lock);
        	irq->vcpu = NULL;													/* 5 */
        	irq->target_vcpu = vcpu0;											/* 6 */
        	kref_init(&irq->refcount);
        	if (dist->vgic_model == KVM_DEV_TYPE_ARM_VGIC_V2)
            	irq->targets = 0;
        	else
            	irq->mpidr = 0;
    	}
           
1. 首先确認輸入SPI中斷引腳個數,如果使用者沒有指定,設定為最大中斷個數減去GIC規定的CPU私有中斷個數:
VGIC_NR_IRQS_LEGACY - VGIC_NR_PRIVATE_IRQS
2. 初始化分發器,每個輸入引腳建立一個連接配接到CPU Interface的中斷vgic_irqGIC控制器主要由三大部分組成:
分發器(distributor),CPU 接口和虛拟化的CPU Interface,硬體上一個GIC控制器的分發器,處理所有引腳上的中斷,根據使用者配置将其路由到正确的CPU Interface。如果把GIC分發器看成黑盒,它的輸入是nr_spis個中斷引腳,輸出是CPU的index,軟體模拟分發器的原則,保證相同的輸入,軟體有相同的CPU index輸
出就可以了。不需要保證内部工作原理相同,是以KVM模拟GIC,針對每個輸入引腳都建立了一個vgic_irq的對象,vgic_irq中的輸出是target_vcpu,這個值是可配置
的,使用者修改vgic_irq這個結構體就可以決定将輸入的中斷路由到哪個vcpu可以說,每個vgic_irq都是針對一個中斷輸入的配置項
3. 為GIC的每個外部中斷輸入引腳都建立一個vgic_irq資料結構
4. 初始化GIC引腳的每一個vgic_irq
5. 中斷最終要投遞的目标CPU,初始化階段無法确定最終的CPU,是以為空
6. 使用者配置的目标CPU,預設值為CPU0
           
  • 首先确認輸入SPI中斷引腳個數,如果使用者沒有指定,設定為最大中斷個數減去GIC規定的CPU私有中斷個數,

    VGIC_NR_IRQS_LEGACY - VGIC_NR_PRIVATE_IRQS

    ;每個輸入引腳建立一個連接配接到CPU Interface的中斷vgic_irq,GIC控制器主要由三大部分組成:分發器(distributor),CPU 接口和虛拟化的CPU Interface;

    硬體上一個GIC控制器的分發器,處理所有引腳上的中斷,根據使用者配置将其路由到正确的CPU Interface,如果把GIC分發器看成黑盒,它的輸入是nr_spis個中斷引腳,輸出是CPU的index,軟體模拟分發器的原則,保證相同的輸入,軟體有相同的CPU index輸出就可以了。不需要保證内部工作原理相同。

    是以KVM模拟GIC,針對每個輸入引腳都建立了一個vgic_irq的對象,vgic_irq中的輸出是target_vcpu,這個值是可配置的,使用者修改vgic_irq這個結構體就可以決定将輸入的中斷路由到哪個vcpu,可以說,每個vgic_irq都是針對一個中斷輸入的配置項

  • Distributor的初始化就是為每個中斷引腳建立配置項,預設情況,如果使用者不配置,所有的SPI中斷都會被分發到第0個CPU。

路由表

  • KVM模拟GIC的分發器,本質上已經實作中斷的路由,使用者通過配置分發器的vgic_irq,就可以控制每個引腳的中斷資訊deliver到哪個CPU。但KVM在真正路由時查詢的不是分發器的每個vgic_irq,而是查詢一個通用的路由表kvm_irq_routing_table,為什麼?因為移植性,vgic_dist是KVM針對ARM平台的中斷控制器設計的分發器對象,KVM如果想通過查找分發器的每個irq來路由中斷,那勢必需要調用這種分發器的接口,這樣就和具體的硬體強綁定了。為了避免這種情況,需要設計一張通用路由表(

    kvm_irq_routing_table

    )來提供中斷路由功能,KVM根據這張表來路由中斷,這張路由表的配置接口(

    kvm_set_irq_routing

    )是通用的,但傳入的每個配置項由平台硬體決定。
  • 中斷路由表資料結構
struct kvm_irq_routing_table {
    int chip[KVM_NR_IRQCHIPS][KVM_IRQCHIP_NUM_PINS];		/* 1 */
    u32 nr_rt_entries;										/* 2 */
    /*
     * Array indexed by gsi. Each entry contains list of irq chips
     * the gsi is connected to.
     */
    struct hlist_head map[0];								/* 3 */
};
           
1. 每個KVM都有一個中斷路由表,chip是個二維資料,一維元素`chip[KVM_NR_IRQCHIPS]`表示晶片個數,二維元素
chip[KVM_NR_IRQCHIPS][KVM_IRQCHIP_NUM_PINS]表示每個晶片的管腳。二維數組存儲的值是一個索引(gsi),用于在map中索引該晶片引腳對應的中斷向
量表項。這裡的晶片數量和管腳數量都是體系結構相關的,比如ARM下晶片數量是1,管腳個數是256。x86下晶片數量是3(PIC slave,PIC master,IOAPIC),管
腳個數是晶片中引腳的最大值(IOAPIC)24。
2. 路由表條目數量
3. 路由表的扁平結構,存放所有的路由表項,chip存放的是晶片引腳在map中的索引,map存放路由表,通過這兩個結構就可以檢視到一個中斷控制器的引腳對應的是哪條路由表,無論這個中斷控制器有幾個晶片,幾個管腳。設計很巧妙啊。下面分析ARM平台和x86平台各自的路由表結構
           

路由表項

  • 配置用的臨時資料結構
struct kvm_irq_routing_irqchip {
    __u32 irqchip;						/* 1 */
    __u32 pin;							/* 2 */
};

struct kvm_irq_routing_entry {
    __u32 gsi;							/* 3 */
    /* gsi routing entry types 
	 * #define KVM_IRQ_ROUTING_IRQCHIP 1
	 * #define KVM_IRQ_ROUTING_MSI 2
	 * /
    __u32 type;							/* 4 */
    __u32 flags;
    __u32 pad;
    union {
        struct kvm_irq_routing_irqchip irqchip;
        struct kvm_irq_routing_msi msi;
        __u32 pad[8];
    } u;
}; 
           
1. 在kvm_irq_routing_table二維數組中的一維索引
2. 中斷引腳在晶片的第幾根pin的索引 
3. 在map數組中的索引
4. 路由項的類型,通常是第1個
           

預設配置

ARM

  • ARM使用GICv2控制器,晶片數量1個,GICv2規範定義其中斷号範圍在0-1019。前32個用作私有中斷号,還剩餘1020-32=988個,是以GICv2控制器理論上可以響應988個外部中斷,其晶片最大可以988個管腳。但kvm初始化時隻用了256,看解釋時為了向前相容。預設路由表初始化代碼如下:
int vgic_init(struct kvm *kvm) {
	.....
	/* 這裡GICv2先組裝一個路由表的數組,用于設定通用路由表 
	 * ARM GICv2路由表初始值很簡單,數組的索引,路由表的gsi和晶片的引腳号,都是一個值
	 */
	kvm_vgic_setup_default_irq_routing(kvm)						/* 1 */
		u32 nr = dist->nr_spis;
	    struct kvm_irq_routing_entry *entries;
	    entries = kcalloc(nr, sizeof(*entries), GFP_KERNEL);	/* 2 */
		
	    for (i = 0; i < nr; i++) {
        	entries[i].gsi = i;									/* 3 */
        	entries[i].type = KVM_IRQ_ROUTING_IRQCHIP;			/* 4 */
        	entries[i].u.irqchip.irqchip = 0;					/* 5 */
        	entries[i].u.irqchip.pin = i;						/* 6 */
    	}
    	/* 傳入準備好的數組,設定通用路由表 */
    	kvm_set_irq_routing(kvm, entries, nr, 0);
}
           
1. 設定預設的路由表
2. 首先根據GIC中斷引腳的個數,生成一個臨時的路由配置結構,它包含了所有引腳的預設路由資訊
3. 設定中斷引腳在全局路由map中的索引,由于GIC控制器隻有一個晶片,是以和map一樣,是扁平的結構,每個條目就對應相應的一個map中的gsi号,并且初始化時被設定為一一對應
4. 設定路由條目的類新
5. 設定引腳所在晶片,因為晶片隻有一個,就是第一個晶片
6. 設定引腳
           
  • ARM中斷控制器連接配接示意圖如下
    KVM中斷控制器模拟QEMU裝置到vGICvGIC到CPU interfaceCPU interface到CPU(Deliver)

X86

  • X86平台使用PIC加IOAPIC的兩種晶片的組合,來控制中斷資訊。PIC晶片一主一從2塊,IOAPIC晶片1塊,PIC主從晶片分别8個管腳,共16個管腳。IOAPIC晶片24個管腳。一共40個管腳。兩種晶片共存的情況下,為了讓IOAPIC相容PIC,IOAPIC的前16個管腳于PIC的主從晶片共享一個中斷引腳,對,共享中斷,這個在ARM平台上不存在,因為ARM的中斷引腳多,而X86上一個IOAPIC的管腳最多24個,多個中斷源必須充分利用有限的中斷引腳,是以需要共享中斷。下面是X86平台預設的中斷路由表配置項,從中可以看出X86平台模拟的硬體接線方式
#define IOAPIC_ROUTING_ENTRY(irq) \
    { .gsi = irq, .type = KVM_IRQ_ROUTING_IRQCHIP,  \
      .u.irqchip = { .irqchip = KVM_IRQCHIP_IOAPIC, .pin = (irq) } }
#define ROUTING_ENTRY1(irq) IOAPIC_ROUTING_ENTRY(irq)

#define PIC_ROUTING_ENTRY(irq) \
    { .gsi = irq, .type = KVM_IRQ_ROUTING_IRQCHIP,  \
      .u.irqchip = { .irqchip = SELECT_PIC(irq), .pin = (irq) % 8 } }
#define ROUTING_ENTRY2(irq) \
    IOAPIC_ROUTING_ENTRY(irq), PIC_ROUTING_ENTRY(irq)

static const struct kvm_irq_routing_entry default_routing[] = {
    ROUTING_ENTRY2(0), ROUTING_ENTRY2(1),
    ROUTING_ENTRY2(2), ROUTING_ENTRY2(3),
    ROUTING_ENTRY2(4), ROUTING_ENTRY2(5),
    ROUTING_ENTRY2(6), ROUTING_ENTRY2(7),
    ROUTING_ENTRY2(8), ROUTING_ENTRY2(9),
    ROUTING_ENTRY2(10), ROUTING_ENTRY2(11),
    ROUTING_ENTRY2(12), ROUTING_ENTRY2(13),
    ROUTING_ENTRY2(14), ROUTING_ENTRY2(15),
    ROUTING_ENTRY1(16), ROUTING_ENTRY1(17),
    ROUTING_ENTRY1(18), ROUTING_ENTRY1(19),
    ROUTING_ENTRY1(20), ROUTING_ENTRY1(21),
    ROUTING_ENTRY1(22), ROUTING_ENTRY1(23),
};

int kvm_setup_default_irq_routing(struct kvm *kvm)
{
    return kvm_set_irq_routing(kvm, default_routing,
                   ARRAY_SIZE(default_routing), 0);
}
           
  • 首先看前16個中斷号對應的路表項,以第0,9,17為例展開宏
1. ROUTING_ENTRY2(0)	=>	IOAPIC_ROUTING_ENTRY(0), PIC_ROUTING_ENTRY(0)
    { .gsi = 0,											/* 在路由表map數組中的索引 */
      .type = KVM_IRQ_ROUTING_IRQCHIP,					/* 路由表項類型為控制器晶片路由項 */
      .u.irqchip = { .irqchip = KVM_IRQCHIP_IOAPIC,		/* IOAPIC控制器晶片*/
      					   .pin = 0 } 					/* 中斷引腳在IOAPIC第0根 */
  	},
    { .gsi = 0, 										/* 在路由表map數組中的索引 */						
      .type = KVM_IRQ_ROUTING_IRQCHIP,
      .u.irqchip = { .irqchip = KVM_IRQCHIP_PIC_MASTER, // PIC Master控制器晶片
      			 	 .pin = 0 }							// 中斷引腳在PIC Master第0根
   	}
2.ROUTING_ENTRY2(9)	=>	IOAPIC_ROUTING_ENTRY(9), PIC_ROUTING_ENTRY(9)
    { .gsi = 9,
      .type = KVM_IRQ_ROUTING_IRQCHIP,					// 路由表項類型為控制器晶片路由項
      .u.irqchip = { .irqchip = KVM_IRQCHIP_IOAPIC,		// IOAPIC控制器晶片
      					   .pin = 9 } 					// 中斷引腳在IOAPIC第9根
  	},
    { .gsi = 9, 									
      .type = KVM_IRQ_ROUTING_IRQCHIP,
      .u.irqchip = { .irqchip = KVM_IRQCHIP_PIC_SLAVE,  // PIC Slave控制器晶片
      			 	 .pin = 1 }							// 中斷引腳在PIC Slave第1根
   	}
3.ROUTING_ENTRY1(17)	=>	IOAPIC_ROUTING_ENTRY(17)
    { .gsi = 17,
      .type = KVM_IRQ_ROUTING_IRQCHIP,					// 路由表項類型為控制器晶片路由項
      .u.irqchip = { .irqchip = KVM_IRQCHIP_IOAPIC,		// IOAPIC控制器晶片
      					   .pin = 17 } 					// 中斷引腳在IOAPIC第17根
           
  • 從預設配置的路由項可以看出X86平台對40個管腳的中斷輸入配置設定情況,40個管腳占用了24個中斷号,PIC Master[0-7],PIC Slave[8-15],IOAPIC[0-23],其中IOAPIC的[0-15] PIN 基于與PIC相容的原因,與PIC共用一個中斷引腳,以中斷共享的方式使用這個引腳。連接配接如下圖所示
    KVM中斷控制器模拟QEMU裝置到vGICvGIC到CPU interfaceCPU interface到CPU(Deliver)

配置

  • 體系結構相關的控制器準備好路由表之後,就調用通用接口設定通用路由表,ARM平台X86平台調用的函數相同,走的流程一樣
kvm_set_irq_routing
	/* 先擴充路由表項,對于ARM來說,gsi号和條目錄數一樣,不需要擴充
	 * 對于X86來說,gsi号會小于條目号,從宏ROUTING_ENTRY2(0)可以看出
	 * gsi為0的路由項就對應兩個路由條目,x86的初始化路由項一共有40項
	 * */
	for (i = 0; i < nr; ++i) {
        if (ue[i].gsi >= KVM_MAX_IRQ_ROUTES)
            return -EINVAL;
        nr_rt_entries = max(nr_rt_entries, ue[i].gsi);
    }
	for (i = 0; i < nr; ++i) 
		/* 逐條時值路由表的值 */
		setup_routing_entry(kvm, new, e, ue)
			e->gsi = gsi;	// 設定gsi
			e->type = ue->type;	// 設定路由表條目的類型KVM_IRQ_ROUTING_IRQCHIP
			r = kvm_set_routing_entry(kvm, e, ue);
			/* 将gsi存放到晶片的引腳chip[e->irqchip.irqchip][e->irqchip.pin]上
			 * 通過引腳存放的gsi可以在扁平化的map數組中找到中斷資訊
			 * ARM的情況比較簡單,gsi号和第1塊晶片的引腳一一對應
			 * X86的情況稍複雜,假設gsi是9,那麼第9條路由項對應的晶片是第1塊晶片(從0開始)
			 * PIC Slave,對應的晶片引腳是第1根引腳(從0開始)
			 */
			if (e->type == KVM_IRQ_ROUTING_IRQCHIP)
        		rt->chip[e->irqchip.irqchip][e->irqchip.pin] = e->gsi;
        	/* 将路由表項作為節點連接配接到map數組的對應元素中 */
        	hlist_add_head(&e->link, &rt->map[e->gsi])
           
  • 通用路由表結構如下,chip存放的是
    KVM中斷控制器模拟QEMU裝置到vGICvGIC到CPU interfaceCPU interface到CPU(Deliver)
  • 由于各體系結構給路由表傳入的配置參數不同,生成的路由表雖在結構上一樣,但具體布局有所差别,ARM平台和X86平台初始化配置後的通用路由表分别如下
    KVM中斷控制器模拟QEMU裝置到vGICvGIC到CPU interfaceCPU interface到CPU(Deliver)
  • ARM隻有一個晶片,晶片引腳,GSI号一一對應,并且一個中斷号對應一個裝置,map指向的連結清單節點數為1。沒有共享中斷的情況。
    KVM中斷控制器模拟QEMU裝置到vGICvGIC到CPU interfaceCPU interface到CPU(Deliver)
  • X86有多個晶片,多個晶片的管腳有共享一個中斷輸入的情況。[0-7]号中斷由PIC Master和IOAPIC共享,[8-15]号中斷由PIC Slave和IOAPIC共享,對應的每個map元素指向的連結清單節點數都是2。
  • 在設定路由表時,針對每一項都進行了路由設定,除了配置引腳和map的對應關系,還有一個重要的流程就是設定每項路由表的中斷響應回調,這個在

    kvm_set_routing_entry

    中實作,ARM平台比較簡單,隻有一個中斷控制器,其回調就是

    vgic_irqfd_set_irq

    ->

    kvm_vgic_inject_irq

    ,X86平台有共享中斷的情況,比較複雜,對于同一各中斷,可能有多個控制器要響應。每個控制器的響應回調都會觸發,PIC對應的是

    kvm_set_pic_irq

    ,IOAPIC對應的是

    kvm_set_ioapic_irq

ARM:
kvm_set_routing_entry
	e->set = vgic_irqfd_set_irq;
X86:
kvm_set_routing_entry
    switch (ue->type) {
    case KVM_IRQ_ROUTING_IRQCHIP:
        if (irqchip_split(kvm))
            return -EINVAL;
        e->irqchip.pin = ue->u.irqchip.pin;
        switch (ue->u.irqchip.irqchip) {
        case KVM_IRQCHIP_PIC_SLAVE:
            e->irqchip.pin += PIC_NUM_PINS / 2;
            /* fall through */
        case KVM_IRQCHIP_PIC_MASTER:
            if (ue->u.irqchip.pin >= PIC_NUM_PINS / 2)
                return -EINVAL;
            e->set = kvm_set_pic_irq;
            break;
        case KVM_IRQCHIP_IOAPIC:
            if (ue->u.irqchip.pin >= KVM_IOAPIC_NUM_PINS)
                return -EINVAL;
            e->set = kvm_set_ioapic_irq;
            break;
        default:
            return -EINVAL;
        }
        e->irqchip.irqchip = ue->u.irqchip.irqchip;
        break;
           

CPU interface

  • 自此,再回到最初核心發起中斷注入的地方

    kvm_vm_ioctl_irq_line

    ,分别分析ARM和X86發現兩者有不同的地方:ARM平台雖然設定了中斷路由表,但是對于SPI類型的中斷,它注入過程中沒有用到,SPI類型的中斷預設就發往了CPU 0上。X86平台在中斷到達IOAPIC之前,是查詢了中斷路由表的。下面分别分析:

ARM

kvm_vm_ioctl
	kvm_vm_ioctl_irq_line
	    irq_type = (irq >> KVM_ARM_IRQ_TYPE_SHIFT) & KVM_ARM_IRQ_TYPE_MASK;	/* SPI 類型 */
    	vcpu_idx = (irq >> KVM_ARM_IRQ_VCPU_SHIFT) & KVM_ARM_IRQ_VCPU_MASK;	/* vcpu_idx: 0*/
    	irq_num = (irq >> KVM_ARM_IRQ_NUM_SHIFT) & KVM_ARM_IRQ_NUM_MASK;		/* 中斷号:32+7 = 39 */
    	
   	case KVM_ARM_IRQ_TYPE_CPU:	/* 發往特定CPU上的快速中斷 */
        if (irqchip_in_kernel(kvm))
            return -ENXIO;

        if (vcpu_idx >= nrcpus)
            return -EINVAL;

        vcpu = kvm_get_vcpu(kvm, vcpu_idx);	/* 根據cpuid取出vcpu結構體*/
        if (!vcpu)
            return -EINVAL;

        if (irq_num > KVM_ARM_IRQ_CPU_FIQ)
            return -EINVAL;
		/* 立即投遞到cpu的中斷狀态字段,然後kick cpu進行處理
		 * 由于是快速中斷,KVM直接更新的irq_lines字段,沒有将中斷信号放到vgic_cpu的ap_list上排隊
		 */
        return vcpu_interrupt_line(vcpu, irq_num, level);		
    case KVM_ARM_IRQ_TYPE_PPI:	/* CPU私有類型的中斷 */
        if (!irqchip_in_kernel(kvm))
            return -ENXIO;

        if (vcpu_idx >= nrcpus)
            return -EINVAL;

        vcpu = kvm_get_vcpu(kvm, vcpu_idx);	/* 根據cpuid取出vcpu結構體 */
        if (!vcpu)
            return -EINVAL;

        if (irq_num < VGIC_NR_SGIS || irq_num >= VGIC_NR_PRIVATE_IRQS)
            return -EINVAL;
		/* 非快速中斷,取出目的vcpu後,将中斷信号放到vcpu的ap_list字段排隊,等待vcpu處理 */
        return kvm_vgic_inject_irq(kvm, vcpu->vcpu_id, irq_num, level, NULL);	
    case KVM_ARM_IRQ_TYPE_SPI:
        if (!irqchip_in_kernel(kvm))
            return -ENXIO;

        if (irq_num < VGIC_NR_PRIVATE_IRQS)
            return -EINVAL;
		/* 非快速中斷,SPI預設發送vcpu 0上,同樣将中斷信号放到vcpu的ap_list字段排隊,等待vcpu處理 */
        return kvm_vgic_inject_irq(kvm, 0, irq_num, level, NULL);
    }
           

X86

  • X86平台PIC對應的中斷信号處理函數

    kvm_set_pic_irq

    ,IOAPIC對應的中斷信号處理函數

    kvm_set_ioapic_irq

    。首先根據gsi從路由表中的查到每條路由表項,如果同一個gsi有多個表項,表示有多個裝置共享同一個中斷,分别調用它們的set回調,注入中斷:
int kvm_set_irq(struct kvm *kvm, int irq_source_id, u32 irq, int level,
        bool line_status)
{       
    struct kvm_kernel_irq_routing_entry irq_set[KVM_NR_IRQCHIPS];
    int ret = -1, i, idx;
                       
    trace_kvm_set_irq(irq, level, irq_source_id);
        
    /* Not possible to detect if the guest uses the PIC or the
     * IOAPIC.  So set the bit in both. The guest will ignore
     * writes to the unused one.
     */
    idx = srcu_read_lock(&kvm->irq_srcu);
    i = kvm_irq_map_gsi(kvm, irq_set, irq);
    srcu_read_unlock(&kvm->irq_srcu, idx);
    
    while (i--) {
        int r;
        r = irq_set[i].set(&irq_set[i], kvm, irq_source_id, level,
                   line_status);
        if (r < 0)
            continue;

        ret = r + ((ret < 0) ? 0 : ret);
    }

    return ret;
}
           

CPU interface到CPU(Deliver)

  • 前面在kvm_vgic_inject_irq函數中,通過vgic_get_irq擷取到了。vgic_irq擷取到之後,這一行代碼執行完之後,表示中斷已經到達vGIC,因為它拿到了vgic_irq,可以通過它deliver中斷到CPU了。這一節介紹怎麼通過vgic_irq deliver中斷到CPU。回到kvm_vgic_inject_irq函數,繼續分析vgic_queue_irq_unlock
vgic_queue_irq_unlock(kvm, irq)
	/* 首先查詢vgic_irq應該deliver到哪個CPU */
	vcpu = vgic_target_oracle(irq)	
		/* f the interrupt is active, it must stay on the current vcpu
		 * 如果中斷的狀态是active的,說明我們目前處理的CPU已經被deliver到CPU了 
		 * 因為隻有當中斷被投遞到CPU,GIC才會把中斷的狀态設定成active
		 * 這時候,我們要deliver的cpu,首選vcpu成員,因為目前中斷已經在該vcpu的ap_list上了
		 * 投遞到vcpu上的中斷如果有多個,KVM會将這些中斷組織起來放到ap_list連結清單上
		 * 如果vcpu不存在,就傳回target_vcpu,這是vGIC路由之後算出來的結果
		 * 它表示中斷下一步應該deliver的那個vcpu
		 **/
		 if (irq->active)
        	return irq->vcpu ? : irq->target_vcpu
     	/*
     	 * If the IRQ is not active but enabled and pending, we should direct
     	 * it to its configured target VCPU.
     	 * If the distributor is disabled, pending interrupts shouldn't be
     	 * forwarded.
     	 * 如果irq沒有被禁止并且處于pending狀态,如果控制器被禁用,就不允許将pending态的中斷deliver了
     	 */
    	if (irq->enabled && irq_is_pending(irq)) {
        	if (unlikely(irq->target_vcpu &&
                 		!irq->target_vcpu->kvm->arch.vgic.enabled))	// 控制器被禁止,enable狀态使用者态程式可以修改 
            	return NULL;    
    		/* 如果一切正常,将vGIC路由出來的vcpu傳回,告訴調用者應該将中斷deliver到這個vcpu */
        	return irq->target_vcpu;	
    	}
    	/* 如果中斷既沒有pending,或者被禁用,我們也不應該deliver這個中斷,enable狀态使用者态程式可以修改 */
    	/* If neither active nor pending and enabled, then this IRQ should not
     	 * be queued to any VCPU.
     	 */
    	return NULL;
           
  • 到這裡,我們根據vgic_irq路由到了我們中斷需要deliver的CPU,這裡隻是擷取路由資訊,其實就是判斷一下中斷的狀态,将vgic_irq裡面的vcpu或者target_vcpu取出來。路由資訊在使用者下發配置或者初始化時已經确定。擷取vcpu之後,就是往vcpu上送出request irq的請求了。
vgic_queue_irq_unlock
	vcpu = vgic_target_oracle(irq);
    if (irq->vcpu || !vcpu) {
        /*
         * If this IRQ is already on a VCPU's ap_list, then it
         * cannot be moved or modified and there is no more work for
         * us to do.
         *
         * Otherwise, if the irq is not pending and enabled, it does
         * not need to be inserted into an ap_list and there is also
         * no more work for us to do.
         */
        spin_unlock(&irq->irq_lock);

        /*
         * We have to kick the VCPU here, because we could be
         * queueing an edge-triggered interrupt for which we
         * get no EOI maintenance interrupt. In that case,
         * while the IRQ is already on the VCPU's AP list, the
         * VCPU could have EOI'ed the original interrupt and
         * won't see this one until it exits for some other
         * reason.
         */
        if (vcpu) {
            kvm_make_request(KVM_REQ_IRQ_PENDING, vcpu);
            kvm_vcpu_kick(vcpu);
        }
        return false;
    }
           

irq的vcpu成員表示中斷已經deliver到cpu,處于active狀态,但其實本中斷還沒有deliver,出現這種情況隻有一種場景:上一次的中斷deliver之後,cpu沒有EOI回應GIC,導緻GIC每有将中斷設定成drop狀态。是以這裡再次設定中斷請求,并提醒CPU去處理。繼續往下走,分析正常的中斷注入流程。

list_add_tail(&irq->ap_list, &vcpu->arch.vgic_cpu.ap_list_head);
    irq->vcpu = vcpu;

    kvm_make_request(KVM_REQ_IRQ_PENDING, vcpu);
    kvm_vcpu_kick(vcpu)
           

将irq将入到vcpu中斷處理連結清單中,設定vcpu為active狀态,發起中斷請求。kvm_make_request和kvm_vcpu_kick涉及到硬體操作,繼續分析需要進一步檢視ARM手冊,而中斷注入原理類似,是以這裡的介紹保留。有興趣的同學可以繼續往下分析。下一章會以X86為例,結合Intel手冊和中斷控制器的datasheet,分析X86平台下的中斷注入硬體部分。

繼續閱讀