エンジニアなら知っておきたい仮想マシンのしくみ

第7回プロセッサの仮想化をソースから知る[その2:qemu-kvm]

qemu-kvmから追う仮想マシンの一生

前回はLinux KVMのソースコードを読んできましたが、Linux KVMの理解をより深めるために、今回はLinux KVMの呼び出し元であるqemu-kvmのソースコードも読んでみましょう。

qemu-kvmは、オープンソースのCPUエミュレータであるQEMU に、Linux KVMに対応するための修正が加えられたバージョンです。Linux KVMがユーザモードプロセスであるqemu-kvmから「どのように制御されているのか」を併せて確認すると、仮想マシンが生成・実行される流れがよくわかるでしょう。

qemu-kvmの入手先

qemu-kvmの開発プロジェクトによる一次成果物は、現在はSourceForge.netから入手できます。

kernel virtual machine - SourceForge.net
URL:http://sourceforge.net/projects/kvm/

本記事は、執筆時点の最新安定版であるqemu-kvm-0.14.0を基に解説します。

qemu-kvmの一生 = 仮想マシンの一生

qemu-kvmは、仮想マシンを1つ起動するごとに、プロセスが1つ発生します。すなわち、仮想マシンはqemu-kvmプロセスの起動と同に発生し、プロセスの終了とともに消滅します。

qemu-kvmプロセスは、Linux KVMを利用する場合、kvm_init()関数を通してLinux KVMの初期化を行います。この関数では、Linux KVMのAPIにアクセスするために、必要な/dev/kvmファイルをオープンします。以後、Linux KVMのカーネルモジュールで仮想マシンや仮想プロセッサ(vcpu)を作成したり、メモリなどの資源を割り当てたり、また仮想プロセッサを実行する操作は、ここで得られたfdに対するioctl()システムコールで実現することになります。

qemu-kvm-0.14.0/qemu-kvm.c
149 int kvm_init(void)
150 {
151     int fd;
152     int r, gsi_count;
153
154
155     fd = open("/dev/kvm", O_RDWR);
156     if (fd == -1) {
157         perror("open /dev/kvm");
158         return -1;
159     }
  《中略》 
211     return kvm_create_context();
212
213   out_close:
214     close(fd);
215     return -1;
216 }

kvm_init() 関数は、Linux KVMがオープンできたら、続けてkvm_create_context()関数を呼び出して、Linux KVMへ仮想マシンの作成を指示します。この処理は、さっそくオープンしたばかりの/dev/kvmデバイスを通してKVM_CREATE_VM APIを呼び出すことで実現されています。

qemu-kvm-0.14.0/qemu-kvm.c
308 int kvm_create_vm(kvm_context_t kvm)
309 {
310     int fd;
311 #ifdef KVM_CAP_IRQ_ROUTING
312     kvm->irq_routes = qemu_mallocz(sizeof(*kvm->irq_routes));
313     kvm->nr_allocated_irq_routes = 0;
314 #endif
315
316     fd = kvm_ioctl(kvm_state, KVM_CREATE_VM, 0);
317     if (fd < 0) {
318         fprintf(stderr, "kvm_create_vm: %m\n");
319         return -1;
320     }
321     kvm_state->vmfd = fd;
322     return 0;
323 }

この後、qemu-kvmプロセスは、仮想プロセッサの割り当てやメモリの割り当て、デバイスエミュレータの登録などさまざまな処理を行います。今回は、仮想プロセッサに着目して、仮想プロセッサがどのように実行されているかを追ってきましょう。

qemu-kvmのスレッドモデル

以下に、qemu-kvmがLinux KVMを使った仮想マシンを実行する場合のスレッドモデルを示します。

qemu-kvmは、プロセス起動後に必要な仮想マシン分の仮想プロセッサを作成し、またスレッドを立ち上げます。以後、各スレッドは仮想プロセッサとしてひとつひとつが独立して動くイメージです。

図1 qemu-kvmのスレッドモデル
図1 qemu-kvmのスレッドモデル

仮想プロセッサの生成

