侧边栏壁纸
博主头像
蔚然小站博主等级

未来会有的,不要辜负了梦想

  • 累计撰写 35 篇文章
  • 累计创建 14 个标签
  • 累计收到 9 条评论

目 录CONTENT

文章目录

中断虚拟化:3.APIC虚拟化

皮蛋熊
2023-08-27 / 0 评论 / 0 点赞 / 178 阅读 / 13737 字
温馨提示:
本文最后更新于 2023-08-27,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

APIC包含有LAPIC(内核CORE里面都有,但不清楚是否是每个线程(超线程CPU)里面都有)

Pasted image 20220722111651

I/O APIC

  • 一般位于南桥中
  • 响应外部设备的中断,并将中断发给LAPIC,然后由LAPIC发给对应的CPU

LAPIC

  • 内核CORE里面都有,但不清楚是否是每个线程(超线程CPU)里面都有。
  • CPU核间通讯的唯一手段

LAPIC和I/O APIC的连接方式:

  • 早期是专用的线连接(3-wire APIC Bus)
  • 随着核心数量的增加,改为系统总线的连接方式,每增加一个核心,就是在总线上多挂一个LAPIC,不再受管教数量的约束

Pasted image 20220722114147

专用线连接演进为系统总线连接

Pasted image 20220722114157

当I/O APIC收到设备的中断请求的时候,会通过寄存器决定将中断发送给哪个LAPIC(CORE)。其中I/O APIC寄存器如图所示:

Pasted image 20220722114319

其中0x10~0x3F,有24个64位寄存器,对应着I/O APIC的24个中断引脚,这24个64位寄存器组成了中断重定向表(Redirection Table),重定向表使用管脚号作为索引,比如IR0号管脚对应第一个重定向表项。

Pasted image 20220722150104

  • destination表示中断发送目标的CPU(可能是一个,也可能是一组),设置中断负载均衡(中断亲和性),设置的就是destination字段。
  • interrupt vector:中断向量,用于在IDT中索引中断服务程序。

OS初始化->I/O APIC写入数据(设置重定向各个表项) OS运行时->I/O APIC写入数据(通过proc文件,设置中断亲和性Affinity),更新重定向表。

中断发生->I/0 APIC查询重定向表,并将中断发往OS预先设置的目标CPU->LAPIC接收到中断,并开始处理。

外设的中断过程

外设中断过程.drawio

外设调用虚拟I/O APIC接口->虚拟IO APIC接收到中断请求->以中断请求号作为索引,查找中断重定向表->中断重定向表项des值决定将值发送到哪个CPU的LAPIC。

虚拟LAPIC收到请求后,首先设置IRR寄存器,然后唤醒正在睡眠的VCPU,或者触发正在运行的Guest退出到Host模式,下一次VM entry时评估虚拟LAPIC中的中断,执行注入过程。

Pasted image 20220722154929

IRR:Interrupt Request Register,中断请求寄存器;请求中断的外设在IRR对应的位中会置为1.当有多个中断请求,IRR会有多位被置为1,相当于维持了一个中断请求序列。

linux.git/arch/x86/kernel/apic/io_apic.c
static void __ioapic_write_entry(int apic, int pin, struct IO_APIC_route_entry e)
{
	union entry_union eu;
	eu.entry = e;
	io_apic_write(apic, 0x11 + 2*pin, eu.w2);
	io_apic_write(apic, 0x10 + 2*pin, eu.w1);
}
static inline void io_apic_write(unsigned int apic,
unsigned int reg, unsigned int value)
{
	struct io_apic __iomem *io_apic = io_apic_base(apic);
	writel(reg, &io_apic->index);
	writel(value, &io_apic->data);
}

io_apic_write的三个参数

  • apic:
  • reg:是寄存器地址(相对比基址的偏移);0x10,是IO APIC重定向开始的偏移,对于第1个管脚,pin值为0,0x10,0x11恰好是第一个entry;对于第2个管脚,pin值为2,0x12,0x13对应第二个entry。
  • value:要写入的内容,eu.w2是entry的低32位;eu.w1是entry的高32位。

对于中断重定向表中的entry(寄存器),只能通过间接访问。CPU地址空间映射了两个寄存器IOREGSEL(I/O Register Select)和IOWIN(I/O Window)。CPU访问entry需要通过他们,具体方式如下: 假设需要写入中断重定向表的第一个entry: CPU写入0x10到IOREGSEL,CPU写入32bit的value1到IOWIN(这就写入了entry1 的低32bit) CPU写入0x11到IOREGSEL,CPU写入32bit的value2到IOWIN(这就写入了entry1 的高32bit)

