Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Qemuのしくみ (の一部)

執筆者 : 箕浦 真


こういう 仕事をしていると、ときどきQemuの仕組みや内部動作をお客様に説明する必要があることがあるが、そういう時に「Qemuの〜についてはここを見てね」と言えるような文書があるといいなぁと思って自分で作ってみることにした。

1. 細々とした予備知識

1.1 Qemuのデバイスエミュレーション

Qemuはコンピューターシステム (PC、サーバ、ワークステーション、SBCなど) のエミュレーターであって、システム全体のエミュレーションができる。システムの中には、CPU、メモリ、その他のI/Oデバイスなどが含まれるが、Qemuはこのすべてを含めたシステムのエミュレーションができる。

このうち、I/Oデバイスとしては、例えばNICが挙げられる。Intelのギガビットイーサネットコントローラーである82574 (Linuxのデバイスドライバー名からe1000eと通称される) や、仮想環境専用の仮想的なNICであるvirtio-netなどのエミュレーションができる。この、仮想マシン (VM) 側に対してエミュレーションを行なうドライバーを、フロントエンド側などと呼ぶ。一方、フロントエンドが生成したパケットをQemuの外に送り出したり、逆にホストから受け取ったパケットをフロントエンドに送る (VMから見れば受信) 動作をするのはバックエンドと呼んでいる。NICのバックエンドとしては、Qemuが仮想的なNICデバイスを生やしてホストに送受信するtapバックエンドや、ユーザースペースのIPスタックSlirpを利用したuserバックエンドなどがある *1

同様に、シリアルポートであれば、COMポート (16550 UART) やvirtio-serialなどのフロントエンドに対して、ホストのファイルとして出力をするものや、TCPやUnix-domainソケットに送受信するバックエンドなどがある。ブロックストレージデバイスのフロントエンドとして、PCの標準的なIDEやSATAをエミュレートするものや、virtio-blkといったものがあり、バックエンド側としてはディスクイメージファイルを利用するものや、ホストデバイスをパススルーするもの、Ceph RBDを利用するものなどがある。ブロックデバイスのバックエンドは複数段が重ねられることがあり、例えばファイルバックエンドとSATAフロントエンドの間には、QCOW2フォーマットを解するqcow2バックエンドや、ISOイメージなどのためのrawバックエンドなどが入る。

これらの様子は、例えばlibvirt (virshやvirt-manager) を使ってQemuのVMを起動した際のコマンドラインから垣間見ることができる。コマンドラインを一部抜粋する。

-netdev tap,fd=35,id=hostnet0
-device e1000e,netdev=hostnet0,id=net0,mac=52:54:00:XX:YY:ZZ,bus=pci.1,addr=0x0

-netdevがバックエンドの、-deviceがフロントエンドの設定だ。バックエンドにhostnet0という名前 (id) がつけられており、-deviceオプションのnetdev=hostnet0で両者が結び付けられている。fd=35は、tapデバイスがすでにlibvirtにより作成済みであることを示す*2。シリアルポートのバックエンドは-chardevで、またブロックデバイスのバックエンドは-blockdev-driveで指定する。

なお、中にはフロントエンドとバックエンドが明確に区別されないようなものもあり、例えばPCIブリッジなどのように、エミュレーションがQemu内部で完結するものがその一例である。

Qemuにはこのほかにlinux-userのように特定のOS向けの機械語プログラムを実行する機能もあり、この場合デバイスはエミュレートされない。

1.2 QemuのCPUエミュレーション

古くからあるQemuの元々の機能として、CPUのエミュレーションというものがある。TCG (Tiny Code Generator) というバイナリートランスレーターの実装が、この機能の中心だ。ゲストCPUの機械語命令を、ホストCPUの機械語命令に変換して実行する。

ホストとゲストのCPUが同じであれば、バイナリートランスレーターを使うより、ゲストの命令をホストが直接実行してしまった方が速いのは当然だ。しかし、ゲストに実行されては困る命令も多くあり (例えばデバイスを制御する命令)、簡単にはいかない。Xenでは、ゲストOSに手を加えてそうした命令をハイパバイザーに指令するようにしていた。近年では、ホストCPUが仮想化支援機能を持ち、OSもその機能を利用するための機構を用意していることも多く、LinuxのKVMがその典型的な例である (Xenも現在ではCPUの仮想化支援機能を利用して手を加えないゲストOSも動かせる)。