仮想プロセッサの初期化処理は、qemu-kvm-0.14.0/hw/pc.cにあります。pc_cpus_init()関数は、仮想マシンに割り当てられた数の仮想プロセッサを初期化します。

qemu-0.14.0/hw/pc.c
942 void pc_cpus_init(const char *cpu_model)
943 {
944     int i;
945
946     /* init CPUs */
947     for(i = 0; i < smp_cpus; i++) {
948         pc_new_cpu(cpu_model);
949     }
950 }

pc_cpus_init()関数は、各仮想プロセッサを初期化するためにpc_new_cpu()関数を呼び出していますが、その過程でLinux KVM利用時にはkvm_init_vcpu()関数に制御が移ります。

265 void qemu_init_vcpu(void *_env)
266 {
267     CPUState *env = _env;
268
269     env->nr_cores = smp_cores;
270     env->nr_threads = smp_threads;
271     if (kvm_enabled())
272         kvm_init_vcpu(env);
273     return;
274 }

kvm_init_vcpu()関数では、各仮想プロセッサごとにスレッドが生成され、実行が開始されます。

qemu-kvm-0.14.0/qemu-kvm.c
1470 int kvm_init_vcpu(CPUState *env)
1471 {
1472     pthread_create(&env->kvm_cpu_state.thread, NULL, ap_main_loop, env);
1473
1474     while (env->created == 0) {
1475         qemu_cond_wait(&qemu_vcpu_cond);
1476     }
1477
1478     return 0;
1479 }

上記の過程により、仮想プロセッサが動きだします。

仮想プロセッサの起動

各スレッドは、Linux KVMの機能を利用して、仮想マシン上に仮想プロセッサを生成します。そして、メインスレッド側で仮想マシン実行準備が完了するまで待ち合わせてから、実際に仮想マシンの実行を開始します。

qemu-kvm-0.14.0/qemu-kvm.c
1505 static void *ap_main_loop(void *_env)
1506 {
1507     CPUState *env = _env;
  《中略》 
1527     kvm_create_vcpu(env, env->cpu_index);
  《中略》 
1533
1534     /* and wait for machine initialization */
1535     while (!qemu_system_ready)
1536         qemu_cond_wait(&qemu_system_cond);
1537
1538     /* re-initialize cpu_single_env after re-acquiring qemu_mutex */
1539     cpu_single_env = env;
1540
1541     kvm_main_loop_cpu(env);
1542     return NULL;
1543 }

仮想プロセッサのメインループ

kvm_main_loop_cpu()関数は動作中の仮想プロセッサのメインループです。Linux KVMのAPIによって仮想プロセッサを実行します。

qemu-kvm-0.14.0/qemu-kvm.c
1486 static int kvm_main_loop_cpu(CPUState *env)
1487 {
1488     while (1) {
1489         int run_cpu = !kvm_cpu_is_stopped(env);
1490         if (run_cpu && !kvm_irqchip_in_kernel()) {
1491             process_irqchip_events(env);
1492             run_cpu = !env->halted;
1493         }
1494         if (run_cpu) {
1495             kvm_cpu_exec(env);
1496             kvm_main_loop_wait(env, 0);
1497         } else {
1498             kvm_main_loop_wait(env, 1000);
1499         }
1500     }
1501     pthread_mutex_unlock(&qemu_mutex);
1502     return 0;
1503 }

kvm_cpu_exec()関数は、実際にLinux KVMを使って仮想マシンを実行するための関数です。/dev/kvmデバイスに対して実際にKVM_RUNを発行し、仮想マシンを実行します。仮想マシンの実行が停止すると、仮想マシンからの復帰理由が通知されるため、その値から判断し適宜必要な処理を行い、仮想プロセッサの実行を再開します。

KVM_EXIT_*発生後のイベントディスパッチ

仮想プロセッサを実行してしばらくすると、何らかの理由により仮想プロセッサが停止し、Linux KVMからqemu-kvmに制御が戻ることになります。たとえば、仮想マシン上でデバイスI/Oが発生してLinux KVMがハードウェアエミュレーションの処理をしなければならない場合、一定時間仮想マシンの実行をしたため別の仮想マシンやプロセスへ切り替えする場合などです。

