2012年12月14日金曜日

割り込みはいかにしてゲストの割り込みハンドラに届くのか

こんばんわ。出遅れましたが12月13日は カーネル/VM Advent Calendar 2012 にて私の当番、といっても一昨年、去年ほど頑張ってネタを仕込めていないので、昨晩調べてたことを軽くまとめておきたいと思います。


ご存じのとおり、Linux KVMやFreeBSDのBHyVeなどはメインループをユーザモードプロセスに持たせる形で仮想マシンが実装されています。また、CPUにはIntel VTもしくはAMD-Vと呼ばれる仮想化支援機能が搭載されており、これらが仮想マシンの実行を行う命令群を提供します。
ただ、実際にはI/O操作など非同期で処理が行われるものがあります。例えば、わかりやすいのは下記の三点でしょう。


  • タイマ割り込み


  • ディスクI/Oの完了通知


  • ネットワークパケットの到着通知


割り込みは遅延させずに通知させる必要があります。たとえば、タイマ割り込みは遅延するとゲスト内で正確な時間をカウントできなくなります(もっとも従来の割り込みを使った時刻の管理が仮想マシンで適しているとは思わないのですが)。
時刻同期のほかにも、例えばディスクI/Oの割り込みも大事ですね。例えばデータベースの更新トランザクションを考えてみます。一般的にデータベースのトランザクション保護はログ領域やダブルライト領域にデータの書き込みが完了してから、次のトランザクションや実際のデータ領域の更新に着手します。このためディスクI/Oが完了したことがリアルタイムに通知されることは非常に大事です。
割り込みは、VMENTERするときにフィールドを指定することによりゲストに挿入できます。が、もしVMが実行されている状況で割り込みが発生したとしたらどういう風に通知されるんだろう、と思って、今回は、割り込みが通知されてからVMENTERするあたりのロジックをちょっとだけ追いかけてみました。


■ KVM編

KVMのソースコードを見てみたところ、kvm_lapic_find_highest_irr() に、まさにその答えがありました。割り込みを挿入したい場合には、 kvm_vcpu_kick() を介してVMEXIT を促します。

arch/x86/kvm/lapic.c

int kvm_lapic_find_highest_irr(struct kvm_vcpu *vcpu)
{
        struct kvm_lapic *apic = vcpu->arch.apic;
        int highest_irr;

        /* This may race with setting of irr in __apic_accept_irq() and
         * value returned may be wrong, but kvm_vcpu_kick() in __apic_accept_irq
         * will cause vmexit immediately and the value will be recalculated
         * on the next vmentry.
         */
        if (!apic)
                return 0;
        highest_irr = apic_find_highest_irr(apic);

        return highest_irr;
}


kvm_vcpu_kick() 自体は kvm_main.c にありました。

kvm_main.c

#ifndef CONFIG_S390
/*
* 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;
        wait_queue_head_t *wqp;


        wqp = kvm_arch_vcpu_wq(vcpu);
        if (waitqueue_active(wqp)) {
                wake_up_interruptible(wqp);
                ++vcpu->stat.halt_wakeup;
        }


        me = get_cpu();
        if (cpu != me && (unsigned)cpu < nr_cpu_ids && cpu_online(cpu))
                if (kvm_arch_vcpu_should_kick(vcpu))
                        smp_send_reschedule(cpu);
        put_cpu();
}
#endif /* !CONFIG_S390 */

vCPUが停止状態であればそれを活性化し、またvCPUが稼働中であればvCPUに対して割り込みを送ることで、仮想マシンから強制的にVMEXITさせます。このあとLinuxカーネルが再度vCPUをスケジュールする際に割り込みが挿入されるわけですね。

■ BHyVe 編

続いて BHyVe の場合どうなっているかを見てみます。BHyVeを調べるにあたっては、PCIデバイスのエミュレーション部分のMSI挿入用関数である pci_generate_msi() を起点に追いかけてみます。

src/usr.sbin/bhyve/pci_emul.c

void
pci_generate_msi(struct pci_devinst *pi, int msg)
{

        if (pci_msi_enabled(pi) && msg < pci_msi_msgnum(pi)) {
                vm_lapic_irq(pi->pi_vmctx,
                             pi->pi_msi.cpu,
                             pi->pi_msi.vector + msg);
        }
}


vm_lapic_irq() という関数が割り込みを挿入していますが、これは libvmmapi で提供されるライブラリ関数です。実際には仮想マシンのファイル識別子に ioctl() を投げるラッパです。

