KVM中断注入机制

前言

  • X86平台内核对QEMU下发的中断处理大致分三部分:查路由表,递交IO到中断控制器直至LAPIC,寄存器注入。第一部分路由中断在前一章已经介绍,中断向量的传递涉及到8259中断控制器的模拟,IOAPIC中断控制器模拟和LAPIC控制器的模拟,本文首先根据控制器手册分析其工作原理,然后介绍其软件模拟的实现,和中断流程的模拟

中断信号处理路径

  • X86平台处理中断信号分为四个阶段:
  1. 根据中断的gsi号查询中断路由表,找到对应的表项,取出handler,投递中断到IOAPIC
  2. IOAPIC根据输入的中断引脚索引查询重定向表,取出中断信号的目的cpu id和vector,投递中断到LAPIC
  3. LAPIC判断中断类型和向量号,将其写入vcpu中的IRR(Interrupt Request Register)寄存器
  4. 下一次VM-Entry进入时,检查IRR寄存器是否有中断请求,如果有,将IRR对应的bit转化为vector,写入vmcs区域的VM-Entry interrupt information区域
    在这里插入图片描述

IOAPIC递交中断

  • IOAPIC的中断流程处理,开始于kvm_set_ioapic_irq,整个流程如下
kvm_set_ioapic_irq
	kvm_ioapic_set_irq
		ioapic_set_irq
			ioapic_service
				kvm_irq_delivery_to_apic
					kvm_apic_set_irq
  • 中断信息最终要传递lapic,这中间就是ioapic的处理流程,其中主要的工作在ioapic_service完成