代码中的:

  • io_apic->index 是IOREGSEL
  • io_apic->data 是IOWIN

回到虚拟化中:虚拟I/O APIC接收到Guest内核I/O APIC填充中断重定向向量表的请求后,将中断向量、目标CPU、触发模式等中断信息记录到中断重定向表中。

file: arch/x86/kvm/ioapic.c

static int ioapic_mmio_write(struct kvm_vcpu *vcpu, struct kvm_io_device *this, gpa_t addr, int len, const void *val)

{
...
    addr &= 0xff;
    spin_lock(&ioapic->lock);
    switch (addr) {
    case IOAPIC_REG_SELECT:
        ioapic->ioregsel = data & 0xFF; /* 8-bit register */
        break;
    case IOAPIC_REG_WINDOW:
        ioapic_write_indirect(ioapic, data);
        break;
    default:
        break;
    }
    spin_unlock(&ioapic->lock);
    return 0;
}

static void ioapic_write_indirect(struct kvm_ioapic *ioapic, u32 val)
{
    unsigned index;
    bool mask_before, mask_after;
    int old_remote_irr, old_delivery_status;
    union kvm_ioapic_redirect_entry *e;

    switch (ioapic->ioregsel) {
    case IOAPIC_REG_VERSION:
        /* Writes are ignored. */
        break;
    case IOAPIC_REG_APIC_ID:
        ioapic->id = (val >> 24) & 0xf;
        break;
    case IOAPIC_REG_ARB_ID:
        break;
    default:
        index = (ioapic->ioregsel - 0x10) >> 1;
        if (index >= IOAPIC_NUM_PINS)
            return;
        index = array_index_nospec(index, IOAPIC_NUM_PINS);
        e = &ioapic->redirtbl[index];
        mask_before = e->fields.mask;
        /* Preserve read-only fields */
        old_remote_irr = e->fields.remote_irr;
        old_delivery_status = e->fields.delivery_status;
        if (ioapic->ioregsel & 1) {
            e->bits &= 0xffffffff;
            e->bits |= (u64) val << 32;
        } else {
            e->bits &= ~0xffffffffULL;
            e->bits |= (u32) val;
        }

        e->fields.remote_irr = old_remote_irr;
        e->fields.delivery_status = old_delivery_status;
		...
    }
}

中断过程

对于在用户空间的虚拟设备,通过ioctl接口向内核的KVM模块发送KVM__IRQ_LINE请求:

void kvm__irq_line(struct kvm *kvm, int irq, int level)
{
...
    if (ioctl(kvm->vm_fd, KVM_IRQ_LINE, &irq_level) < 0)
        die_perror("KVM_IRQ_LINE failed");
...
}

虚拟APIC处理中断的代码:

linux\virt\kvm\kvm_main.c