Qemuはその他に、Hypervisor.framework (macOS)、NVMM (NetBSD)、Intel HAXMなどを利用して、CPUの仮想化支援機能を活用できる。これらをアクセラレーターと呼んでいる。

1.3 Qemuのスレッド

Qemuはマルチスレッドプログラムである。マルチプラットフォームのプログラムでもあるため、WindowsのスレッドやPOSIXスレッド (pthread) に対応したラッパー群も用意されており、スレッドの生成や終了のほか、mutexやセマフォの仕組みも用意されている。

例えば、仮想CPU (vCPU) は、それぞれ独立して動作するため、Qemuが起動すると指定された数のvCPUスレッドを生成する。vCPUスレッドは、VMのメモリから命令を読み出して、TCGやKVMといったアクセラレーターを利用してCPUのエミュレーションを行う。

Qemu monitorも、QMPモードの場合はスレッドを生成する。QMP monitorは複数持つこともできるが、単一のスレッドが全てを担当するつくりだ。その他、一部のデバイス処理や、Qemuの利用しているライブラリが、スレッドを生成することがある。例えば、VNCを利用すると、vnc_workerというスレッドができる。

Qemuのメインスレッド (main()のコンテキスト) は何をするかというと、その他のデバイス全般のエミュレーションを行う。イベントドリブン型のループになっていて、イベント、例えばvCPUスレッドからの要求や、デバイスのバックエンドからの完了通知などを受けて、処理を行なう*3。ソースコードで言えば、ループはsoftmmu/runstate.cにあるが、重要な部分はutil/main-loop.cのmain_loop_wait()os_host_main_loop_wait()にある。glibのメインループイベントソースの機能が使われている。

Qemuがエミュレートするデバイスの中には、ストレージデバイスのように処理に時間のかかるものもあるので、単一のスレッドでそうした処理を行うために、Qemuにはさまざまな仕組みが用意されている。本稿では、これを紹介してみたい。

なお、Linuxなど多くのOSではスレッドに名前をつけることができる。Qemuでは、-nameオプションに、debug-threads=onと入れることにより、この機能によりスレッドに名前をつけることができる。この様子は、ps Ho comm <PID>で見ることができる。以下に例を挙げる。commだけでは寂しいので、他に2つの項目を追加した。

$ ps Ho pid,tid,comm 384933
    PID     TID COMMAND
 384933  384933 qemu-system-x86
 384933  384935 qemu-system-x86
 384933  384938 IO mon_iothread
 384933  384939 CPU 0/KVM
 384933  384940 CPU 1/KVM
 384933  384941 CPU 2/KVM
 384933  384942 CPU 3/KVM
 384933  384949 SPICE Worker
 384933  825329 worker

CPU ?/KVMというのがvCPUスレッドというのはわかりやすい。IO mon_iothreadは、QMP monitorのスレッドである。SPICE Workerというのは、Qemuがリンクしているlibspiceのスレッドで、SPICEプロトコルの処理を行う (のだと思うが、実は詳細は調べていない)。workerは、次回紹介するスレッドプールのワーカースレッドである。qemu-system-x86というスレッドが2つあるが、これは名前をつける操作をしなかった無名のスレッドを意味する。1つ目のPIDとTID (スレッドID) が一致しているものがメインスレッドである。2つ目の方は、call_rcuという名前のつくべきスレッドであるが、-name引数の評価より前に生成されてしまうため、名前がつかなかったものである。call_rcuはRead Copy Update関連の処理を行うが、ここでは詳細は割愛する。

長い前書きだった。

2. 追加のI/OスレッドとAioContext

2.1 追加のI/Oスレッド

単一のメインスレッドがデバイスのエミュレーションを行なう、と書いたが、実はQemuは特定のデバイスのエミュレーションのためにI/Oスレッドを増やすこともできる。これを、本稿では追加のI/Oスレッドと呼ぶことにする*4

libvirtを使う場合、iothreadsという要素を使う。<domain>直下に、

  <iothreads>1</iothreads>