Linux KVMのカーネルモジュール内にはVMEXIT発生時のイベントハンドラが含まれていました。基本的に、それらのハンドラにより仮想プロセッサが停止した原因を除去し実行を再開可能なら、Linux KVMが内部的に対処し仮想マシンの実行を継続します。しかし、Linux KVMが自体で原因を除去できないような理由でVMEXITが発生した場合には、ioctl()の呼び出し元であるqemu-kvmへ制御を戻します。 qemu-kvmは、VMEXITの発生理由を除去して、再度ioctl()で仮想プロセッサの実行をLinux KVMに指示します。

仮想マシン上でデバイスI/Oが発生したケースを考えてみましょう。I/Oが発生すると、プロセッサの仮想化支援機能により、仮想マシンからホストへモード遷移が発生します。Linux KVMは、VMEXITが発生した理由を確認し、それがデバイスI/Oであると認識します。しかし、Linux KVM自体はハードウェアエミュレーションの機能を持っていませんので、qemu-kvmにKVM_EXIT_IOを通知し、デバイスI/Oエミュレーションの処理を要求します。

kvm_cpu_exec()関数を追ってみましょう。この関数では、KVM_EXIT_IOが発生した場合には、仮想マシンのデバイスI/Oをエミュレーション処理を行うkvm_handle_io()関数を呼び出します。

qemu-kvm-0.14.0/kvm-all.c
912 int kvm_cpu_exec(CPUState *env)
913 {
  《中略》 
941         ret = kvm_vcpu_ioctl(env, KVM_RUN, 0);
  《中略》 
961         switch (run->exit_reason) {
962         case KVM_EXIT_IO:
963             DPRINTF("handle_io\n");
964             ret = kvm_handle_io(run->io.port,
965                                 (uint8_t *)run + run->io.data_offset,
966                                 run->io.direction,
967                                 run->io.size,
968                                 run->io.count);
969             break;
970         case KVM_EXIT_MMIO:
971             DPRINTF("handle_mmio\n");
972             cpu_physical_memory_rw(run->mmio.phys_addr,
973                                    run->mmio.data,
974                                    run->mmio.len,
975                                    run->mmio.is_write);
976             ret = 1;
977             break;
978         case KVM_EXIT_IRQ_WINDOW_OPEN:
  《中略》 
1002             }
  《中略》 
1012     } while (ret > 0);
  《中略》 
1025 }

この先でどのような処理が行われているかは、デバイスI/Oの回にて改めて説明したいと思います(が、ここまで読まれた方であれば、だいたい想像がつきますよね⁠⁠。

プロセッサ仮想化のまとめ

第4回では、仮想マシンにおいて仮想プロセッサがどのようなものであるべきか、また、それを実現するための手法としてtrap-and-emulateを紹介しました。また、仮想化支援機能を持たないx86プロセッサでシステム仮想マシンを実現するにはどのような障壁があったのかについて解説しました。

そのような状況の中で、仮想化支援機能を搭載したx86プロセッサが各社よりリリースされました。第5回は、x86プロセッサが持つ仮想化支援機能がどのようなものであるかについて解説しました。

現在x86システムの世界で使われているx86システム仮想マシンは、ほぼ全てが仮想化支援機能を用いています。第6回、そして第7回(今回)は、仮想化支援機能を搭載したプロセッサを前提に、trap-and-emulateによるシステム仮想マシンの実装の一例として、Linux KVMおよびqemu-kvmのソースコードを追いました。

今回をもって、Intel VTやAMD-V、そしてLinux KVMを組み合わせることにより、仮想プロセッサを実現する仕組みについて解説しました。読者の皆さんには、仮想マシンの仕組みについて大体ご理解いただけたと思うのですが、いかがでしょうか?

次回予告

これまで、コンピュータを構成する三大コンポーネントのうち、プロセッサの仮想化に焦点をあてて解説してきました。今回をもって仮想プロセッサの概念からいったん離れ、次回からはシステムメモリの仮想化手法について紹介したいと思います。

おすすめ記事

記事・ニュース一覧