01 static long kvm_vm_ioctl(…)
02 {
03 …
04     case KVM_IRQ_LINE: {
05 …
06     if (irqchip_in_kernel(kvm)) {
07 …
08         kvm_ioapic_set_irq(kvm->vioapic,
09         irq_event.irq, irq_event.level);
10 …
11     }
12 …
13 }

LAPIC向Guest注入中断的过程于PIC本质上完全相同:8259A是收到外设发起的中断后,先在IRR寄存器中记录下来,然后再设置一个变量output表示由pending的中断,在下次VM-entry时,KVM会发起中断评估等一系列过程,最终将中断信息写入到VMCS的VM-entry interruption-information中。

当目标CPU处于Guest模式,或者vcpu正处于睡眠状态,为减少延迟,需要虚拟LAPIC kick(踢)一下目标vCPU,要么将其从Guest模式提出到Host模式,要么唤醒挂起的vCPU线程kvm_vcpu_kick

static int __apic_accept_irq(struct kvm_lapic *apic, int delivery_mode,
                 int vector, int level, int trig_mode,
                 struct dest_map *dest_map)
{
    int result = 0;
    struct kvm_vcpu *vcpu = apic->vcpu;

    trace_kvm_apic_accept_irq(vcpu->vcpu_id, delivery_mode,
                  trig_mode, vector);
    switch (delivery_mode) {
    case APIC_DM_LOWEST:
        vcpu->arch.apic_arb_prio++;
        /* fall through */
    case APIC_DM_FIXED:
        if (unlikely(trig_mode && !level))
            break;
        /* FIXME add logic for vcpu on reset */
        if (unlikely(!apic_enabled(apic)))
            break;
        result = 1;
        if (dest_map) {
            __set_bit(vcpu->vcpu_id, dest_map->map);
            dest_map->vectors[vcpu->vcpu_id] = vector;
        }
        if (apic_test_vector(vector, apic->regs + APIC_TMR) != !!trig_mode) {
            if (trig_mode)
                kvm_lapic_set_vector(vector,apic->regs + APIC_TMR);
            else
                kvm_lapic_clear_vector(vector,apic->regs + APIC_TMR);
        }
        if (kvm_x86_ops->deliver_posted_interrupt(vcpu, vector)) {
            kvm_lapic_set_irr(vector, apic);
            kvm_make_request(KVM_REQ_EVENT, vcpu);
            kvm_vcpu_kick(vcpu);
        }
        break;
	}
    ...
    return result;

}

核间中断过程

APIC page(4kb),APIC所有寄存器都存储在这个页面上。linux内核将APIC page映射到内核空间中,通过MMIO访问,当内核访问这些寄存器的时候,将触发Guest退出到Host的KVM模块中的vLAPIC。当Guest发送核间中断时,虚拟LAPIC确定目的CPU,向目的CPU所在的LAPIC发送核间中断,最后由目标vLAPIC完成向Guest的中断注入的过程。

Pasted image 20220722215926

vLAPIC处理函数为apic__mmio_write

linux.git/drivers/kvm/lapic.c  
static void apic_mmio_write(…)  
{  
… 
	case APIC_ICR:  
		/* No delay here, so we always clear the pending bit */  
		apic_set_reg(apic, APIC_ICR, val & ~(1 << 12));  
		apic_send_ipi(apic);  
		break;  
	case APIC_ICR2:  
		apic_set_reg(apic, APIC_ICR2, val & 0xff000000);  
		break;  
…  
}
static void apic_send_ipi(struct kvm_lapic *apic)  
{  
	u32 icr_low = apic_get_reg(apic, APIC_ICR);  
	u32 icr_high = apic_get_reg(apic, APIC_ICR2);  
	unsigned int dest = GET_APIC_DEST_FIELD(icr_high);  
	… 
	for (i = 0; i < KVM_MAX_VCPUS; i++) {  
		vcpu = apic->vcpu->kvm->vcpus[i];  
		… 
		if (vcpu->apic && apic_match_dest(vcpu, apic, short_hand, dest, dest_mode)) {  
		…  
		__apic_accept_irq(vcpu->apic, delivery_mode,  
			vector, level, trig_mode);  
		}  
	}  
…  
}

当Guest写的是LAPIC的ICR寄存器(64位),表明这个CPU向另外一个CPU(56-63存储目标CPU)发送核间中断,APIC_ICR\APIC_ICR2分别指ICR低32位/高32位寄存器,写高32位是指定目标vgpu关联的vLAPIC,写低32位时候会触发vLAPIC向vCPU发送核间中断。

Pasted image 20220722231333

当Guest写ICR低32位时,触发vLAPIC向目标CPU发送核间中断,apic_send_ipi。函数apic_send_ipi先从寄存器中读取目标CPU,然后遍历虚拟机的所有vCPU,如果有vCPU匹配成功,则向其发送IPI中断,目标vLAPIC调用函数__apic_accept_irq接收中断。

linux.git/drivers/kvm/lapic.c
static int __apic_accept_irq(struct kvm_lapic *apic, int delivery_mode, int vector, int level, int trig_mode)  
{  
… 
	switch (delivery_mode) {  
	case APIC_DM_FIXED:  
	case APIC_DM_LOWEST:  
… 
	if (apic_test_and_set_irr(vector, apic) && trig_mode) {  
… 
	kvm_vcpu_kick(apic->vcpu);  
…  
}

__apic_accept_irqkvm_apic_set_irq实现几乎完全相同,首先在IRR寄存器记录中断信息,为减少中断延迟,需要kick一下vcpu,唤醒CPU后下次VM entry时,KVM发起中断评估等一系列过程,最终将中断信息写入VMCS的字段VM-entry interruption-information。

IRQ routing

系统中存在虚拟8259A和vAPIC,当中断请求发生时候,系统是通过哪个进行中断注入呢?因为kvm无法检测guest是使用PIC或者APIC,因此guest会忽略没用的那个。

linux.git/drivers/kvm/kvm_main.c  
static long kvm_vm_ioctl(…)  
{  
… 
	case KVM_IRQ_LINE: {  
…  
		if (irq_event.irq < 16)  
		    kvm_pic_set_irq(pic_irqchip(kvm), …);  
			kvm_ioapic_set_irq(kvm->vioapic, …);  
…  
}

当管脚号小于16时,会同时写入kvm_pic_set_irqkvm_ioapic_set_irq,这段代码已经重构了,有IRQ routing方案。 IRQ routing 包含一个表,表格的每一项:

linux.git/include/linux/kvm_host.h  
struct kvm_kernel_irq_routing_entry {  
	u32 gsi;  
	void (*set)(struct kvm_kernel_irq_routing_entry *e,  
		struct kvm *kvm, int level);  
	union {  
		struct {  
			unsigned irqchip;  
			unsigned pin;  
		} irqchip;  
	};  
	struct list_head link;  
};

gsi:管脚号如IR0,IR1等 set:函数指针

当一个外设请求到来时,KVM会遍历这个表格,匹配gsi,匹配成功则调用set指向的函数,负责注入中断。

  • 对于使用PCI的外设:set指向虚拟PIC对外提供的发射中断接口
  • 对于使用APIC的外设:set指向vAPIC对外提供的发送中断的接口
  • 对于支持并启用了MSI的外设:set指向MSI实现的发送中断的接口 当KVM收到设备发来的中断时,不再区分PIC\APIC\MSI,统一调用kvm_set_irq,遍历IRQ routing表中的每一个表项,如此就实现了代码统一:
linux.git/arch/x86/kvm/x86.c  
long kvm_arch_vm_ioctl(…)  
{  
… 
	case KVM_IRQ_LINE: {  
…  
		kvm_set_irq(kvm, KVM_USERSPACE_IRQ_SOURCE_ID,  
			irq_event.irq, irq_event.level);  
…  
} 
linux.git/virt/kvm/irq_comm.c  
void kvm_set_irq(struct kvm *kvm, int irq_source_id, …)  
{  
… 
	list_for_each_entry(e, &kvm->irq_routing, link)  
	if (e->gsi == irq)  
		e->set(e, kvm, !!(*irq_state));  
}

当创建内核中的虚拟中断芯片的时候,虚拟中断芯片会创建一个默认的表格。KVM为用户空调提供API以设置这个表格,用户空间的虚拟设备可以按需增加这个表格的表项。

新的数据结构:

  • 小于16的管脚会创建两个entry(set),一个指向8259A提供的中断注入接口,一个指向APIC提供的中断注入接口。
KVM: Userspace controlled irq routing  
linux.git/virt/kvm/irq_comm.c  
static const struct kvm_irq_routing_entry default_routing[] = {  
	ROUTING_ENTRY2(0), ROUTING_ENTRY2(1),  
… 
	ROUTING_ENTRY1(16), ROUTING_ENTRY1(17),  
…  
};  

linux.git/virt/kvm/irq_comm.c  
#define ROUTING_ENTRY1(irq) I/O APIC_ROUTING_ENTRY(irq)  
#define ROUTING_ENTRY2(irq) \  
	I/O APIC_ROUTING_ENTRY(irq), PIC_ROUTING_ENTRY(irq)

IOAPIC_ROUTING_ENTRYPIC_ROUTING_ENTRY创建结构体kvm_irq_routing中的entry,管脚小于16时候,创建了PIC和APIC的表项。管脚大于16时候,只创建APIC的表项。setup_routing_entry根据PIC/APIC分别指向kvm_pic_set_irqkvm_ioapic_set_irq

linux.git/virt/kvm/irq_comm.c  
int setup_routing_entry(…)  
{  
…  
	switch (ue->u.irqchip.irqchip) {  
		case KVM_IRQCHIP_PIC_MASTER:  
			e->set = kvm_set_pic_irq;  
	… 
		case KVM_IRQCHIP_IOAPIC:  
			e->set = kvm_set_ioapic_irq;  
… 
	}  
…  
}

参考文献

  1. 《深度探索Linux系统虚拟化:原理与实现》 - 王柏生 & 谢广军
  2. 计算机中断体系一:历史和原理
  3. 一文讲透计算机的“中断”
  4. ACPI.sys,从Windows到Bios的桥梁(2):Windows应用程序响应GPIO(SCI)设备中断 Bios篇
0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区