と書くと、追加のI/Oスレッドが1つでき、デバイスのエミュレーションがメインスレッドと合わせて2つのスレッドで分担できるようになる。ただし、追加のI/Oスレッドでエミュレートするデバイスは限られる上に、それぞれ手で指定する必要がある。例えば、virtio-blkデバイスは追加のI/Oスレッドで処理できるので、

    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2'/>
      <source file='/some/where/image.qcow2'/>
      <target dev='vda' bus='virtio'/>
(略)

となっているところを、

    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2' iothread='1'/>
      <source file='/some/where/image.qcow2'/>
      <target dev='vda' bus='virtio'/>
(略)

のようにすると、vdaが追加のI/Oスレッドで処理されるようになる。追加のI/Oスレッドには1から始まる番号が付くので、それをdriver要素のiothread属性で指定する、というわけだ。複数のデバイスを、追加のI/Oスレッド1つで担当させることもできるし、追加のI/Oスレッドをデバイス数だけ生やしてそれぞれ担当させる、ということもできる。

追加のI/Oスレッドもイベントドリブンループになっているが、メインスレッドよりずっと簡略化されている

2.2 AioContext

メインスレッド、追加のI/Oスレッドの双方で、中心的なQemu内部構造体が、AioContextだ。メインスレッドには2つ、追加のI/Oスレッドに1つずつ確保される。AioContextは、

  • AioHandler
  • event_notifer
  • タイマー
  • Bottom half (BH)
  • スレッドプール

などのリストがその中心である。最初の4つはさまざまなイベントに際してコールバックを呼ぶ仕組みで、それぞれ、各デバイスのバックエンドなどが管理するファイルディスクリプター (fd) の監視、他スレッドからの通知の受け取り、各種の時限処理、他スレッドからの依頼に対するコールバックの実行、という処理を行なう。またスレッドプールは、ワーカースレッドを適宜立てて時間のかかるジョブを実行する仕組みである。

これらの仕組みを使うための内部APIでは、常にAioContextへのポインターを扱うことになっている。もともとは単一のメインスレッドが全部を処理する仕組みであったため、今でもAioContextを指定しない形式のAPIが残っており、多くのデバイスで使われている。さらに、もともと違うものであったiohandlerという仕組みをAioHandlerに統合した都合で、メインスレッドのみ2つのAioContextが割り当てられていて、これはメインループが追加のI/Oスレッドよりずっと複雑になっている理由の1つだ。

bool aio_poll(AioContext *ctx, bool blocking);
AioContext *qemu_get_aio_context(void);
AioContext *iohandler_get_aio_context(void);

aio_poll()は、ctxの各イベント処理を進め、なにかしらが実行されればtrueを返す。blockingがtrueであれば、なにかしら実行されるまで待つ。一見メインループの中からしか使い道がなさそうに見えるが、I/Oリクエストのキャンセル時に処理待ちの完了イベントを回収する、などの際に使われているようだ。

qemu_get_aio_context()iohandler_get_aio_context()は、メインスレッドの担当する2つのAioContextの一方を返す。

2.3 Big Qemu Lock

Qemuはマルチスレッドプログラムである、と書いたが、当初はそうではなかった。vCPUスレッドが独立した時に、いわゆるGiant Lock形式のmutexが導入され、現在も使われている。基本的に、メインスレッドではイベント待ちに入る直前にunlockしてイベント発生後その処理に入る直前にlockする、という動作を行い、またvCPUスレッドではKVMやTCGなどがゲストのコードの実行に入る前にunlockしてQemuのコードに戻る時にlockする、という動作をしている。

このmutexの実態は、qemu_global_mutexであるが、通称BQL (Big Qemu Lock) と呼ばれる。デバイスエミュレーションの多くのコードは、スレッド・セーフではないため、同時に複数実行されないようBQLによって保護する必要がある。

#define qemu_mutex_lock_iothread()                      \
    qemu_mutex_lock_iothread_impl(__FILE__, __LINE__)
void qemu_mutex_lock_iothread_impl(const char *file, int line);
void qemu_mutex_unlock_iothread(void);