src/lib/libvmmapi/vmmapi.c

int
vm_lapic_irq(struct vmctx *ctx, int vcpu, int vector)
{
        struct vm_lapic_irq vmirq;

        bzero(&vmirq, sizeof(vmirq));
        vmirq.cpuid = vcpu;
        vmirq.vector = vector;

        return (ioctl(ctx->fd, VM_LAPIC_IRQ, &vmirq));
}



この ioctl() を受け取るコードは BHyVe の肝ともいえる vmm.ko 内にあります。 ioctl() に応じてVMENTERしたりするわけですが、割り込みのインジェクションも ioctl() なんですね。

src/sys/amd64/vmm/vmm_dev.c

static int
vmmdev_ioctl(struct cdev *cdev, u_long cmd, caddr_t data, int fflag,
             struct thread *td)
{
        省略        /*
         * Some VMM ioctls can operate only on vcpus that are not running.
         */
        switch (cmd) {
        case VM_RUN:
        case VM_SET_PINNING:
        case VM_GET_REGISTER:
        case VM_SET_REGISTER:
        case VM_GET_SEGMENT_DESCRIPTOR:
        case VM_SET_SEGMENT_DESCRIPTOR:
        case VM_INJECT_EVENT:
        case VM_GET_CAPABILITY:
        case VM_SET_CAPABILITY:
        case VM_PPTDEV_MSI:
        省略
        case VM_LAPIC_IRQ:
                vmirq = (struct vm_lapic_irq *)data;
                error = lapic_set_intr(sc->vm, vmirq->cpuid, vmirq->vector);
                break;
        省略}


VM_LAPIC_IRQの処理の実体は vmm_lapic.c にありました。ここでは、 vlapic_set_intr_ready() で「仮想マシンに割り込みが来ている」というフラグを立てて、さらにここから vmm_ipi.c の中の vm_interrupt_hostcpu() 関数を呼んでいます。IPIとはInter Processor Interruptですかね。

src/sys/amd64/vmm/vmm_lapic.c

lapic_set_intr(struct vm *vm, int cpu, int vector)
{
        struct vlapic *vlapic;

        if (cpu < 0 || cpu >= VM_MAXCPU)
                return (EINVAL);

        if (vector < 32 || vector > 255)
                return (EINVAL);

        vlapic = vm_lapic(vm, cpu);
        vlapic_set_intr_ready(vlapic, vector);

        vm_interrupt_hostcpu(vm, cpu);

        return (0);
}


src/sys/amd64/vmm/vmm_ipi.c

void
vm_interrupt_hostcpu(struct vm *vm, int vcpu)
{
        int hostcpu;

        if (vcpu_is_running(vm, vcpu, &hostcpu) && hostcpu != curcpu)
                ipi_cpu(hostcpu, ipinum);
}


ここで呼び出している ipi_cpu() という関数はカーネル内でCPUを強制的にコンテクストスイッチしたりNMI突っ込んだりする際に使われる関数のようです。なるほど、これで仮想マシンから強制的にVMEXITできるんですね。

src/sys/amd64/amd64/mp_machdep.c

void
ipi_cpu(int cpu, u_int ipi)
{

        /*
         * IPI_STOP_HARD maps to a NMI and the trap handler needs a bit
         * of help in order to understand what is the source.
         * Set the mask of receiving CPUs for this purpose.
         */
        if (ipi == IPI_STOP_HARD)
                CPU_SET_ATOMIC(cpu, &ipi_nmi_pending);

        CTR3(KTR_SMP, "%s: cpu: %d ipi: %x", __func__, cpu, ipi);
        ipi_send_cpu(cpu, ipi);
}


VMEXITされると、vmm.koから仮想マシンを実行するプロセスであるbhyveコマンドに制御が移ります。この時点で「次回はMSIを発生させるぞ」というステータスになっているわけなので、bhyveコマンドが次に仮想マシンを実行すると割り込みが挿入され、ゲストOSの割り込みハンドラがトリガされるわけですね。

qemu-kvmでもbhyveでも、強制的にVMEXITさせた後にプロセスがpreemptするかどうかはOSのスケジューラの動作にかかってくる感じでしょうか。たとえばVMENTER直後でタイムスライスを使い切っていない状態であれば、VMEXITした後にコンテクストスイッチをはさまずVMENTERすることもあるでしょう。VMENTERしてから暫く時間が経過していると、VMEXITと同時に他のプロセスにスイッチされちゃうのかな。

0 件のコメント:

コメントを投稿