QEMUのなかみ(QEMU internals) part1
ここ一ヶ月ほどQEMUのコードとお戯れしていたのですが、
qemuのソースコードもうすぐ読みきりそうなのでどこかにまとめたいんだけど、qemu internalみたいな記事ってどれぐらい需要あるの
— 前代未聞 (@RKX1209) 2015, 11月 9
と言ってみた所なんとなく需要がありそうだったので書きました。
本記事ではQEMUの内部実装を追い、具体的な仕組みを見ていきます。もし研究や仕事などでqemuを読む必要がある方や、これから趣味で読んでみようという方はぜひ参考にしてください。
(QEMU internalsというよりはQEMUコードリーディングの方が適切かもしれませんね....)
さてここで扱うQEMUはqemu2.4.0でゲストはx86,ホストはx64であると仮定します。
両方共x86系となるとDBTの意味はあまり無く、KVM使ってどうぞという話になるのですが、あくまでコードリーティングが目的であるため、よく知られたアーキテクチャを採用する事にしました。
(私がforkしたqemu2.4.0のレポジトリを置いておきます。コードを読む際に残したメモなどが書き込んであります。)github.com
また本記事は多少システムプログラムをかじったことがある人を対象にします。ですのでページング機構など、intelの基本的な仕様はある程度知っている事を前提とします。(そんな所まで全て1から説明していると本が1冊書けてしまうので...
またqemuの方もコア部分の基本的な処理のみに絞ります。全て書いてると本が1冊(ry
では早速見て行きましょう。
1.イベントループ
QEMUは仮想CPU上でのゲストコードの実行に加え、仮想デバイスからの割り込みやIOを監視し
内容に応じたコールバック関数を呼び出す必要があります。これはpoll(正確にはGlibのg_poll)を用いて実現されており、各file descriptorを監視し読み書き可能になればコールバックするようになっています。実際のコードを見てみましょう。
まずqemuのエントリポイントである(/vl.c)内のmain関数からmain_loop関数が呼ばれておりさらに(/main-loop.c)内のmain_loop_wait->os_host_main_loop_waitと続きます。
(/main-loop.c: os_host_main_loop_wait)
static int os_host_main_loop_wait(int64_t timeout) { ... glib_pollfds_fill(&timeout); [*1] ... ret = qemu_poll_ns((GPollFD *)gpollfds->data, gpollfds->len, timeout); [*2] glib_pollfds_poll(); return ret; }
[*1](/main-loop.c)
gcontextの作成とgpollfdsの初期化を行います(この辺の詳しい情報はGlibのreferenceを参照してください)
[*2](/qemu-timer.c)
g_pollのwrapperです。ここで実際のpollを行い読み書き可能なfile descriptorを返します。
コールバック関数の登録はqemu_set_fd_handler(/iohandler.c)を使用します。
(/iohandler.c: qemu_set_fd_handler)
void qemu_set_fd_handler(int fd, IOHandler *fd_read, IOHandler *fd_write, void *opaque) { iohandler_init(); aio_set_fd_handler(iohandler_ctx, fd, fd_read, fd_write, opaque); }
引数は、登録するfd,fdに(read|write)する時のコールバック関数,ハンドラに渡すデータ
です。実質の処理はaio_set_fd_handlerにあります。
(/aio-posix.c: aio_set_fd_handler)
void aio_set_fd_handler(AioContext *ctx, int fd, IOHandler *io_read, IOHandler *io_write, void *opaque) { AioHandler *node; node = find_aio_handler(ctx, fd); /* Are we deleting the fd handler? */ if (!io_read && !io_write) { ... } else { if (node == NULL) { /* Alloc and insert if it's not already there */ node = g_new0(AioHandler, 1); node->pfd.fd = fd; QLIST_INSERT_HEAD(&ctx->aio_handlers, node, node); [*1] g_source_add_poll(&ctx->source, &node->pfd); [*2] } ... }
[*1][*2]
新たなノードをAioContextのaio_handlersにつなぎGSourceを初期化しています。
GSourceはpollする先のソースを表しており(この場合はnode->pfdで表されるfile descriptor)先程glib_pollfds_fillで初期化しておいたGContextに関連付ける必要があります。
これはmain-loop.c内のqemu_init_main_loopで行われています。
int qemu_init_main_loop(Error **errp) { ... src = iohandler_get_g_source(); g_source_attach(src, NULL);//attach iohandler_ctx(iohandler.c) g_source_unref(src); return 0; }
g_source_attachでAioContext内のGSourceを関連付けています。
これでqemu_set_fd_handlerで登録されたコールバック関数が呼ばれるようになります。
2.QOM(QEMU Object Model)
では次にQEMUのデバイスモデルで使用されているQOMについて見ていきます。
QOMとはQEMU Object Modelの略で、大雑把に言うとCで書かれたQEMUのコード内で、
オブジェクト指向を実現するための機構です。モデルとしてはsysfsのkobjectに
似ているためlinux hackerな人達には馴染みあるかもしれません。
例えばPCIバスはPCIBusクラス(実体は構造体)で表されますがこれはBusStateを"継承"しています。
また"ポリモーフィズム"も実装されており例えばMachineStateを継承したPCMachineStateをMachineState型の引数に渡し、内部でPCMachineStateのメンバにアクセスする事ができます。
これらがどのように実装されているか見ていきます。
クラスに共通して必要な要素としてまずコンストラクタ,デストラクタを見てみます。これらはTypeImplという構造体に関数ポインタとして定義されています。
(/qom/object.c: struct TypeImpl)
struct TypeImpl { const char *name; size_t class_size; size_t instance_size; void (*class_init)(ObjectClass *klass, void *data); void (*class_base_init)(ObjectClass *klass, void *data); void (*class_finalize)(ObjectClass *klass, void *data); void *class_data; void (*instance_init)(Object *obj); void (*instance_post_init)(Object *obj); void (*instance_finalize)(Object *obj); bool abstract; const char *parent; TypeImpl *parent_type; ObjectClass *class; int num_interfaces; InterfaceImpl interfaces[MAX_INTERFACES];//implemented interfaces };
instance_init,instance_finalizeというのがそれぞれ、クラスのインスタンスが作成される際呼び出されるコンストラクタ,デストラクタの役割を担っています。
またclass_init,class_finalizeはクラスが初期化される際一度だけ呼びだされます。これはclassのstaticなメンバーなどを初期化する際に使用されます。
では次に全てのクラスの親となるルートクラスObjectClassを見てみましょう。
(/include/qom/object.h: struct ObjectClass)
struct ObjectClass { /*< private >*/ Type type; GSList *interfaces; const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE]; const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE]; ObjectUnparent *unparent; };
Typeというのはstruct TypeImplをtypedefした物です。(以後TypeImplの事もTypeと呼びます)
メンバ関数は関数ポインタで表されます。
ObjectClassの場合、Type構造体を持っておりこれでコンストラクタ,デストラクタをメンバ関数として持っている状態です。ObjectClassにはこれら以外のメンバ関数はありません。
ではこのObjectClassのインスタンス(の雛形)struct Objectを見てみます。
(/include/qom/object.h: struct Object)
struct Object { /*< private >*/ ObjectClass *class; ObjectFree *free; QTAILQ_HEAD(, ObjectProperty) properties; uint32_t ref; Object *parent; };
インスタンスはクラスObjectClassへのポインタを持っています。このObjectClassが実際にはPCIBusやMachineStateなどのクラスになります。それ以外のメンバはインスタンス特有のメンバになります。
では継承の実装を見て行きます。
一般にQOMに限らずCで継承を表現するのは単純で、継承元の親クラスを子クラスが持つ事で実現しています。例えばBusStateを継承したPCIBusは以下のように第一要素にBusStateを持っています。
struct PCIBus {
BusState qbus;
...
}
これで親クラスの全てのメンバを引き継いでいます。
次にQOMの要であるポリモーフィズムですがこれは単純な型キャストを用いて実現されています。実際にポリモーフィズムが使用されている所を見てみます。
(/hw/i386/pc_piix.c: pc_init1)
static void pc_init1(MachineState *machine, const char *host_type, const char *pci_type) { PCMachineState *pcms = PC_MACHINE(machine); ... }
PCMachineStateはMachineStateを継承しています。引数としてはMachineStateを渡していますが、中でPC_MACHINEというマクロを利用し継承先のPCMachineStateクラスに変換しています。
(/include/hw/i386/pc.h: struct PCMachineState)
struct PCMachineState { /*< private >*/ MachineState parent_obj; ... }
ではPC_MACHINEマクロの定義を見てみましょう。
(/include/hw/i386/pc.h: PC_MACHINE)
#define PC_MACHINE(obj) \ OBJECT_CHECK(PCMachineState, (obj), TYPE_PC_MACHINE) (/include/qom/object.h: OBJECT_CHECK) #define OBJECT_CHECK(type, obj, name) \ ((type *)object_dynamic_cast_assert(OBJECT(obj), (name), \ __FILE__, __LINE__, __func__))
なんだかめんどくさそうな関数がありますが、要はOBJECT_CHECK(type,obj,name)で
obj構造体をtype構造体に型キャストして返すだけです。MachineStateをPCMachineStateにキャストすることで元々アクセスできなかったparent_objよりも下のオブジェクトにアクセスする事ができます。
これがポリモーフィズムの実体です。同じメモリ領域をどの構造体の型で解釈するかの違いだけです。
基本要素の説明が終わったので実際にクラスのインスタンスを作成するためのobject_new関数の実装を追っていきます。
(/qom/object.c)
Object *object_new(const char *typename) { TypeImpl *ti = type_get_by_name(typename); [*1] return object_new_with_type(ti); }
[*1]
typenameで指定された名前の型をハッシュテーブルから取得します。型の情報がいつここに登録されているかは後ほど説明します。
Object *object_new_with_type(Type type) { Object *obj; g_assert(type != NULL); type_initialize(type); [*2] obj = g_malloc(type->instance_size); [*3] object_initialize_with_type(obj, type->instance_size, type); [*4] obj->free = g_free; return obj; }
[*2]
typeからクラスを作成し、クラス初期化関数(class_init)を呼び出します。
[*3]
クラスのインスタンスを確保します。
[*4]
確保したインスタンスの初期化処理を行います。コンストラクタ(instance_init)を呼び出します。
/* Init type class. */ static void type_initialize(TypeImpl *ti) { TypeImpl *parent; if (ti->class) { return; } ti->class_size = type_class_get_size(ti); ti->instance_size = type_object_get_size(ti); ti->class = g_malloc0(ti->class_size); [*5] parent = type_get_parent(ti); if (parent) { type_initialize(parent); [*6] GSList *e; int i; g_assert(parent->class_size <= ti->class_size); memcpy(ti->class, parent->class, parent->class_size); [*7] ti->class->interfaces = NULL; ... } ti->class->type = ti; ... if (ti->class_init) { ti->class_init(ti->class, ti->class_data); [*8] } }
[*5]
実際のクラスを作成します。
[*6]
再帰的に親クラスを辿り初期化していきます。
[*7]
親クラスの初期化が完了したので第一要素の親クラスメンバに実体をコピーします。
[*8]
クラスの初期化関数を呼び出します。
void object_initialize_with_type(void *data, size_t size, TypeImpl *type) { Object *obj = data; g_assert(type != NULL); type_initialize(type); ... memset(obj, 0, type->instance_size); obj->class = type->class; [*9] object_ref(obj); QTAILQ_INIT(&obj->properties); object_init_with_type(obj, type);[*10] object_post_init_with_type(obj, type); } static void object_init_with_type(Object *obj, TypeImpl *ti) { if (type_has_parent(ti)) { object_init_with_type(obj, type_get_parent(ti)); } if (ti->instance_init) { ti->instance_init(obj); } }
[*9]
インスタンスobjがクラスをポイントするようにします。これでようやくtype->classクラスのインスタンスという意味を持つようになります。
[*10]
再帰的にコンストラクタを呼び出します。
では新しいType型を定義し登録する方法を見ていきます。
(/include/qom/object.h: コメント中のサンプルコード)
#include "qdev.h" #define TYPE_MY_DEVICE "my-device" // No new virtual functions: we can reuse the typedef for the // superclass. typedef DeviceClass MyDeviceClass; typedef struct MyDevice { DeviceState parent; int reg0, reg1, reg2; } MyDevice; static const TypeInfo my_device_info = { .name = TYPE_MY_DEVICE, .parent = TYPE_DEVICE, .instance_size = sizeof(MyDevice), }; static void my_device_register_types(void) { type_register_static(&my_device_info);[*3] } type_init(my_device_register_types) [*1]
TypeInfoというのはTypeの簡易版で登録の際にTypeに変換されます。
このTypeInfoに新たに定義したいクラスの情報を持たせます。
型の登録は[*1]のtype_initマクロを利用します。
(/include/qemu/module.h )
#define module_init(function, type) \ static void __attribute__((constructor)) do_qemu_init_ ## function(void) \ { \ register_module_init(function, type); [*2] \ } #endif typedef enum { MODULE_INIT_BLOCK, MODULE_INIT_MACHINE, MODULE_INIT_QAPI, MODULE_INIT_QOM, MODULE_INIT_MAX } module_init_type; #define block_init(function) module_init(function, MODULE_INIT_BLOCK) #define machine_init(function) module_init(function, MODULE_INIT_MACHINE) #define qapi_init(function) module_init(function, MODULE_INIT_QAPI) #define type_init(function) module_init(function, MODULE_INIT_QOM)
type_initマクロはmodule_initマクロにQOMタイプを渡している事が分かります。
module_initマクロには__attribute__*1が付いているためこれはmain関数よりも前に実行されます。
[*2]のregister_module_init(function, type)(util/module.c)はMODULE_INIT_タイプごとに用意されたModuleTypeListにfunctionをModuleTypeEntryとして追加します。
(util/module.c: register_module_init)
void register_module_init(void (*fn)(void), module_init_type type) { ModuleEntry *e; ModuleTypeList *l; e = g_malloc0(sizeof(*e)); e->init = fn; e->type = type; l = find_type(type); QTAILQ_INSERT_TAIL(l, e, node); }
つまりtype_initマクロを使用すればmain関数より先に、指定された関数がModuleTypeListに追加されていく訳です。この登録されたModuleEntry->initはvl.cのmain関数内にてmodule_call_init(MODULE_INIT_QOM)関数を呼び出すことで順次実行されます。
(util/module.c: module_call_init)
void module_call_init(module_init_type type)
{
ModuleTypeList *l;
ModuleEntry *e;
module_load(type);
l = find_type(type);
QTAILQ_FOREACH(e, l, node) {
e->init();
}
}
実際に呼び出される関数は[3]のtype_register_staticです。ではこいつは何をしているかと言うと
(/qom/object.c)
static TypeImpl *type_register_internal(const TypeInfo *info) { TypeImpl *ti; ti = type_new(info); type_table_add(ti); return ti; } TypeImpl *type_register(const TypeInfo *info) { assert(info->parent); return type_register_internal(info); } TypeImpl *type_register_static(const TypeInfo *info) { return type_register(info); }
type_newでTypeInfoで指定された新しいType型を作成し、type_table_addでtype_tableという<型の名前,Type>のハッシュテーブルに登録します。
QOMの基本要素となるType,ObjectClass,Objectの説明が終わったので次はこれらを実際に使ったハードウェアエミュレーションの実装を見ていきます。
3.ハードウェアエミュレーション
QEMUでは-machineオプションでエミュレートするマシンを選択する事ができます。今回はデフォルトのpc-i440fx-2.5を見ていきます。
pc-i440fx-2.5は名前の通りi440fxおよびpiix4(正確にはpiix3に近いのですが)をチップセットとしたマシンです。全体的なアーキテクチャは以下のようになっています
QEMU wikiより
Features/Q35 - QEMU
- machineで指定したマシンタイプはMachineClassクラスで表されます。実際にオプションをパースしている部分を見てみましょう。
(/vl.c)
MachineClass *machine_class; ... opts = qemu_get_machine_opts(); optarg = qemu_opt_get(opts, "type"); if (optarg) { machine_class = machine_parse(optarg); }
optargには-machineオプションで指定された値が入り、それをmachine_parseに渡しています。
(/vl.c: machine_parse)
static MachineClass *machine_parse(const char *name) { ... if (name) { mc = find_machine(name); } if (mc) { g_slist_free(machines); return mc; } ... } static MachineClass *find_machine(const char *name) { GSList *el, *machines = object_class_get_list(TYPE_MACHINE, false); MachineClass *mc = NULL; for (el = machines; el; el = el->next) { MachineClass *temp = el->data; if (!strcmp(temp->name, name)) { mc = temp; break; } if (temp->alias && !strcmp(temp->alias, name)) { mc = temp; break; } } g_slist_free(machines); return mc; }
指定された名前と一致するMachineClassクラスを取得します。
(なおここではMachineClassとして取得していますが、正確にはpc-i440fx-2.5の場合、MachineClassを継承したPCMachineClassです。)
次にMachineClassのインスタンスを作成します。このインスタンスはObjectを継承したMachineState構造体で表されます。QOMのインスタンスの作り方はあまり直感的でないので慣れるまで少し分かりづらいと思います。
(/vl.c)
MachineState *current_machine; ... current_machine = MACHINE(object_new(object_class_get_name( OBJECT_CLASS(machine_class))));
これは何をやっているかというとまずmachine_classを一旦親のObjectClassとして解釈し、ObjectClass->type->nameをobject_newに渡してインスタンスObjectを作成し、MachineStateとして解釈し直します。これでMachineClassのインスタンスcurrent_machineができました。(これも正確にはMachineStateではなくPCMachineStateです。ObjectClass->type->nameがPCMachineClassを指示しているためobject_newでPCMachineStateが作成されているわけです)
ではこれらPCMachineInitクラスが登録されている所を見てみましょう。
(hw/i386/pc_piix.c)
#define DEFINE_I440FX_MACHINE(suffix, name, compatfn, optionfn) \ static void pc_init_##suffix(MachineState *machine) \ { \ void (*compat)(MachineState *m) = (compatfn); \ if (compat) { \ compat(machine); \ } \ pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \ TYPE_I440FX_PCI_DEVICE); \ [*4] } \ DEFINE_PC_MACHINE(suffix, name, pc_init_##suffix, optionfn) [*3] static void pc_i440fx_2_5_machine_options(MachineClass *m) { pc_i440fx_machine_options(m); m->alias = "pc"; m->is_default = 1; [*2] } DEFINE_I440FX_MACHINE(v2_5, "pc-i440fx-2.5", NULL, pc_i440fx_2_5_machine_options); [*1]
[*1]のマクロでクラスを登録しています。また[*2]でMachineClass->is_defaultに1が入っているため、この"pc-i440fx-2.5"が-machineオプションを指定しなかった際のデフォルト選択になる事が分かります。実際の内部的な登録処理は[*3]のマクロで行われています。
(/include/hw/i386/pc.h)
#define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) \ static void pc_machine_##suffix##_class_init(ObjectClass *oc, void *data) \ { \ MachineClass *mc = MACHINE_CLASS(oc); \ optsfn(mc); \ mc->name = namestr; \ mc->init = initfn; [*7]\ } \ static const TypeInfo pc_machine_type_##suffix = { \ .name = namestr TYPE_MACHINE_SUFFIX, \ .parent = TYPE_PC_MACHINE, \ .class_init = pc_machine_##suffix##_class_init, \ [*6] }; \ static void pc_machine_init_##suffix(void) \ { \ type_register(&pc_machine_type_##suffix); \ [*5] } \ machine_init(pc_machine_init_##suffix) [*4]
[*4]のmachine_initは先程type_initを説明した際のコードに出てきています。動作としてはtype_initとほぼ同じですが渡す型のタイプが違うだけです。
[*5]で型の登録をしていますね。これは先程示したQOMの型定義の例と同じフォーマットになっている事が分かります。
さて実際のクラス初期化関数は[*6]で設定しています。この関数は[*7]でMachineClass->initに指定された関数、この場合はpc_init_##suffixを設定するだけです。
ではこのMachineClass->initはいつ呼び出されるのでしょうか。
これはvl.c内でcurrent_machineを作成した後しっかり呼び出されています。
(/vl.c)
machine_class->init(current_machine);
pc_init_##suffix内ではpc_init1が呼び出されています。これがマシンの実質的な初期化処理を担っています。
(/hw/i386/pc_piix.c)
static void pc_init1(MachineState *machine, const char *host_type, const char *pci_type) { PCMachineState *pcms = PC_MACHINE(machine); /* system main memory space */ MemoryRegion *system_memory = get_system_memory(); MemoryRegion *system_io = get_system_io(); int i; PCIBus *pci_bus; ISABus *isa_bus; PCII440FXState *i440fx_state; int piix3_devfn = -1; qemu_irq *gsi; qemu_irq *i8259; qemu_irq smi_irq; GSIState *gsi_state; DriveInfo *hd[MAX_IDE_BUS * MAX_IDE_DEVS]; BusState *idebus[MAX_IDE_BUS]; ISADevice *rtc_state; MemoryRegion *ram_memory; MemoryRegion *pci_memory; MemoryRegion *rom_memory; PcGuestInfo *guest_info; ram_addr_t lowmem; ... pc_cpus_init(machine->cpu_model); [*1] ... if (pci_enabled) { pci_memory = g_new(MemoryRegion, 1); memory_region_init(pci_memory, NULL, "pci", UINT64_MAX); [*2] rom_memory = pci_memory; } ... if (!xen_enabled()) { pc_memory_init(pcms, system_memory, rom_memory, &ram_memory, guest_info); [*3] } ... }
結構長いので章立てて見ていきます。まずは[*1]のcpuの初期化から。
4.仮想CPU
(/hw/i386/pc.c: pc_cpus_init)
void pc_cpus_init(const char *cpu_model) { int i; X86CPU *cpu = NULL; Error *error = NULL; unsigned long apic_id_limit; current_cpu_model = cpu_model; ... for (i = 0; i < smp_cpus; i++) { cpu = pc_new_cpu(cpu_model, x86_cpu_apic_id_from_index(i), &error); ... } ... } static X86CPU *pc_new_cpu(const char *cpu_model, int64_t apic_id, Error **errp) { X86CPU *cpu = NULL; ... cpu = cpu_x86_create(cpu_model, &local_err);[*4] ... object_property_set_int(OBJECT(cpu), apic_id, "apic-id", &local_err); object_property_set_bool(OBJECT(cpu), true, "realized", &local_err);[*5] return cpu; }
[*4]で新しいCPUのインスタンスを作成しています。X86CPUはX86CPUClassクラス(target-i386/cpu-qom.h)のインスタンスでOBjectClass->DeviceClass(include/hw/qdev-core.h)->CPUClass(include/qom/cpu.h)->X86CPUClassという継承関係になっています。
で重要なのが[*5]でこれはcpuの'realized'プロパティにtrueを設定しています。QOMにおけるプロパティは文字列をキーにした連想配列でセッタとゲッタを持っています。realizedのセッタは(hw/core/qdev.c)で登録されています。
(hw/core/qdev.c)
static void device_initfn(Object *obj) { DeviceState *dev = DEVICE(obj); ObjectClass *class; Property *prop; ... object_property_add_bool(obj, "realized", device_get_realized, device_set_realized, NULL);[*7] ... } ... static const TypeInfo device_type_info = { ... .instance_init = device_initfn,[*8] ... }; ... static void qdev_register_types(void) { type_register_static(&bus_info); type_register_static(&device_type_info);[*6] } ... type_init(qdev_register_types)
一番下のtype_initでqdev関係の型を宣言しています。具体的には[*6]でDeviceClassとDeviceStateの型をtype_registerし、型情報TypeInfoにおいてコンストラクタにdevice_initfnを指定しています。[*7]
device_initfnでは[*8]でインスタンスにreazliedプロパティを追加しています。このセッタの部分device_set_realizedを見てみましょう。
(/hw/core/qdev.c: device_set_realized)
static void device_set_realized(Object *obj, bool value, Error **errp) { DeviceState *dev = DEVICE(obj); DeviceClass *dc = DEVICE_GET_CLASS(dev); HotplugHandler *hotplug_ctrl; BusState *bus; Error *local_err = NULL; ... if (value && !dev->realized) { //value == true ... if (dc->realize) { dc->realize(dev, &local_err);[*9] } } ... }
重要なのは[*9]でDeviceClassのrealizeメンバを呼び出しています。つまりrealizedプロパティにtrueを設定するとこのrealizedメンバが呼び出されるわけです。
では先程のpc_new_cpuに戻りましょう。結局X86CPUのreazliedメンバが呼び出されることになるのですが、実際にどの関数が呼ばれるのか調べてみましょう。
(target-i386/cpu.c)
static void x86_cpu_common_class_init(ObjectClass *oc, void *data) { /* Cast ObjectClass to each classes (inheritance) means, x86_cpu is classfied as X86CPUClass and CPUClass and DeviceClass*/ X86CPUClass *xcc = X86_CPU_CLASS(oc); CPUClass *cc = CPU_CLASS(oc); DeviceClass *dc = DEVICE_CLASS(oc); xcc->parent_realize = dc->realize; dc->realize = x86_cpu_realizefn; [*10] }
[*10]でrealizeにx86_cpu_realizefnが設定されている事が分かります。
(target-i386/cpu.c: x86_cpu_realizefn)
static void x86_cpu_realizefn(DeviceState *dev, Error **errp) { CPUState *cs = CPU(dev); X86CPU *cpu = X86_CPU(dev); X86CPUClass *xcc = X86_CPU_GET_CLASS(dev); CPUX86State *env = &cpu->env; Error *local_err = NULL; static bool ht_warned; ... qemu_init_vcpu(cs);[*11] ... x86_cpu_apic_realize(cpu, &local_err); if (local_err != NULL) { goto out; } cpu_reset(cs); xcc->parent_realize(dev, &local_err); out: if (local_err != NULL) { error_propagate(errp, local_err); return; } }
[*11]でvcpuの初期化を行います。
(cpus.c)
static void qemu_tcg_init_vcpu(CPUState *cpu) { char thread_name[VCPU_THREAD_NAME_SIZE]; static QemuCond *tcg_halt_cond; static QemuThread *tcg_cpu_thread; tcg_cpu_address_space_init(cpu, cpu->as); /* share a single thread for all cpus with TCG */ if (!tcg_cpu_thread) { cpu->thread = g_malloc0(sizeof(QemuThread));//not share ... /* Create TCG thread proceeding qemu_tcg_cpu_thread_fn */ qemu_thread_create(cpu->thread, thread_name, qemu_tcg_cpu_thread_fn, cpu, QEMU_THREAD_JOINABLE);[*13] ... tcg_cpu_thread = cpu->thread; } ... } void qemu_init_vcpu(CPUState *cpu) { cpu->nr_cores = smp_cores; cpu->nr_threads = smp_threads; cpu->stopped = true; if (kvm_enabled()) { qemu_kvm_start_vcpu(cpu); } else if (tcg_enabled()) { qemu_tcg_init_vcpu(cpu); [*12] } else { qemu_dummy_start_vcpu(cpu); } }
[*12]でTCGの初期化を行います。TCG用のスレッドを[*13]で作成しqemu_tcg_cpu_thread_fn関数を登録しています。
(cpus.c)
static void *qemu_tcg_cpu_thread_fn(void *arg) { CPUState *cpu = arg; ... CPU_FOREACH(cpu) { cpu->thread_id = qemu_get_thread_id(); cpu->created = true; cpu->can_do_io = 1; } qemu_cond_signal(&qemu_cpu_cond); /* wait for initial kick-off after machine start */ while (first_cpu->stopped) { [*14] qemu_cond_wait(first_cpu->halt_cond, &qemu_global_mutex); /* process any pending work */ CPU_FOREACH(cpu) { qemu_wait_io_event_common(cpu); } } /* process any pending work */ atomic_mb_set(&exit_request, 1); while (1) { tcg_exec_all();[*15] ... qemu_tcg_wait_io_event(QTAILQ_FIRST(&cpus)); } return NULL; }
他のデバイスの初期化がまだ未完了なため、[*14]でcpuが起動されるまで待ちます。[*15]からいよいよホストマシンコードの実行が始まりますがこれは後で説明します。
ではpc_init1に戻って次は[*2][*3]の仮想メモリの初期化を見ていきます。
5.仮想メモリ(SoftMMU)
QEMUではメモリ空間のアドレスをアドレスをキーとしたradix treeとして管理しています。(linuxもそうですね)
以下の図を見てください。
KVM Forum2013より
http://www.linux-kvm.org/images/1/17/Kvm-forum-2013-Effective-multithreading-in-QEMU.pdf
address_space_memoryはAddressSpace構造体として宣言されています。
(include/exec/memory.h)
struct AddressSpace { /* All fields are private. */ struct rcu_head rcu; char *name; MemoryRegion *root; int ioeventfd_nb; struct AddressSpaceDispatch *dispatch; [*1] struct AddressSpaceDispatch *next_dispatch; MemoryListener dispatch_listener; QTAILQ_ENTRY(AddressSpace) address_spaces_link; };
[*1]にあるAddressSpaceDispatchがradix treeの実体です。
MemoryRegionはradix treeの中間ノード(つまりキーに対して値が無いノードです)を表しており、MemoryRegionSectionがキーを表すノードです。
ちなみに図中のoffset=0x1fc00000のMemoryRegionはさらに2つほどMemoryRegionがネストしていますね。これはMemoryRegionのaliasやcontainerのsubregionを使用しているためです。詳しくは(docs/memory.txt)を見てください。
では次にsoftmmuの処理を見ていきます。
softmmuはゲストの仮想アドレス(GVA)からゲストの物理アドレス(GPA)を経てホストの仮想アドレス(HVA)へ変換する処理を担います。この変換結果はCPUTLBEntryにキャッシュされます。名前の通りTLBの役割をしています。
(include/exec/cpu-defs.h)
typedef struct CPUTLBEntry { union { struct { target_ulong addr_read; target_ulong addr_write; target_ulong addr_code; uintptr_t addend; }; uint8_t dummy[1 << CPU_TLB_ENTRY_BITS]; }; } CPUTLBEntry;
addend = host_virtual_address – guest_virtual_address
という関係が成り立っています。つまりaddendはGVAに対するHVAのオフセットです。これでアドレス変換を行っています。
CPUTLBEntryはCPUX86State(target-i386/cpu.h)がメンバとして持っており実体は(include/exec/cpu-defs.h)にて定義されています。
(include/exec/cpu-defs.h)
#define CPU_COMMON_TLB \ /* The meaning of the MMU modes is defined in the target code. */ \ CPUTLBEntry tlb_table[NB_MMU_MODES][CPU_TLB_SIZE];
NB_MMU_MODESにはカーネルモードかユーザーモードが入り、これらモードごとにCPU_TLB_SIZE個のTLBEntryが用意されています。
では実際にGVAからHVAに変換する関数get_page_addr_codeを見てみましょう。
(cputlb.c)
tb_page_addr_t get_page_addr_code(CPUArchState *env1, target_ulong addr) { int mmu_idx, page_index, pd; void *p; MemoryRegion *mr; CPUState *cpu = ENV_GET_CPU(env1); page_index = (addr >> TARGET_PAGE_BITS) & (CPU_TLB_SIZE - 1); mmu_idx = cpu_mmu_index(env1, true); [*2] if (unlikely(env1->tlb_table[mmu_idx][page_index].addr_code != (addr & TARGET_PAGE_MASK))) { cpu_ldub_code(env1, addr); [*3] } ... p = (void *)((uintptr_t)addr + env1->tlb_table[mmu_idx][page_index].addend); [*4] return qemu_ram_addr_from_host_nofail(p); }
第2引数のaddrがGVAです。また第1引数のCPUArchStateはCPUX86Stateをdefineしただけの別名です。
まず[*2]で現在のモードに合わせてTLBEntryのインデックスを求めています。
次にTLBEntryにキャッシュが存在しない場合は[*3]でいわゆるページフォールトを発生させて新たなページを確保し、TLBEntryにキャッシュします。ではこのcpu_ldub_codeを追っていきましょう。
(include/exec/cpu_ldst_template.h)
static inline RES_TYPE glue(glue(cpu_ld, USUFFIX), MEMSUFFIX)(CPUArchState *env, target_ulong ptr) { return glue(glue(glue(cpu_ld, USUFFIX), MEMSUFFIX), _ra)(env, ptr, 0); }
はいこれがcpu_ldub_codeの定義です。glueというのは2つのマクロを連結するマクロです。お世辞にも分かりやすいとは言えないコードですが、関数名にマクロを使用し、USUFFIXやMEMSUFFIXを適時変更することでテンプレートから関数名を作成しているわけです。このマクロを展開するとcpu_ldub_codeになります。またこの関数からはcpu_ldub_code_raが呼ばれています。
(include/exec/cpu_ldst_template.h)
static inline RES_TYPE glue(glue(glue(cpu_ld, USUFFIX), MEMSUFFIX), _ra)(CPUArchState *env, target_ulong ptr, uintptr_t retaddr) { int page_index; RES_TYPE res; target_ulong addr; int mmu_idx; TCGMemOpIdx oi; addr = ptr; page_index = (addr >> TARGET_PAGE_BITS) & (CPU_TLB_SIZE - 1); mmu_idx = CPU_MMU_INDEX; if (unlikely(env->tlb_table[mmu_idx][page_index].ADDR_READ != (addr & (TARGET_PAGE_MASK | (DATA_SIZE - 1))))) { oi = make_memop_idx(SHIFT, mmu_idx); res = glue(glue(helper_ret_ld, URETSUFFIX), MMUSUFFIX)(env, addr, oi, retaddr); [*5] } return res; }
[*5]はhelper_ret_ldl_cmmuになります。これは(tcg/tcg.h)内でhelper_be_ldl_cmmuの別名としてdefineされています。でこのhelper_be_ldl_cmmuは(softmmu_template.h)で
(softmmu_template.h)
# define helper_le_ld_name glue(glue(helper_ret_ld, USUFFIX), MMUSUFFIX)
とすることで結局helper_le_ld_nameという名前で扱われています。ややこしいですね...
さてhelper_le_ld_nameの処理を見て行きましょう。
WORD_TYPE helper_le_ld_name(CPUArchState *env, target_ulong addr, TCGMemOpIdx oi, uintptr_t retaddr) { unsigned mmu_idx = get_mmuidx(oi); int index = (addr >> TARGET_PAGE_BITS) & (CPU_TLB_SIZE - 1); target_ulong tlb_addr = env->tlb_table[mmu_idx][index].ADDR_READ; uintptr_t haddr; DATA_TYPE res; /* Adjust the given return address. */ retaddr -= GETPC_ADJ; /* If the TLB entry is for a different page, reload and try again. */ if ((addr & TARGET_PAGE_MASK) != (tlb_addr & (TARGET_PAGE_MASK | TLB_INVALID_MASK))) { ... if (!VICTIM_TLB_HIT(ADDR_READ)) { tlb_fill(ENV_GET_CPU(env), addr, READ_ACCESS_TYPE, mmu_idx, retaddr); [*6] } tlb_addr = env->tlb_table[mmu_idx][index].ADDR_READ; } }
TLBキャッシュに無い場合[*6]のtlb_fillで新たにページを確保しTLBにキャッシュしています。
(target-i386/mem_helper.c: tlb_fill)
void tlb_fill(CPUState *cs, target_ulong addr, int is_write, int mmu_idx, uintptr_t retaddr) { int ret; ret = x86_cpu_handle_mmu_fault(cs, addr, is_write, mmu_idx);[*7] ... }
[*7]でページフォルトを処理します。
(target-i386/helper.c: x86_cpu_handle_mmu_fault)
int x86_cpu_handle_mmu_fault(CPUState *cs, vaddr addr, int is_write1, int mmu_idx) { X86CPU *cpu = X86_CPU(cs); CPUX86State *env = &cpu->env; uint64_t ptep, pte; target_ulong pde_addr, pte_addr; int error_code = 0; int is_dirty, prot, page_size, is_write, is_user; hwaddr paddr; uint64_t rsvd_mask = PG_HI_RSVD_MASK; uint32_t page_offset; target_ulong vaddr; is_user = mmu_idx == MMU_USER_IDX; ... if (!(env->cr[0] & CR0_PG_MASK)) { pte = addr; [*8] #ifdef TARGET_X86_64 if (!(env->hflags & HF_LMA_MASK)) { /* Without long mode we can only address 32bits in real mode */ pte = (uint32_t)pte; } #endif prot = PAGE_READ | PAGE_WRITE | PAGE_EXEC; page_size = 4096; goto do_mapping; } else { uint32_t pde; /* page directory entry */ pde_addr = ((env->cr[3] & ~0xfff) + ((addr >> 20) & 0xffc)) & env->a20_mask; pde = x86_ldl_phys(cs, pde_addr); [*9] if (!(pde & PG_PRESENT_MASK)) { goto do_fault; } ptep = pde | PG_NX_MASK; ... /* page directory entry */ pte_addr = ((pde & ~0xfff) + ((addr >> 10) & 0xffc)) & env->a20_mask; pte = x86_ldl_phys(cs, pte_addr); [*10] if (!(pte & PG_PRESENT_MASK)) { goto do_fault; } /* combine pde and pte user and rw protections */ ptep &= pte | PG_NX_MASK; page_size = 4096; rsvd_mask = 0; } do_mapping: pte = pte & env->a20_mask; /* align to page_size */ pte &= PG_ADDRESS_MASK & ~(page_size - 1); /* Even if 4MB pages, we map only one 4KB page in the cache to avoid filling it too fast */ vaddr = addr & TARGET_PAGE_MASK; page_offset = vaddr & (page_size - 1); paddr = pte + page_offset; tlb_set_page_with_attrs(cs, vaddr, paddr, cpu_get_mem_attrs(env), prot, mmu_idx, page_size);[*11] return 0; ... }
ページングが無効な場合[*8]でストレートにHVAを対応させています。(ちなみにlinuxカーネルもブートして少しの間はページング無効です まあブートストラップ的に当たり前ですが)
では次にページングが有効な場合、[*9],[*10]でそれぞれPDE(PageTableEntry)とPTE(PageTableEntry)のアドレスを計算ています。OSなどで定番な処理なので見慣れてる人もいると思います。
さてここで得たアドレスはホストのページテーブル中のアドレスであるためGPAです。x86_ldl_phys()を使用してHVAに変換し実際の値を取り出します。
(target-i386/helper.c: x86_ldl_phys)
uint32_t x86_ldl_phys(CPUState *cs, hwaddr addr) { X86CPU *cpu = X86_CPU(cs); CPUX86State *env = &cpu->env; return address_space_ldl(cs->as, addr, cpu_get_mem_attrs(env), NULL); } (exec.c: address_space_ldl) uint32_t address_space_ldl(AddressSpace *as, hwaddr addr, MemTxAttrs attrs, MemTxResult *result) { return address_space_ldl_internal(as, addr, attrs, result, DEVICE_NATIVE_ENDIAN); } static inline uint32_t address_space_ldl_internal(AddressSpace *as, hwaddr addr, MemTxAttrs attrs, MemTxResult *result, enum device_endian endian) { uint8_t *ptr; uint64_t val; MemoryRegion *mr; hwaddr l = 4; hwaddr addr1; rcu_read_lock(); mr = address_space_translate(as, addr, &addr1, &l, false);[*12] ... else { /* RAM case */ ptr = qemu_get_ram_ptr((memory_region_get_ram_addr(mr) & TARGET_PAGE_MASK) + addr1);[*13] switch (endian) { case DEVICE_LITTLE_ENDIAN: val = ldl_le_p(ptr); break; case DEVICE_BIG_ENDIAN: val = ldl_be_p(ptr); break; default: val = ldl_p(ptr); break; } r = MEMTX_OK; } if (result) { *result = r; } ... rcu_read_unlock(); return val; }
[*12]のaddress_space_translateは与えられたGPAからそれが含まれるMemoryRegionを検索して返します。
(exec.c: address_space_translate)
MemoryRegion *address_space_translate(AddressSpace *as, hwaddr addr, hwaddr *xlat, hwaddr *plen, bool is_write) { IOMMUTLBEntry iotlb; MemoryRegionSection *section; MemoryRegion *mr; for (;;) { AddressSpaceDispatch *d = atomic_rcu_read(&as->dispatch);[*14] section = address_space_translate_internal(d, addr, &addr, plen, true);[*15] mr = section->mr; ... } ... return mr; }
[*14]でAddressSpaceDispatchをRCUを使用して取得した後、[*15]でMemoryRegionSectionを得ています。そしてSectionが指し示すMemoryRegionを返していますね。ちなみにRCU(ReadCopyUpdate)はlinuxカーネルで使用されている排他機構と同じものです。
さてこのMemoryRegionの情報はまだGPAですのでこれを[*13]のqemu_get_ram_ptrでHVAに変換しています。
(exec.c: qemu_get_ram_ptr)
void *qemu_get_ram_ptr(ram_addr_t addr) { RAMBlock *block; void *ptr; block = qemu_get_ram_block(addr); ... ptr = ramblock_ptr(block, addr - block->offset); ... return ptr; }
qemuでは仮想メモリをRAMBlock構造体で表現しており、必要になればこれをホストのメモリ空間にmmapしています。qemu_get_ram_ptrではGPAからRAMBlockを検索しramblock_ptrでHVAに変換しています。
ではx86_cpu_handle_mmu_faultに戻って[*11]のtlb_set_page_with_attrsを見ていきます。
(cputlb.c: tlb_set_page_with_attrs)
void tlb_set_page_with_attrs(CPUState *cpu, target_ulong vaddr, hwaddr paddr, MemTxAttrs attrs, int prot, int mmu_idx, target_ulong size) { CPUArchState *env = cpu->env_ptr; MemoryRegionSection *section; unsigned int index; target_ulong address; target_ulong code_address; uintptr_t addend; CPUTLBEntry *te; hwaddr iotlb, xlat, sz; unsigned vidx = env->vtlb_index++ % CPU_VTLB_SIZE; ... address = vaddr; if (!memory_region_is_ram(section->mr) && !memory_region_is_romd(section->mr)) { /* IO memory case */ address |= TLB_MMIO; addend = 0; } else { /* TLB_MMIO for rom/romd handled below */ addend = (uintptr_t)memory_region_get_ram_ptr(section->mr) + xlat; } code_address = address; ... index = (vaddr >> TARGET_PAGE_BITS) & (CPU_TLB_SIZE - 1); te = &env->tlb_table[mmu_idx][index]; [*16] te->addend = addend - vaddr; }
まあ見たままですが[*16]で該当するtlb_tableを取得しaddendにHGAのHVAに対するオフセットを代入しています。これでページ変換ができるようになりました。
さてpart1はここまでです。
次のpart2では仮想IOや仮想IRQおよびTCGの処理を見ていきます。
Reference
QEMUのコードを読むにあたって参考にした資料です。非常に良質な資料が多いのでコードリーディングの際はぜひ参考にしてみてください。
- QOM exegesis and apocalypse
http://events.linuxfoundation.jp/sites/events/files/slides/kvmforum14-qom_0.pdf
- QEMU Internals: Overall architecture and threading model
http://blog.vmsplice.net/2011/03/qemu-internals-overall-architecture-and.html
- QEMU Internals: Big picture overview
http://blog.vmsplice.net/2011/03/qemu-internals-big-picture-overview.html
- QEMUのDynamic Binary Translationがあって良かったねという話
https://speakerdeck.com/ntddk/qemufalsedynamic-binary-translationgaatuteliang-katutanetoiuhua
- Coroutines in QEMU: The basics
http://blog.vmsplice.net/2014/01/coroutines-in-qemu-basics.html
- Effective multi-threading in QEMU
http://www.linux-kvm.org/images/1/17/Kvm-forum-2013-Effective-multithreading-in-QEMU.pdf
- KVM and CPU feature enablement
http://wiki.qemu.org/images/c/c8/Cpu-models-and-libvirt-devconf-2014.pdf
- 2011-forum-qapi-liguori
http://www.linux-kvm.org/images/e/e6/2011-forum-qapi-liguori.pdf
- QEMU Source Code Notes
http://chenyufei.info/notes/qemu-src.html
http://wiki.qemu.org/Main_Page
- Qemu Detailed Study
https://lists.gnu.org/archive/html/qemu-devel/2011-04/pdfhC5rVdz7U8.pdf
- QEMU AND DEVICE EMULATION
http://logtel-conferences.com/Portals/2/09.%20QEMU.pdf
- GNOME DEVELOPER
- QEMU: Architecture and Internals Lecture for the Embedded Systems Course CSD
http://www.csd.uoc.gr/~hy428/reading/qemu-internals-slides-may6-2014.pdf
- Real Time QEMU
https://www.ccur.co.jp/external/TechSup/RealtimeQEMU.pdf
*1:constructor