qemu_mutex_lock_iothread()でBQLを獲得するが、これはデバッグ用にlockした場所をトレースできるようにしたマクロで、実態はqemu_mutex_lock_iothread_impl()である。競合状態などのプロファイルも別に取られるようになっている。解放するのはqemu_mutex_unlock_iothread()だ。

例えば、KVM利用のvCPUスレッドで、KVMが呼び出されるのはaccel/kvm/kvm-all.cというファイルのkvm_cpu_exec()というところにある。この中で、最初にqemu_mutex_unlock_iothread()が実行されており、裏を返すとそれまではlockされていた、と分かる。KVMから戻ってくると、どうして戻ったのかの理由 (シャットダウンやI/O命令の実行などがある) がrun_ret変数に収められているが、これに従って各種の処理を行う前にqemu_mutex_lock_iothread()でBQLをlockしている。

追加のI/Oスレッドでは、いちいちBQLをunlockしたりlockしたりといった処理はしていない。比較的新しい機能であるため、BQLの必要な機能を呼び出さないよう書くことができる (そのように書かれたものだけが追加のI/Oスレッドで処理できる)、ということだと考えられる。BQLの競合は、徐々に解消されているとはいえやはり多く、追加のI/Oスレッドの効果の一つは、BQLの不要な処理を分離・独立して動かすことにある。

3. AioContextの各種イベント処理

3.1 AioHandler

先ほどos_host_main_loop_wait()を見た方は、イベント待ちとはqemu_poll_ns()という関数を呼ぶことであることに気づかれたかと思う。qemu_poll_ns()は、(Linuxでは) ppollというシステムコールのラッパーとなっている。これはpoll(2)の仲間で、指定された1つ以上のファイルディスクリプターの状態が変わる (読めるようになった、書けるようになった、など) か、指定された時間が経つかするまで待つ、というものだ *5。AioHandlerでは、ファイルディスクリプターの状態が変わった時に呼ばれるコールバック関数を登録できる。

typedef bool AioPollFn(void *opaque);
typedef void IOHandler(void *opaque);

void aio_set_fd_handler(AioContext *ctx,
                        int fd,
                        bool is_external,
                        IOHandler *io_read,
                        IOHandler *io_write,
                        AioPollFn *io_poll,
                        void *opaque);
void qemu_set_fd_handler(int fd,
                         IOHandler *fd_read,
                         IOHandler *fd_write,
                         void *opaque);

aio_set_fd_handler()がコールバックの登録・解除のための内部APIである。fdが監視対象、io_readio_writeが、それぞれfdからの読み出し、fdへの書き込みができるようになったときに呼ばれるコールバック関数である。is_externalというのは、Qemuの内蔵しているnbdから外部への送信のように、監視対象が外部に影響のあるものであるときにtrueを設定する。io_pollもコールバック関数であるが、これは現在読み書きができるかどうかのポーリングを実施する関数である。例えばバルク転送中には高確率で次の読み書きが可能であるが、いちいちppoll()を呼ぶオーバーヘッドがイヤ、ということで、このようなコールバック関数が用意されている。io_pollの中でシステムコールを呼んではオーバーヘッドという意味では変わらないため、システムコールを呼ばなくても判断ができるvirtioで利用されている。opaque引数はこれらコールバック関数が呼ばれる際の引数である。

なお、io_read、io_write、io_pollの全てにNULLを指定すると、監視の解除を意味する。

qemu_set_fd_handler()は、暗黙にAioContextとしてiohandler_get_aio_context()のコンテキストを利用する古い形式のAPIである。

3.2 event_notifier

Linuxにはeventfdというシステムコールがある。呼び出すと、ファイルディスクリプターを1つ返す。初期値0のカウンター1つと結び付いていて、このfdに数値を書き込むと加算される。fdから読み出すとカウンターの値が読めると同時に、値が0に再初期化される。ただし、値が0だった場合には読み出せず、1以上になるまで待たされるか、non-blockingモードの場合EAGAINになる。書く人から読む人に対してイベントを通知する、という想定で、eventfdという名はそれを示す (詳しくはman pageを参照のこと。セマフォとしても使える)。