ioapic_service
	kvm_ioapic_redirect_entry *entry = &ioapic->redirtbl[irq]
	irqe.dest_id = entry->fields.dest_id;
    irqe.vector = entry->fields.vector;
	kvm_irq_delivery_to_apic(ioapic->kvm, NULL, &irqe, NULL)
		kvm_for_each_vcpu(i, vcpu, kvm) {
        	if (!kvm_apic_match_dest(vcpu, src, irq->shorthand,  irq->dest_id, irq->dest_mode))
            	continue;     
            if (!kvm_lowest_prio_delivery(irq)) {
            	if (r < 0)
                	r = 0;
            	r += kvm_apic_set_irq(vcpu, irq, dest_map)
            		__apic_accept_irq

首先将中断向量对应的重定向表项取出,重要的两个数据是目的cpu的id和向量号。然后调用继续传递中断信息,kvm_irq_delivery_to_apic中遍历每个vcpu,查看其apic id和重定向表中取出的id是否一样,如果一样,调用kvm_apic_set_irq继续传递中断信息,走到__apic_accept_irq,就是lapic负责的流程了

LAPIC处理

Make Request

  • KVM LAPIC处理中断信号的流程开始于__apic_accept_irq,该函数收到来自其它CPU或IOAPIC发来的中断向量。我们讨论普通的情况,收到来自IOAPIC的中断
static int __apic_accept_irq(struct kvm_lapic *apic, int delivery_mode,
                 int vector, int level, int trig_mode,
                 struct dest_map *dest_map)
	switch (delivery_mode) 
	case APIC_DM_FIXED:
		if (apic_test_vector(vector, apic->regs + APIC_TMR) != !!trig_mode) {
            if (trig_mode)
                kvm_lapic_set_vector(vector, apic->regs + APIC_TMR);
            else
                apic_clear_vector(vector, apic->regs + APIC_TMR);
        }

        if (vcpu->arch.apicv_active)
            kvm_x86_ops->deliver_posted_interrupt(vcpu, vector);
        else {
            kvm_lapic_set_irr(vector, apic);

            kvm_make_request(KVM_REQ_EVENT, vcpu);
            kvm_vcpu_kick(vcpu);
        }
  • lapic递交中断信号的主要动作就是往它模拟的lapic的寄存器地址上,IRR对应的地方写1,之后标记vcpu上的request的对应bit,这里是event。注意,除了这种方式,如果硬件支持posted-interrupt方式deliver中断,需要优先考虑使用。lapic将内存中IRR上中断向量对应bit置1后,make request就完成了。之后就是唤醒vcpu或者将其从guest态踢出,让其再次进入guest态前能够处理中断,这个动作被称为Kick
    在这里插入图片描述

Kick vCPU

  • 被Kick的vCPU进程可能处于以下两种状态,Kick要分别进行处理:
  1. vCPU进程在睡眠状态,Kick函数需要将它唤醒,投入到vCPU所在物理CPU的运行队列中,等待调度,当vCPU被调度运行后进入guest态,响应注入的中断
  2. vCPU进程在运行状态,正在guest态运行,Kick函数发送核间中断触发VM-Exit,使vCPU退出guest态,同样被投入到运行队列中,等待调度,后面的流程同上
  • 第二种情况需要利用LAPIC发送核间中断,这是LAPIC的另一个功能,这个功能主要是向其它cpu或自己发送中断,这种中断称为核间中断——IPI(Interprocessor Interrupt)
  • 接下来介绍核间中断的工作方式,然后是如何Kick一个睡眠的vCPU进程,最后是如何Kick一个正在guest态运行的vCPU进程

核间中断

  • 核间中断的发起最重要的一个寄存器叫ICR(interrupt command register),软件按照寄存器的使用规则往该寄存器中写信息,就可以发出IPI,ICR格式如下,截图来自Intel手册vol3-10.6
    在这里插入图片描述
    ICR中有几个重要的字段如下:
  1. Vector,IPI要发送的中断向量号
  2. Delivery Mode,描述发送中断的类型,以下中断类型可供选择
    000 (Fixed):固定向量号的中断,这种就是普通的中断
    001 (Lowest Priority):固定向量号的中断,和Fixed一样,但这种中断被硬件控制到优先级最低
    010 (SMI):System Managment Interrupt,发送系统管理中断,这种中断要求Vector字段必须位0
    100 (NMI):None-Maskable Interrupt,不可屏蔽中断,不可屏蔽中断的向量号是2,因此这里的Vector会被忽略
    110 (Start Up):“Start-up” IPI,一种特殊的核间中断。该中断对应的ISR在BIOS启动的时候就被创建好了,这种投递方式硬件不保证信号准确送达到目的CPU,失败后不会重试,需要靠软件保证,kick vcpu通过这种方式实现
  3. Destination:存放中断要发往目的CPU或者CPU组,共8bit可以发往所有256个处理器
  • 当ICR内容准备好之后,往其低16个字节写入内容,就会触发发送IPI的动作。

Kick Sleep vCPU

/*          
 * Kick a sleeping VCPU, or a guest VCPU in guest mode, into host kernel mode.
 */   
void kvm_vcpu_kick(struct kvm_vcpu *vcpu)
{     
    int me;
    int cpu = vcpu->cpu;
    /* vcpu线程处于睡眠状态,唤醒 */
    if (kvm_vcpu_wake_up(vcpu))
        return;
    /* 获取当前进程所在cpu的id,启动阶段由操作系统从lapic 的ID寄存器中读取 */ 
    me = get_cpu();
    /* 目标cpu不是当前进程所在cpu并且在线,满足条件后执行kick动作 */
    if (cpu != me && (unsigned)cpu < nr_cpu_ids && cpu_online(cpu))
        if (kvm_arch_vcpu_should_kick(vcpu))
            smp_send_reschedule(cpu);
    put_cpu();
}
  • kvm_vcpu_kick的核心功能就是让睡眠的,或者处在Guest态的vCPU进程,重新投入到运行队列中等待调度,如果目标vCPU在睡眠,那么它必然处在内核态,因此将其唤醒之后直接返回,唤醒函数的主要流程如下:
bool kvm_vcpu_wake_up(struct kvm_vcpu *vcpu)
{
	......
    wqp = kvm_arch_vcpu_wq(vcpu);			/* 1 */
 	swake_up(wqp);							/* 2 */
	......
}
1. 获取当前vcpu的等待队列
2. 如果等待队列中有正在睡眠的进程,将队列中的第一个task唤醒
  • 唤醒一个进程的实质就是将它的状态设置为TASK_RUNNING并投入到运行队列中,等待调度器的调度。swake_up最终调用到wake_up_process函数,如下:
int wake_up_process(struct task_struct *p)
{   
    return try_to_wake_up(p, TASK_NORMAL, 0);	/* 3 */
}
3. 将处于TASK_NORMAL状态的进程唤醒,TASK_NORMAL状态包括了可中断睡眠和不可中断睡眠
  • 分析try_to_wake_up的实现:
static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
	......
	cpu = task_cpu(p);													/* 4 */
	cpu = select_task_rq(p, p->wake_cpu, SD_BALANCE_WAKE, wake_flags);	/* 5 */
	if (task_cpu(p) != cpu) {											/* 6 */
        wake_flags |= WF_MIGRATED;
        set_task_cpu(p, cpu);
    }
    ......
4. 获取进程上一次运行所在的cpu id
5. 选择进程要投入到哪个cpu上的运行队列,这里会调用具体的调度器select_task_rq方法进行选择,完成后,进程在运行队列中等待调度
6. 将上一次进程运行的cpu与这一次选择的cpu做比较,不同表示进程发生了迁移,标记以下,最后更新进程运行的cpu
  • 到这里,原来处于睡眠中的vCPU进程就被放到了运行队列中,接下来就是等待调度时机的出现,将vCPU投入运行了,整个Kick过程结束

Kick in-guest vCPU

  • 除了Kick睡眠的vCPU,还有一种情况是Kick运行的vCPU。整个过程由smp_send_reschedule函数实现,其中会涉及到核间中断的发送,所以是体系结构相关的,每个体系结构定义了一套多核操作,X86上smp_send_reschedulenative_smp_send_reschedule函数,如下:
static inline void smp_send_reschedule(int cpu)
{
    smp_ops.smp_send_reschedule(cpu);
}

struct smp_ops smp_ops = {
    ......
    .smp_send_reschedule    = native_smp_send_reschedule,
    ......
};
  • 看名字,这个操作是通用接口,让目标物理CPU重新调度进程,为什么这个接口就能实现Kick guest vCPU的功能?看看里面的具体实现:
/*
 * this function sends a 'reschedule' IPI to another CPU.			
 * it goes straight through and wastes no time serializing
 * anything. Worst case is that we lose a reschedule ...
 */
static void native_smp_send_reschedule(int cpu)
{
	......
    apic->send_IPI_mask(cpumask_of(cpu), RESCHEDULE_VECTOR);		/* 1 */
    ......
}
注释:该函数向其它CPU发送一个'重调度'的核间中断,这个中断不保证串行化,就是说,如果在这个中断到达目标CPU之前,已经有一个中断存在,这个中断可能会丢失
1. 调用本地APIC接口,发送'重调度'核间中断,它的向量号是RESCHEDULE_VECTOR(253)。说明,核间中断内核和IOAPIC发送的中断内容一样,有两个基本的组成:目标CPU的ID,中断向量号。当这个IPI成功发送到目标CPU之后,硬件会跳转到中断向量表,转入中断处理流程
  • 本地APIC接口是一个静态的全局变量,它是一组APIC操作的驱动,系统在上电时根据平台不同进行初始化,操作中定义了发送核间中断的操作接口,如下:
struct apic {
	......
    /* ipi */
    void (*send_IPI)(int cpu, int vector);
    void (*send_IPI_mask)(const struct cpumask *mask, int vector);
	......
}
  • 一个具体的APIC操作驱动实现如下,我们选取NumaConnect system举例, numachip_send_IPI_mask最终操作具体的LAPIC硬件寄存器ICR,完成核间中断的发送,之后就是目标CPU上的中断处理了
static const struct apic apic_numachip __refconst = {
    .name               = "NumaConnect system",
	.send_IPI_mask          = numachip_send_IPI_mask,
	......
}
  • 内核中针对APIC核间中断的中断向量表定义如下:
/*
 * The APIC and SMP idt entries
 */
static const __initconst struct idt_data apic_idts[] = {
	......
    INTG(RESCHEDULE_VECTOR,     reschedule_interrupt),		/* 2 */
    INTG(CALL_FUNCTION_VECTOR,  call_function_interrupt),
	......
};
2. 重调度中断向量为RESCHEDULE_VECTOR,中断处理函数是smp_reschedule_interrupt,它的主要功能就是让目标CPU上正在运行的进程调度出去,然后让调度器重新选择新进程投入运行
  • RESCHEDULE_VECTOR核间中断实际上被很多内核模块调用,而Kick函数之所以也用这个接口,是看中了它的两个功能,一是它发送了一条中断信息,二是这个中断的处理流程是让CPU上的调度器重新调度,首先第一个功能,就能让guest态的CPU退出,VMCS中的Pin-based VM-execution control字段的最低位可以设置外部中断请求到达时是否触发VM-exit,这个字段被称为external-interrupt exiting。通常,这个字段会被设置为1。参考Intel手册volume 3 24.6.1部分。有了这个硬件机制,就能让guest CPU退出。再说重调度的第二个功能,它会让目标CPU上的调度器从运行队列中重新选择进程,投入运行,这个进程,可能还是之前刚退出的vCPU进程,也可能是其它进程,这都不重要,我们的目的是让vCPU进程退出guest后再进入,从而发现被注入的中断。
  • 最后给出’重调度’中断处理例程,这里涉及内核调度器的机制,有空继续分析
  • TODO
/*
 * Reschedule call back. KVM uses this interrupt to force a cpu out of
 * guest mode 
 */
__visible void __irq_entry smp_reschedule_interrupt(struct pt_regs *regs)
{
    ack_APIC_irq();
    inc_irq_stat(irq_resched_count);    
    kvm_set_cpu_l1tf_flush_l1d();

    if (trace_resched_ipi_enabled()) {
        /*
         * scheduler_ipi() might call irq_enter() as well, but
         * nested calls are fine.
         */
        irq_enter();
        trace_reschedule_entry(RESCHEDULE_VECTOR);
        scheduler_ipi();
        trace_reschedule_exit(RESCHEDULE_VECTOR);
        irq_exit();
        return;
    }
    scheduler_ipi();
}

VM-Entry

硬件基础

  • 中断注入最底层要依靠硬件实现,VMCS中VM-entry控制类字段,interruption-information是实现中断注入的基础,这个字段32bit,各字段含义如下,截图来自Intel手册vol3-24.8.3
    在这里插入图片描述
  • CPU进入客户态时,中断注入区域VM-entry interruption-information的加载,是在guest state区域加载之后——跳转到guest state中RIP指向的地址之前。实际上,中断注入的本质就是在硬件上deliver中断信息到CPU,而deliver的时机必须在CPU VM-entry建立好guest上下文,但还没开始运行的时间窗口里,这个时候deliver的中断信息,硬件查到的IDT就是guest的IDT,因为IDTR的加载是在guest state区域加载过程中完成的。

Request检查

  • qemu通过vm ioctl命令字创建vcpu之后,获得了vcpu的fops,通过下发vcpu ioctl的KVM_RUN命令字,让内核运行该vcpu
kvm_vm_ioctl
    switch (ioctl) {
    case KVM_CREATE_VCPU:
    	kvm_vm_ioctl_create_vcpu(kvm, arg)
    		static struct file_operations kvm_vcpu_fops = {
    			.release        = kvm_vcpu_release,
    			.unlocked_ioctl = kvm_vcpu_ioctl,
				......
			}
				kvm_vcpu_ioctl
					switch (ioctl) {
    				case KVM_RUN:
    					kvm_arch_vcpu_ioctl_run(vcpu, vcpu->run)
    						vcpu_run(vcpu)
    							for (;;) {
        							if (kvm_vcpu_running(vcpu)) {
            							r = vcpu_enter_guest(vcpu);
            						......
            					}

上面是一个vcpu从创建到使用的过程,进入vcpu_run之后,调用vcpu_enter_guest进入客户态,如果没有特殊的处理,vcpu就会一直在客户态待下去

  • vcpu_enter_guest在进入客户态前,会对当前的vcpu->requests做检查,看是否在上一轮进入客户态的时候,有别的cpu向自己发出了request,如果有,需要做相应的处理,vcpu_enter_guest中有这么一段代码
    if (kvm_check_request(KVM_REQ_EVENT, vcpu) || req_int_win) {
        ++vcpu->stat.req_event;
        kvm_apic_accept_events(vcpu);
        if (vcpu->arch.mp_state == KVM_MP_STATE_INIT_RECEIVED) {
            r = 1;
            goto out;
        }
		/* 将request中等待的中断注入 */
        if (inject_pending_event(vcpu, req_int_win) != 0)

硬件注入

在inject_pending_event中有一个vm_cpu_has_injectable_intr判断当前是否有可注入的中断,里面通过kvm_apic_has_interrupt检查当前vcpu上的irr中最高优先级的中断向量,判断是否可以注入

inject_pending_event
	kvm_apic_has_interrupt
		__apic_update_ppr
		apic_has_interrupt_for_ppr
			highest_irr = apic_find_highest_irr(apic)

__apic_update_ppr先根据TPR和ISRV更新PPR,apic_has_interrupt_for_ppr中寻找优先级最高的IRR,如果能找到,表示有中断需要注入,调用中断注入的实现

if (kvm_x86_ops->interrupt_allowed(vcpu)) {
	kvm_queue_interrupt(vcpu, kvm_cpu_get_interrupt(vcpu), false);
    kvm_x86_ops->set_irq(vcpu);
}
	vmx_inject_irq
		irq = vcpu->arch.interrupt.nr
		intr = irq | INTR_INFO_VALID_MASK
		intr |= INTR_TYPE_EXT_INTR
		vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, intr)

最终将中断信息写入vmcs对应区域,中断信息需要按照前面介绍的格式来组装,将interruption-information区域的32bit内容填满,首先是最高位的有效位,必须置1,然后是中断的类型写到[10-8],这里是外部中断,最后是中断向量本身。组装完成后,就可以往interruption-information区域写入了。VM_ENTRY_INTR_INFO_FIELDinterruption-information区域在VMCS整个区域的偏移

Q&A

Q:为什么Posted-Interrupt类型的中断注入,不需要CPU退出,就能达到中断退出的目的?
A:首先思考下为什么中断注入一定要让CPU退出,这是因为intel硬件设计使然。因为只有在VM-Entry这个时间硬件才检查VMCS域中的VM-entry interruption-information的字段,所以要让虚机先退出,然后复现一个VM-Entry的时机。
现在Intel提供了另外一种中断检查机制,通过VMCS域中的 Posted-Interrupt Descriptor字段就可以让Guest态的CPU感知到中断,并且这个字段Host可以直接写,不需要在VM-Entry时通过写VMCS间接设置。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

享乐主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值