カウンターの値はイベントの発生回数という想定であるが、Qemuでは常に1を書き、また読めるかどうかしか判断しておらず、どちらかというと「状態」に近い使い方をしている。つまり、イベントが発生した状態なのか否か、という表現である。eventfdシステムコール自体はLinux特有の機能で、マルチプラットフォームサポートのQemuとしてはevent_notifierという形で抽象化している。例えばeventfdのない他のPOSIX互換システムでは、パイプを用いるようになっている。

event_notifierは、struct EventNotifierという構造体で表される。中身を気にする必要はない、いわゆるopaqueポインターとして使われるが、構造体自体のアロケートは呼び出し元でやっておく必要がある。通常、デバイスを表す構造体などイベントを利用するデータ構造の中に埋め込まれる。

typedef void EventNotifierHandler(EventNotifier *);

int event_notifier_init(EventNotifier *, int active);
void event_notifier_cleanup(EventNotifier *);
int event_notifier_set(EventNotifier *);
int event_notifier_test_and_clear(EventNotifier *);
void event_notifier_set_handler(EventNotifier *e,
                                EventNotifierHandler *handler);
void aio_set_event_notifier(AioContext *ctx,
                            EventNotifier *notifier,
                            bool is_external,
                            EventNotifierHandler *io_read,
                            AioPollFn *io_poll);

最初にevent_notifier_init()でstruct EventNotifierを初期化する。eventfdを開く動作もこの中で行われる。event_notifier_cleanup()は、この逆にevent_notifierを破棄する。event_notifier_set()は、イベントを送信する関数で、event_notifier_test_and_clear()は、イベントを受信する関数である。event_notifer_set_handler()は、event_notiferとAioHandlerを組み合わせたもので、イベントを受信するとコールバックが呼び出される。これは、qemu_set_fd_handler()と同様iohandler_get_aio_context()のAioContextを利用する古い形式であるが、aio_set_event_notifier()は、AioContextを指定できる新しい形式である。

このeventfd、というか、event_notifierであるが、Qemuでは非常に多用している。ためしに手元のKVM仮想マシン (ホストubuntu 20.04、ゲストCentOS 7.9) に対応するqemuのlsofを取ってみると

# lsof -p 3965 | grep eventfd | wc -l
81

という感じだ。この大半は、ioeventfdと呼ばれる仕組みのために使われているが、これについては次回記事でご紹介する。

3.3 タイマー、Bottom half

タイマーはその名の通りである。指定した時間にコールバック関数が呼ばれるようスケジュールする。

まず、Qemuのクロックについて触れておく必要がある。

typedef enum {
    QEMU_CLOCK_REALTIME = 0,
    QEMU_CLOCK_VIRTUAL = 1,
    QEMU_CLOCK_HOST = 2,
    QEMU_CLOCK_VIRTUAL_RT = 3,
    QEMU_CLOCK_MAX
} QEMUClockType;

4種類のクロックがある。QEMU_CLOCK_HOSTが一番わかりやすく、ホストの時刻を指す。QEMU_CLOCK_REALTIMEは、ホストの時計が更新されようと (例えばNTPにより)、一定の速度で進む。QEMU_CLOCK_VIRTUALは、エミュレーションが中断すると止まる。QEMU_CLOCK_VIRTUAL_RTは、KVMとの組み合わせでは利用されないため割愛する。

int64_t qemu_clock_get_ns(QEMUClockType type);

qemu_clock_get_ns()は、typeで示されたクロックの値を得る。単位はナノ秒である。他に、qemu_clock_get_us()qemu_clock_get_ms()があり、それぞれマイクロ秒、ミリ秒単位の値を得る。

タイマー処理に使われる構造体が、QEMUTimerだ。これのリストがQEMUTimerListで、関連づけられたQEMUClockTypeが同じであるQEMUTimerのリスト構造である。さらに、QEMUClockTypeを添え字とする配列がQEMUTimerListGroupという名前で定義されている。

typedef struct QEMUTimerList QEMUTimerList;

typedef void QEMUTimerCB(void *opaque);
struct QEMUTimer {
    int64_t expire_time;        /* in nanoseconds */
    QEMUTimerList *timer_list;
    QEMUTimerCB *cb;
    void *opaque;
    QEMUTimer *next;
    int attributes;
    int scale;
};
typedef struct QEMUTimer QEMUTimer;

struct QEMUTimerListGroup {
    QEMUTimerList *tl[QEMU_CLOCK_MAX];
};
typedef struct QEMUTimerListGroup QEMUTimerListGroup;

struct QEMUTimerListはAPIとしては公開されていない。

QEMUTimerListGroupは、AioContextに含まれるほか、main_loop_tlgという名のグローバル変数としても存在しており、メインスレッドと結び付けられている。

void timer_init_full(QEMUTimer *ts,
                     QEMUTimerListGroup *timer_list_group, QEMUClockType type,
                     int scale, int attributes,
                     QEMUTimerCB *cb, void *opaque);
void timer_deinit(QEMUTimer *ts);
QEMUTimer *timer_new_full(QEMUTimerListGroup *timer_list_group,
                                        QEMUClockType type,
                                        int scale, int attributes,
                                        QEMUTimerCB *cb, void *opaque);
void timer_free(QEMUTimer *ts);

fullとついているのは、オプションを全部指定できる、という意味で、他にtimer_init()とかtimer_init_us()など、一部のオプションが規定されたような似たような関数がいくつかある。tsを指定の値で初期化するのがこれらの関数である。timer_list_groupを指定しないと、main_loop_tlgが指定されたことになり、コールバックcbはメインスレッドから呼ばれる。opaqueは、コールバック関数の引数である。

initとdeinitは、単に初期化と解除のみであるが、newとfreeではQEMUTimerオブジェクトの確保と解放も合わせて行う。QEMUTimerオブジェクトを他のオブジェクトに埋め込んだ場合はinit/deinitを、他のオブジェクトにはポインターのみを置く場合にはnew/freeを、それぞれ利用できる。

この段階ではまだタイムアウト値が指定されていないので発火することはなく、timer_mod()などでタイムアウト値を指定する必要がある。

void timer_mod(QEMUTimer *ts, int64_t expire_timer);
void timer_del(QEMUTimer *ts);

timer_del()は、タイマーの解除である。

AioContextと結びつけたタイマーを初期化するのは、aio_timer_init()などである。

void aio_timer_init(AioContext *ctx,
                    QEMUTimer *ts, QEMUClockType type,
                    int scale,
                    QEMUTimerCB *cb, void *opaque);

この場合、コールバックはAioContextと結び付けられたメインスレッドまたは追加のI/Oスレッドから呼ばれる。

一方、Bottom Halfは、即座に発火するタイマーと説明されている。

typedef struct QEMUBH QEMUBH;

中身はAPIとしては公開されておらず、いわゆるopaqueポインターである。コールバック関数をその場で実行するのと異なり、メインスレッド、または、AioContextと結び付けられたメインスレッドか追加のI/Oスレッドで実行される。

QEMUBH *aio_bh_new_full(AioContext *ctx, QEMUBHFunc *cb, void *opaque,
                        const char *name);

nameはデバッグ用の任意の名前である。タイマーの時と同じように、_fullに対してnameを省略した初期化マクロaio_bh_new() (コールバック関数名が名前になる) も用意されており、こちらの利用例の方が多い。_initという初期化のみのAPIは用意されていない。

3.5 スレッドプール

を説明しようと思ったが、そのためにはAioCBというヤツを説明する必要があることに気づいてしまった。長くなってしまったので、この先は次回としたい。

*1:virtioの文脈では、ゲストOSのデバイスドライバーをフロントエンドドライバー、ホスト側でフロントエンドドライバーの要求を処理する部分をバックエンドドライバー、と読んだりするので、注意が必要である。

*2:libvirtは、名前をvnetXにしたり、ブリッジに組み込んだりするために、qemu起動前にtapデバイスを作成し、Qemuに引き渡す。

*3:これを「I/Oスレッド」と呼ぶこともあるが、本稿では後述の「追加のI/Oスレッド」との混同を避けて「メインスレッド」と呼ぶことにする

*4:実は、QMP monitorのIO mon_iothreadスレッドは、追加のI/Oスレッドの仕組みを使って実装されている。このスレッドは、以下の説明にあるような明示的に指定したものとは別に自動的に生成される。

*5:poll(2)とはシグナルの扱いが異なる