前回の続きです。 part3ではライブマイグレーション 6.ライブマイグレーション docs/migration.txt ゲストが動いているデバイスの各状態を保存&リストアする機能がある。 これらはQEMUFileを利用して書き出される。これに対するヘルパー関数の役目としてregister_savevmなどが利用される。 がこれはレガシーな方法で今はVMStateが利用されている。
スナップショットの形式: 恐らくセクションに分かれている。register_savevmは、指定したsave/loadメソッドを持つSaveStateEntryを持った新しいセクションを追加する(?)
7.QMP(QEMU Machine Protocol)
(monitor.c)
|c| static const mon_cmd_t qmp_cmds[] = { #include "qmp-commands-old.h" { /* NULL */ }, }; ||<
既存のQEMUmonitorで使用されるコマンドは全てQMPコマンド(qmp_*)に置き換えられている。
(qdev-monitor.c)
|c| void qmp_device_add(QDict *qdict, QObject **ret_data, Error **errp) { Error *local_err = NULL; QemuOpts *opts; DeviceState *dev;
opts = qemu_opts_from_qdict(qemu_find_opts("device"), qdict, &local_err); [*]
dev = qdev_device_add(opts, &local_err); [*]
object_unref(OBJECT(dev));
} ||< [*]のqemu_opts_from_qdict(list,qdict,err)でqdictをQemuOptsに変換し、listに繋ぐ。 つまりここではdeviceオプションにqdictを追加している。
|sh| (QEMU) device_add driver=e1000 id=net1 ||< とした場合、deviceオプションはdriver=e1000 id=net1になる。 その後[*]のqdev_device_addを読んでいる。
(qdev-monitor.c)
|c| DeviceState *qdev_device_add(QemuOpts *opts, Error **errp) { DeviceClass *dc; const char *driver, *path, *id; DeviceState *dev; BusState *bus = NULL; Error *err = NULL;
driver = qemu_opt_get(opts, "driver"); [*]
/* find driver */
dc = qdev_get_device_class(&driver, errp); [*]
/* find bus */
path = qemu_opt_get(opts, "bus"); [*]
else if (dc->bus_type != NULL) {
bus = qbus_find_recursive(sysbus_get_default(), NULL, dc->bus_type); [*]
if (!bus || qbus_is_full(bus)) {
error_setg(errp, "No '%s' bus found for device '%s'",
dc->bus_type, driver);
return NULL;
}
}
/* create device */
dev = DEVICE(object_new(driver)); [*]
if (bus) {
qdev_set_parent_bus(dev, bus); [*]
}
id = qemu_opts_id(opts);
if (id) {
dev->id = id; [*]
}
if (dev->id) {
object_property_add_child(qdev_get_peripheral(), dev->id,
OBJECT(dev), NULL); [*]
}
...
dev->opts = opts;
object_property_set_bool(OBJECT(dev), true, "realized", &err); [*]
return dev;
} ||<
[]でdriver(上記コマンドならe1000)オプションを取得し[]のqdev_get_device_classで該当するDeviceClassを取得。またbusオプションがあれば[]で取得してBusStateを検索する。今回はbusは指定されていないため、[]のqbus_find_recursiveでDeviceClassで指定されているバスを検索。idが指定されていれば(id=net1)[]で設定し[]でperipheralのプロパティにつないでいる。最後に[*]でrealizedをtrueにすることでホットプラグがデバイスに通知される。
(blockdev.c)
|c|
||<
上記のqmp-marshal.cなどは(qapi-schema.json)にQAPIが記述されており、これをqapi-commands.py(scripts/)によりCのコードに変換する事で自動生成されている。
(scripts/qapi-commands.py)
|py| middle_mode = False
(input_file, output_dir, do_c, do_h, prefix, opts) =
parse_command_line("m", ["middle"]) [*]
exprs = parse_schema(input_file) [] commands = filter(lambda expr: expr.has_key('command'), exprs) commands = filter(lambda expr: not expr.has_key('gen'), commands) [] ... (fdef, fdecl) = open_output(output_dir, do_c, do_h, prefix, 'qmp-marshal.c', 'qmp-commands.h', c_comment, h_comment) [*]
fdef.write(mcgen(''' #include "qemu-common.h" ...
fdecl.write(mcgen(''' #include "%(prefix)sqapi-types.h" ... for cmd in commands: arglist = [] ret_type = None if cmd.has_key('data'): arglist = cmd['data'] if cmd.has_key('returns'): ret_type = cmd['returns'] ret = generate_command_decl(cmd['command'], arglist, ret_type) + "\n" [] fdecl.write(ret) if ret_type: ret = gen_marshal_output(cmd['command'], arglist, ret_type, middle_mode) + "\n" [] fdef.write(ret)
if middle_mode:
fdecl.write('%s;\n' % gen_marshal_input_decl(cmd['command'], arglist, ret_type, middle_mode))
ret = gen_marshal_input(cmd['command'], arglist, ret_type, middle_mode) + "\n" [*]
fdef.write(ret)
if not middle_mode: ret = gen_registry(commands) fdef.write(ret)
close_output(fdef, fdecl) ||<
[]のparse_schemaでQAPIのjsonをpythonの辞書形式に変換する。 []のopen_outputで生成するソースファイル(.c,.h)を指定する。 []のgenerate_command_declで関数宣言を生成し吐く。 []のgen_marshal_outputでoutput系(?),[*]のgen_marshal_inputでinput系(?)関数の生成を行う。
(scripts/qapi.py)
|py| class QAPISchema:
def __init__(self, fp, previously_included = [], incl_info = None):
abs_fname = os.path.abspath(fp.name)
fname = fp.name
self.fname = fname
previously_included.append(abs_fname)
self.incl_info = incl_info
self.src = fp.read() [*]
if self.src == '' or self.src[-1] != '\n':
self.src += '\n'
self.cursor = 0
self.line = 1
self.line_pos = 0
self.exprs = []
self.accept() [*]
while self.tok != None:
expr_info = {'file': fname, 'line': self.line,
'parent': self.incl_info}
expr = self.get_expr(False)
if isinstance(expr, dict) and "include" in expr:
if len(expr) != 1:
raise QAPIExprError(expr_info, "Invalid 'include' directive")
include = expr["include"]
if not isinstance(include, str):
raise QAPIExprError(expr_info,
'Expected a file name (string), got: %s'
% include)
incl_abs_fname = os.path.join(os.path.dirname(abs_fname),
include)
# catch inclusion cycle
inf = expr_info
while inf:
if incl_abs_fname == os.path.abspath(inf['file']):
raise QAPIExprError(expr_info, "Inclusion loop for %s"
% include)
inf = inf['parent']
# skip multiple include of the same file
if incl_abs_fname in previously_included:
continue
try:
fobj = open(incl_abs_fname, 'r')
except IOError, e:
raise QAPIExprError(expr_info,
'%s: %s' % (e.strerror, include))
exprs_include = QAPISchema(fobj, previously_included,
expr_info)
self.exprs.extend(exprs_include.exprs)
else:
expr_elem = {'expr': expr,
'info': expr_info}
self.exprs.append(expr_elem)
... def parse_schema(fname): try: schema = QAPISchema(open(fname, "r")) [] return check_exprs(schema.exprs) [] ... ||<
[]のQAPISchemaクラスが構文解析器。 []のaccept関数(scripts/qapi.py)で''で挟まれる1単語を取得する。'struct'->structなど。[]のget_expr関数(scripts/qapi.py)は{}または[]で囲まれる式を評価する。つまり <get_expr> ::= '{' | '[' ']' | '}'といった感じで構文解析するだけ。 []でQAPISchema.exprsに格納された辞書を構文的に正しいかチェックする。
8.フロントエンド
前回も見たpc_init1を再度見る。 (hw/i386/pc_piix.c)
|c| static void pc_init1(MachineState *machine, const char *host_type, const char *pci_type) { ... pc_vga_init(isa_bus, pcmc->pci_enabled ? pci_bus : NULL); ... } ||< (hw/i386/pc.c) |c| DeviceState *pc_vga_init(ISABus *isa_bus, PCIBus *pci_bus) { DeviceState *dev = NULL;
if (pci_bus) {
PCIDevice *pcidev = pci_vga_init(pci_bus); [*]
dev = pcidev ? &pcidev->qdev : NULL;
} else if (isa_bus) {
ISADevice *isadev = isa_vga_init(isa_bus);
dev = isadev ? DEVICE(isadev) : NULL;
}
return dev;
} ||< (hw/pci/pci.c)
|c| PCIDevice *pci_vga_init(PCIBus bus) { switch (vga_interface_type) { case VGA_CIRRUS: return pci_create_simple(bus, -1, "cirrus-vga"); [] ... } ||<
pc_init1はmain関数よりも前に実行される事に留意。これにより[]でPCI用のvgaを初期化している。 []でVGA_CIRRUSタイプのVGAを指定された場合pci_create_simpleを使ってPCIDeviceを初期化している。pci_create_simple->pci_create_simple_multifunction(hw/pci/pci.c)にてPCIDeviceの初期化と、qemu_init_nofail(hw/core/qdev.c)でrealizeを行っている。 このrealize関数は例えばcirrus-vgaなら以下のようになる。 (hw/display/cirrus-vga.c)
|c| static void pci_cirrus_vga_realize(PCIDevice *dev, Error **errp) { PCICirrusVGAState *d = PCI_CIRRUS_VGA(dev); CirrusVGAState *s = &d->cirrus_vga; PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(dev); int16_t device_id = pc->device_id;
/* follow real hardware, cirrus card emulated has 4 MB video memory.
Also accept 8 MB/16 MB for backward compatibility. */
if (s->vga.vram_size_mb != 4 && s->vga.vram_size_mb != 8 &&
s->vga.vram_size_mb != 16) {
error_setg(errp, "Invalid cirrus_vga ram size '%u'",
s->vga.vram_size_mb);
return;
}
/* setup VGA */
vga_common_init(&s->vga, OBJECT(dev), true);
cirrus_init_common(s, OBJECT(dev), device_id, 1, pci_address_space(dev),
pci_address_space_io(dev));
s->vga.con = graphic_console_init(DEVICE(dev), 0, s->vga.hw_ops, &s->vga); [*]
/* setup PCI */
memory_region_init(&s->pci_bar, OBJECT(dev), "cirrus-pci-bar0", 0x2000000);
/* XXX: add byte swapping apertures */
memory_region_add_subregion(&s->pci_bar, 0, &s->cirrus_linear_io);
memory_region_add_subregion(&s->pci_bar, 0x1000000,
&s->cirrus_linear_bitblt_io);
/* setup memory space */
/* memory #0 LFB */
/* memory #1 memory-mapped I/O */
/* XXX: s->vga.vram_size must be a power of two */
pci_register_bar(&d->dev, 0, PCI_BASE_ADDRESS_MEM_PREFETCH, &s->pci_bar);
if (device_id == CIRRUS_ID_CLGD5446) {
pci_register_bar(&d->dev, 1, 0, &s->cirrus_mmio_io);
}
} ||<
(vl.c)
|c| ds = init_displaystate(); [] ||< []でDisplayStateを初期化している。
(ui/console.c)
|c| DisplayState *init_displaystate(void) { gchar *name; int i;
get_alloc_displaystate(); [*]
for (i = 0; i < nb_consoles; i++) {
if (consoles[i]->console_type != GRAPHIC_CONSOLE &&
consoles[i]->ds == NULL) {
text_console_do_init(consoles[i]->chr, display_state); [*]
}
/* Hook up into the qom tree here (not in new_console()), once
* all QemuConsoles are created and the order / numbering
* doesn't change any more */
name = g_strdup_printf("console[%d]", i);
object_property_add_child(container_get(object_get_root(), "/backend"),
name, OBJECT(consoles[i]), &error_abort); [*]
g_free(name);
}
return display_state;
}
||< consolesはQEMUConsoleの配列で、上で見たように既に初期化済み。 [*]でテキストコンソールの場合は初期化している。これは例えば-monitor stdioなどでテキストコンソールを指定した場合に該当する。
(ui/console.c)
|c| static void text_console_do_init(CharDriverState *chr, DisplayState *ds) { QemuConsole *s; int g_width = 80 * FONT_WIDTH; int g_height = 24 * FONT_HEIGHT;
s = chr->opaque;
chr->chr_write = console_puts; [*]
s->out_fifo.buf = s->out_fifo_buf;
s->out_fifo.buf_size = sizeof(s->out_fifo_buf);
s->kbd_timer = timer_new_ms(QEMU_CLOCK_REALTIME, kbd_send_chars, s);
s->ds = ds;
s->y_displayed = 0;
s->y_base = 0;
s->total_height = DEFAULT_BACKSCROLL;
s->x = 0;
s->y = 0;
if (!s->surface) {
if (active_console && active_console->surface) {
g_width = surface_width(active_console->surface);
g_height = surface_height(active_console->surface);
}
s->surface = qemu_create_displaysurface(g_width, g_height);
}
s->hw_ops = &text_console_ops;
s->hw = s;
/* Set text attribute defaults */
s->t_attrib_default.bold = 0;
s->t_attrib_default.uline = 0;
s->t_attrib_default.blink = 0;
s->t_attrib_default.invers = 0;
s->t_attrib_default.unvisible = 0;
s->t_attrib_default.fgcol = QEMU_COLOR_WHITE;
s->t_attrib_default.bgcol = QEMU_COLOR_BLACK;
/* set current text attributes to default */
s->t_attrib = s->t_attrib_default;
text_console_resize(s);
if (chr->label) {
char msg[128];
int len;
s->t_attrib.bgcol = QEMU_COLOR_BLUE;
len = snprintf(msg, sizeof(msg), "%s console\r\n", chr->label);
console_puts(chr, (uint8_t*)msg, len);
s->t_attrib = s->t_attrib_default;
}
qemu_chr_be_generic_open(chr);
if (chr->init)
chr->init(chr);
} ||<
[*]でCharDriveState->chr_writeにconsole_putsを設定している。 console_puts(chr,buf,len)は長さlenのbufをchrに出力する。 (ui/console.c)
|c| static int console_puts(CharDriverState *chr, const uint8_t *buf, int len) { QemuConsole *s = chr->opaque; int i;
s->update_x0 = s->width * FONT_WIDTH;
s->update_y0 = s->height * FONT_HEIGHT;
s->update_x1 = 0;
s->update_y1 = 0;
console_show_cursor(s, 0);
for(i = 0; i < len; i++) {
console_putchar(s, buf[i]);
}
console_show_cursor(s, 1);
if (s->ds->have_gfx && s->update_x0 < s->update_x1) {
dpy_gfx_update(s, s->update_x0, s->update_y0,
s->update_x1 - s->update_x0,
s->update_y1 - s->update_y0);
}
return len;
} ||<
console_putchar(s, buf[i]);ではESCシーケンスも扱う。(ex. ESC[5n) (ui/console.c)
|c| static void console_putchar(QemuConsole *s, int ch) { TextCell *c; int y1, i; int x, y;
switch(s->state) {
case TTY_STATE_NORM: [*]
...
default: [*]
if (s->x >= s->width) {
/* line wrap */
s->x = 0;
console_put_lf(s);
}
y1 = (s->y_base + s->y) % s->total_height;
c = &s->cells[y1 * s->width + s->x];
c->ch = ch;
c->t_attrib = s->t_attrib;
update_xy(s, s->x, s->y);
s->x++;
break;
}
break;
||< TTY_STATE_NORMは通常の文字を扱っている状態(つまりESCシーケンスの解析途中などではない)で、[*]のdefaultがエスケープ文字以外のAscii文字の処理である。 [問題点] console_putcharは通常文字を1文字その場でコンソールに反映させる設計。ゆえに例えば\e[5n(端末状態取得)など、複数文字列を結果として返す必要がある場合、複数文字をコンソールに出力しなければならない。1文字更新の関数内で\e[5n,\e[6nの時だけ複数文字更新するのは設計的にまずいのではないか? Linuxのtty/vtドライバ(drivers/tty/vt/vt.c)は文字の実際の出力を遅延させているため、1文字更新関数とは別の関数で行っており綺麗に分離できている。
8.QEMUコルーチン QEMUコルーチンはcallback hellを解消するために使用されます。 例えばコールバックを通常の通り登録する以下のようなコードがあるとします。
|c| /* 3-step process written using callbacks */ void start(void) { send("Hi, what's your name? ", step1); }
void step1(void) { read_line(step2); }
void step2(const char *name) { send("Hello, %s\n", name, step3); }
void step3(void) { /* done! */ } ||<
これはstartでHiが送信され、完了したタイミングでstep1を呼び出し、step1ではread_lineで入力を取って、入力完了すればstep2がコールバックされてHelloが送信され、さらに送信が完了するとstep3がコールバックされるプログラムです。つまり何かしらの操作が完了する度にコールバックしているわけですが、コードの見た目が非常にネストしておりまさにcallback hellに近い状態なわけです。
QEMUコルーチンを使えばこれらを次のように書けます。
|c| /* 3-step process using coroutines */ void coroutine_fn say_hello(void) { const char *name;
co_send("Hi, what's your name? ");
name = co_read_line();
co_send("Hello, %s\n", name);
/* done! */
} ||<
「Hiを送信してread_lineしてHelloを送信する」という直感的な記述ができています。 もちろんread_lineはHiの送信が完了してから実行されますしHelloも然りです。 つまりこれは"Hi"を送信すると同時にコールーチン内部に"enter"し、処理が完了すれば"yield"で元の処理に戻って次のco_read_lineを実行するようになってるわけです。"処理が完了すれば別の場所に飛ぶ"という処理を、関数をネストさせずに記述している事になります。 ではQEMUコルーチンが内部でどのような事を行っているか見てみましょう。
コルーチンはQEMUでは4種類の方法で実装されており実行環境に適した物が選ばれる。ここではgthreadを用いた(util/coroutine-ghtread.c)を見ていく。 (util/qemu-coroutine.c)
|c| Coroutine *qemu_coroutine_create(CoroutineEntry *entry) { Coroutine co = NULL; ... if (!co) { co = qemu_coroutine_new(); [] }
co->entry = entry;
} ||< qemu_coroutine_newはGThreadの場合以下のようになっている。 (util/coroutine-ghtread.c)
|c| Coroutine *qemu_coroutine_new(void) { CoroutineGThread *co;
co = g_malloc0(sizeof(*co));
co->thread = create_thread(coroutine_thread, co); [*]
return &co->base;
} ||<
[*]で新たなGThreadを作成している。コルーチン一つがGThread1つに対応している。 ここではcoroutine_threadがスレッドとして新たに実行される。 (util/coroutine-ghtread.c)
|c| static gpointer coroutine_thread(gpointer opaque) { CoroutineGThread *co = opaque;
set_coroutine_key(co, false);
coroutine_wait_runnable(co);
co->base.entry(co->base.entry_arg); [*]
qemu_coroutine_switch(&co->base, co->base.caller, COROUTINE_TERMINATE); [*]
return NULL;
} ||< []でコルーチンに登録した関数を呼び出している。関数が終了すれば[]でTERMINATEする。
ここではcoroutine_threadがスレッドとして新たに実行される。 (util/qemu-coroutine.c)
|c| void qemu_coroutine_enter(Coroutine *co, void *opaque) { Coroutine *self = qemu_coroutine_self(); CoroutineAction ret;
co->caller = self;
co->entry_arg = opaque;
ret = qemu_coroutine_switch(self, co, COROUTINE_ENTER); [*]
qemu_co_queue_run_restart(co);
switch (ret) {
case COROUTINE_YIELD:
return;
case COROUTINE_TERMINATE:
trace_qemu_coroutine_terminate(co);
coroutine_delete(co);
return;
default:
abort();
}
} void coroutine_fn[*] qemu_coroutine_yield(void) { Coroutine *self = qemu_coroutine_self(); Coroutine *to = self->caller;
self->caller = NULL;
qemu_coroutine_switch(self, to, COROUTINE_YIELD); [*]
} ||< coroutine_fnマクロ(include/qemu/coroutine.h)は現在はNULL文字で特に意味は成していない。(将来的にコンパイラがこれを使って、通常関数からコルーチンが誤って呼ばれていないかなどのチェックを行えれば良いねとコメントには書いてある) enterもyieldも[],[]でコルーチン間をswitchしているだけである。
(ui/coroutine-gthread.c)
|c| CoroutineAction qemu_coroutine_switch(Coroutine *from_, Coroutine *to_, CoroutineAction action) { CoroutineGThread from = DO_UPCAST(CoroutineGThread, base, from_); [] CoroutineGThread *to = DO_UPCAST(CoroutineGThread, base, to_);
g_mutex_lock(&coroutine_lock);
from->runnable = false;
from->action = action;
to->runnable = true;
to->action = action;
g_cond_broadcast(&coroutine_cond); [*]
if (action != COROUTINE_TERMINATE) {
coroutine_wait_runnable_locked(from); [*]
}
g_mutex_unlock(&coroutine_lock);
return from->action;
} ||< [*]のDO_UPCASTはcontainer_ofのように、与えられたメンバーが属している構造体を取得しています。(QOM的には親クラスから子クラスを取得してるという事?)
[]のg_cond_broadcastでcoroutine_condでブロックされていたスレッドを起こす。(恐らくこれでtoに切り替わる?) coroutine_condはコルーチン間で共有するコンテキスト。 []のcoroutine_wait_runnable_lockedは指定されたスレッドをcoroutine_condが解法されるまでg_cond_waitで待たせておく。つまりここではfromを停止させている。
(util/coroutine-ghtread.c)
|c| static void coroutine_wait_runnable_locked(CoroutineGThread *co) { while (!co->runnable) { g_cond_wait(&coroutine_cond, &coroutine_lock); } } ||< これでfromが停止し、toに切り替わる。
9.qcow2 次にqcow2フォーマットについて詳しく見ていく。 (block/qcow2.h)
|c| typedef struct QCowHeader { uint32_t magic; //マジックナンバー 0xfb,Q,F,I uint32_t version; //バージョン(qcow or qcow2)
uint64_t backing_file_offset; //ファイルパス(CoW形式の場合バッキングファイルへのパス)
uint32_t backing_file_size; //パスの長さ
uint32_t cluster_bits; //クラスタ変換(2-level)の際に使う(後述)
uint64_t size; //ブロックデバイスのサイズ(バイト単位)
uint32_t crypt_method; //暗号化方法(0.なし 1.AES)
uint32_t l1_size;
uint64_t l1_table_offset;
uint64_t refcount_table_offset;
uint32_t refcount_table_clusters;
uint32_t nb_snapshots; //含まれるスナップショットの数
uint64_t snapshots_offset;//各スナップショットのQCowHeaderが格納されている場所へのオフセット
} QCowHeader; ||< qcowは基本的にクラスタ単位でブロックデバイスの中身を保存している。1クラスタは512byteのセクターの集合になっている。 また与えられたアドレスからクラスタを見つけるために2-level lookupを使用する。これはL1,L2テーブルで構成されており、L1はL2エントリのオフセットを、L2はクラスタのオフセットを格納している。(要はintelのページング機構と同じ) 与えられたアドレスがどのようにL1,L2のエントリ番号に対応するかは上述のcluster_bitsメンバを使う。アドレスの下位cluster_bitsビットが4kbの1クラスタ内でのオフセットで、次のcluster_bits-3ビットがL2エントリの番号を指し残りの上位ビットL1エントリ番号となる。つまり1クラスタのサイズは(1<<cluster_bits)バイトになる。 もしL1,L2のエントリが0の場合それは該当アドレスのクラスタがまだイメージ内に格納されていない事を示します。またL1,L2エントリの最上位1bit目,2bit目はそれぞれ"copied","compressed"として予約されている。
また各クラスタは参照カウンタを持っていて、どのスナップショットからも使われなくったクラスタは開放される。参照カウンタは2段階のデータ構造になっており、qcowヘッダーrefcount_table_offsetの指すrefcount_table_clustersサイズの参照カウンタテーブル、そしてそのテーブルが指す参照カウンタブロックである。この1ブロックのサイズは1クラスタに等しい。 参照カウンタはcluster_bitsのオフセット値をrefcount tableとrefcount offsetに分け、ヘッダのrefcount_table_offsetからアクセスする(多分) もしクラスタの参照カウンタが1である場合、そのクラスタのL1かL2エントリの最上位bitに"copied"フラグが立てられる。
qcowはcopy-on-write形式のイメージを作成できる。これは元となるイメージファイルに対して、新しく加わったクラスターのみ保存しているdiffのような物。元のイメージファイル(バッキングファイル)名はヘッダ内のbacking_file_offsetで示される。このCoW形式のイメージはクラスタにアクセスされると、自身にデータがない場合はbacking_fileを読み込んで持ってくるようになっています。これらのdiffファイルは続けて作成でき、チェインさせられる。バッキングファイルはread-onlyで開かれ、新たな書き込みはdiffに対して、また元々あったデータはバッキングファイルから読む。
スナップショットはよくCoW形式の外部diffと混同されますが、本来のスナップショットは元のイメージファイル自身の中にあり、 各スナップショットは以下のヘッダーを持っている。これらヘッダーの配列はqcow2ヘッダーのsnapshots_offsetが指している。
|c| typedef struct QCowSnapshotHeader { /* header is 8 byte aligned */ uint64_t l1_table_offset;
uint32_t l1_size;
uint16_t id_str_size;
uint16_t name_size;
uint32_t date_sec; //snapshotが作られたホストマシンでの時間[s]
uint32_t date_nsec; //snapshotが作られたホストマシンでの時間[ns]
uint64_t vm_clock_nsec;
uint32_t vm_state_size; //snapshotに保存されているVMの状態の大きさ
uint32_t extra_data_size; //本ヘッダと(id,name)との間にあるスペース(拡張用)
/* extra data follows */
/* id_str follows */
/* name follows */
} QCowSnapshotHeader; ||<
スナップショットを作成すると上のヘッダーが新たに追加され、L1テーブルがそのままコピーされL2テーブルはコピーされない変わりに、L2にある全てのクラスターとL1からダイレクトに参照されるデータクラスタの参照カウンタを1増やす。これは参照カウンタが>=2の場合2つ以上のイメージからクラスターおよびテーブルが参照されている事になり、ここに書き込みが発生した場合、書き込んだ方へコピーを作成するようにする。(まさにCopy-on-Writeそのまま) これでスナップショットが参照していたデータが、その後変更されてもコピーされるためスナップショットには影響が出ない。 スナップショットがロードされれば、参照されていたL2テーブルを作りなおして対応する。
qcowフォーマットは各クラスタに独立にzlib圧縮を施すことができる。 もしクラスタオフセット(cluster_bits)の最上位から2bit目が1の場合は圧縮がかかっている事を表す。次の(cluster_bits-8)bitは圧縮されたクラスタのサイズを表す。残りは実際のクラスタへのアドレスを格納する。
qcow2ヘッダーのcrypt_methodが1の場合、16文字のパスワードをキーとしたAES暗号化がされており、クラスタ内の各セクターは独立にAES暗号化される。この際の初期ベクトルはそれぞれのセクタのRVA(デバイスの先頭からの相対アドレス)を使用する。
- qcowはCoWしかないがqcow2ではスナップショットが導入
- qcow2ではクラスタの参照カウンタを導入
- l2_bitsが固定になった(cluster_bits-3)
- 圧縮されたクラスタのサイズがバイト単位ではなくクラスタ単位になった
(block/qcow2.c)
|c| static coroutine_fn int qcow2_co_readv(BlockDriverState *bs, int64_t sector_num, int remaining_sectors, QEMUIOVector *qiov) { BDRVQcow2State s = bs->opaque; int index_in_cluster, n1; int ret; int cur_nr_sectors; / number of sectors in current iteration */ uint64_t cluster_offset = 0; uint64_t bytes_done = 0; QEMUIOVector hd_qiov; uint8_t *cluster_data = NULL;
qemu_iovec_init(&hd_qiov, qiov->niov);
qemu_co_mutex_lock(&s->lock);
while (remaining_sectors != 0) {
/* prepare next request */
cur_nr_sectors = remaining_sectors;
...
ret = qcow2_get_cluster_offset(bs, sector_num << 9,
&cur_nr_sectors, &cluster_offset);
...
index_in_cluster = sector_num & (s->cluster_sectors - 1);
...
switch (ret) {
case QCOW2_CLUSTER_UNALLOCATED:
if (bs->backing) {
/* read from the base image */
n1 = qcow2_backing_read1(bs->backing->bs, &hd_qiov,
sector_num, cur_nr_sectors);
if (n1 > 0) {
QEMUIOVector local_qiov;
qemu_iovec_init(&local_qiov, hd_qiov.niov);
qemu_iovec_concat(&local_qiov, &hd_qiov, 0,
n1 * BDRV_SECTOR_SIZE); [*]
BLKDBG_EVENT(bs->file, BLKDBG_READ_BACKING_AIO);
qemu_co_mutex_unlock(&s->lock);
ret = bdrv_co_readv(bs->backing->bs, sector_num,
n1, &local_qiov); [*]
}
} else {
/* Note: in this case, no need to wait */
qemu_iovec_memset(&hd_qiov, 0, 0, 512 * cur_nr_sectors);
}
break;
case QCOW2_CLUSTER_ZERO:
qemu_iovec_memset(&hd_qiov, 0, 0, 512 * cur_nr_sectors);
break;
case QCOW2_CLUSTER_COMPRESSED:
/* add AIO support for compressed blocks ? */
ret = qcow2_decompress_cluster(bs, cluster_offset);
if (ret < 0) {
goto fail;
}
qemu_iovec_from_buf(&hd_qiov, 0,
s->cluster_cache + index_in_cluster * 512,
512 * cur_nr_sectors);
break;
case QCOW2_CLUSTER_NORMAL:
if ((cluster_offset & 511) != 0) {
ret = -EIO;
goto fail;
}
BLKDBG_EVENT(bs->file, BLKDBG_READ_AIO);
qemu_co_mutex_unlock(&s->lock);
ret = bdrv_co_readv(bs->file->bs,
(cluster_offset >> 9) + index_in_cluster,
cur_nr_sectors, &hd_qiov);
break;
default:
g_assert_not_reached();
ret = -EIO;
goto fail;
}
remaining_sectors -= cur_nr_sectors;
sector_num += cur_nr_sectors;
bytes_done += cur_nr_sectors * 512;
}
ret = 0;
return ret;
} ||<
データがない場合はバッキングファイルから読み込む。[*]のbdrv_co_readvで再度バッキングファイルに対してreadを行う事で再帰的にチェインを辿って読んでいく。
qcow2はCoW形式だがqemu-img preallocation=(metadata|full|falloc)オプションを指定すると、予め
10.qemu-img まずqemu-imgの意外と知られていない基本的な使い方を見ておく。
|sh| qemu-img create -f qcow2 test2.qcow2 -o backing_file=test1.qcow2 ||< とするとtest2.qcow2はtest1.qcow2をバッキングファイルとした差分ファイルになる。またこれら差分ファイル(以下overlay形式と呼ぶ)はチェインさせられる。 |sh| qemu-img create -f qcow2 test3.qcow2 -o backing_file=test2.qcow2 ||< これによりtest1.qcow2-->test2.qcow2-->test3.qcow2となる。仕組みはほぼgitと同じでこれらはcommit logになっていると考えれば良い。このtest3.qcow2のコミット分を一つにまとめることもできる。 |sh| qemu-img convert -O qcow2 test3.qcow2 test4.qcow2 -o backing_file=test1.qcow2 ||< こうすると test1.qcow2-->test2.qcow2-->test3.qcow2に対して test1.qcow2-->test4.qcow2となる。つまりgit resetしてgit commit --amendしたような感じ。 しかしこのqemu-img convertで-o backing_file=test1.qcow2を付けない場合、つまり |sh| qemu-img convert -O qcow2 test3.qcow2 test4.qcow2 ||< にした場合は、test4.qcow2はtest1.qcow2にtest3.qcow2までの全てのcommitをmergeした新たなイメージファイルになる。(差分ではないためサイズも非常に大きくなる)
qemu-img,qemu-io,qemu-nbd,ivshmem-(client|server)はconfigure内でtoolsとして扱われており、./configure --enable-toolsをつけると(デフォルトでもついてる?)これらもビルドされる。 Makefile内では$(TOOLS)に格納されており、 (Makefile)
|c| all:
$(DOCS) $ (TOOLS)$(HELPERS-y) recurse-all modules ... libqemustub.a: $ (stub-obj-y) libqemuutil.a:$(util-obj-y) ... qemu-img.o: qemu-img-cmds.h qemu-img$ (EXESUF): qemu-img.o$(block-obj-y) $ (crypto-obj-y)$(io-obj-y) $ (qom-obj-y) libqemuutil.a libqemustub.a ... ||< となっている。また*-obj-yはMakefile.objs内で定義されており、qemu-imgに必要な最低限のソースコードを集めている。libqemusutb.aはstub/以下、libqemuutil.aはqemu-optionなどのutil/以下のライブラリアーカイブと思われる。 つまりqemu-imgなどのツールは最低限必要なQEMUのコードを集めてビルドした物になっている。
qemu-imgの実装を見ていく。
(qemu-img.c)
|c| static const img_cmd_t img_cmds[] = { #define DEF(option, callback, arg_string)
{ option, callback }, #include "qemu-img-cmds.h" #undef DEF #undef GEN_DOCS { NULL, NULL, }, };
int main(int argc, char **argv) { ... #ifdef CONFIG_POSIX signal(SIGPIPE, SIG_IGN); [] #endif qemu_init_exec_dir(argv[0]); []
if (qemu_init_main_loop(&local_error)) { [*]
error_report_err(local_error);
exit(EXIT_FAILURE);
}
module_call_init(MODULE_INIT_QOM); [*]
bdrv_init(); [*]
cmdname = argv[1];
qemu_add_opts(&qemu_object_opts);
qemu_add_opts(&qemu_source_opts);
/* find the command */
for (cmd = img_cmds; cmd->name != NULL; cmd++) {
if (!strcmp(cmdname, cmd->name)) {
return cmd->handler(argc - 1, argv + 1); [*]
}
}
...
} ||<
[]のSIGPIPEは切断されたソケットなどに書き込もうとした際に発行される物で、この手のディスクエラーが発生しやすいプログラムは無視(SIGIGN)しておく。(?) qemu_init_exec_dir(util/oslib-posix.c)によってプログラムの絶対パスをexec_dir(util/oslib-posix.c)に格納する。 また[]のmodule_call_initは引数で指定されたMODULE_INIT_QOM型のモジュール初期化関数を呼び出す。これは__(constructor)_do_qemu_init**(include/qemu/module.h)からregister_dso_module_init(util/module.c)を呼び出して登録している。 (util/module.c)
|c| 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();
}
} ||<
[*]のbdrv_init(block.c)では
(block.c)
|c| void bdrv_init(void) { module_call_init(MODULE_INIT_BLOCK); [*] } ||<
[*]でもMODULE_INIT_BLOCK型のモジュール初期化関数を呼び出している。 MODULE_INIT_BLOCK型は (include/qemu/module.h)
|c| #define block_init(function) module_init(function, MODULE_INIT_BLOCK) ||<
block_initで登録されている。これはblock/以下の各ブロックデバイスで固有の関数が登録されている。つまりbdrv_init関数はmodule_call_initによってこれらの初期化関数を全て呼んでいる。
[*]で入力されたコマンドに一致するハンドラを呼び出している。
(qemu-img.c)
|c| static int img_create(int argc, char **argv) { ... for(;;) { switch(c) { case 'b': base_filename = optarg; break; case 'f': fmt = optarg; break; ... case 'q': quiet = true; break; } }
/* Get the filename */
filename = (optind < argc) ? argv[optind] : NULL;
/* Get image size, if specified */
if (optind < argc) {
int64_t sval;
char *end;
sval = qemu_strtosz_suffix(argv[optind++], &end,
QEMU_STRTOSZ_DEFSUFFIX_B);
...
img_size = (uint64_t)sval; [*]
}
bdrv_img_create(filename, fmt, base_filename, base_fmt,
options, img_size, BDRV_O_FLAGS, &local_err, quiet); [*]
} ||< []で指定されたサイズ(10Gなど)を取得し、[]bdrv_img_create(block.c)で実際にイメージを作成しています。
(block.c)
|c| void bdrv_img_create(const char *filename, const char *fmt, const char *base_filename, const char *base_fmt, char *options, uint64_t img_size, int flags, Error **errp, bool quiet) {
/* Find driver and parse its options */
drv = bdrv_find_format(fmt); [*]
proto_drv = bdrv_find_protocol(filename, true, errp); [*]
// The size for the image must always be specified, with one exception:
// If we are using a backing file, we can obtain the size from there
if (size == -1) { [*]
...
}
ret = bdrv_create(drv, filename, opts, &local_err); [*]
...
} ||< backing_**は-bオプションで指定したバッキングファイルの処理である。つまり
|sh| qemu-img create -f qcow2 -b centos-cleaninstall.img snapshot.img ||< といった感じでスナップショットを作る時の対象となるベースイメージがバッキングファイル。このsnapshotの変更をバッキングファイルに適用する事もqemu-img commitで可能。 []で指定されたファイルフォーマットのドライバをロードし、[]では指定されたプロトコルのドライバをロードする。bdrv_find_protocolは以下のようになっている。 (block.c) |c| BlockDriver bdrv_find_protocol(const char filename, bool allow_protocol_prefix, Error **errp) { ... drv1 = find_hdev_driver(filename); [] if (drv1) { return drv1; } if (!path_has_protocol(filename) || !allow_protocol_prefix) { return &bdrv_file; [] }
QLIST_FOREACH(drv1, &bdrv_drivers, list) {
if (drv1->protocol_name &&
!strcmp(drv1->protocol_name, protocol)) {
return drv1; [*]
}
}
} ||< bdrv_find_protocolではまず最初に[*]のfind_hdev_driverでデバイス名からデバイスの種類を特定している。ここで注意するべきなのはブロックデバイスの""フォーマットの種類""(ex. qcow2,vmdk)ではない。デバイスの種類でこれは今の所rawしか該当しない。
(block.c)
|c| /*
-
Detect host devices. By convention, /dev/cdrom[N] is always
-
recognized as a host CDROM. */ static BlockDriver *find_hdev_driver(const char *filename) { int score_max = 0, score; BlockDriver *drv = NULL, *d;
QLIST_FOREACH(d, &bdrv_drivers, list) { if (d->bdrv_probe_device) { score = d->bdrv_probe_device(filename); [*] if (score > score_max) { score_max = score; drv = d; } } }
return drv; } ||< これは[*]でファイル名がそのBlockDriverにどれだけマッチしているかを評価値として返す。この評価値が最大のものを該当するフォーマットとして採用する。find_hdev_driverはブロックデバイスのリストからbdrv_probe_deviceを使ってこれら評価値を求めるが、bdrv_probe_deviceはraw形式の物にしか定義されていない。つまりこれは必ずrawが選ばれる。 (将来raw以外の形式のデバイスが現れればbdrv_probe_deviceが実装されるはず)
ファイル名に":"が含まれている場合、例えばssh:hogehogeなら指定されたプロトコルは":"で切り出したsshとなる。プロトコルが指定されていなければ[]でbdrv_fileを返す。(ただのローカルのBlockDeviceとみなす) 指定されていれば[]で応じたBlockDriverを返す。
さて[*]ではsize==-1の時となっているがこれはオプションでサイズが指定されなかった時で、これはコメントにもあるとおり、バッキングファイルからスナップショットをcreateする時にバッキングファイルから自動でサイズを取得する処理になっている。
(block.c)
|c| int bdrv_create(BlockDriver drv, const char filename, QemuOpts *opts, Error **errp) { int ret;
Coroutine *co;
CreateCo cco = {
.drv = drv,
.filename = g_strdup(filename),
.opts = opts,
.ret = NOT_DONE,
.err = NULL,
};
if (qemu_in_coroutine()) {
/* Fast-path if already in coroutine context */
bdrv_create_co_entry(&cco);
} else {
co = qemu_coroutine_create(bdrv_create_co_entry); [*]
qemu_coroutine_enter(co, &cco); [*]
while (cco.ret == NOT_DONE) {
aio_poll(qemu_get_aio_context(), true); [*]
}
}
} ||<
[],[]ではQEMUコルーチンを使ってbdrv_create_co_entryを実行している。
(block.c)
|c| static void coroutine_fn bdrv_create_co_entry(void opaque) { ret = cco->drv->bdrv_create(cco->filename, cco->opts, &local_err); [] cco->ret = ret; } ||< [*]で指定されたdrvのbdrv_createを呼び出してimageを作成している。
ではqcow2のbdrv_createを見てみよう。 (block/qcow2.c)
|c| static int qcow2_create(const char *filename, QemuOpts *opts, Error **errp) {
/* Read out options */
size = ROUND_UP(qemu_opt_get_size_del(opts, BLOCK_OPT_SIZE, 0),
BDRV_SECTOR_SIZE);
backing_file = qemu_opt_get_del(opts, BLOCK_OPT_BACKING_FILE);
backing_fmt = qemu_opt_get_del(opts, BLOCK_OPT_BACKING_FMT);
if (qemu_opt_get_bool_del(opts, BLOCK_OPT_ENCRYPT, false)) {
flags |= BLOCK_FLAG_ENCRYPT;
}
cluster_size = qemu_opt_get_size_del(opts, BLOCK_OPT_CLUSTER_SIZE,
DEFAULT_CLUSTER_SIZE);
buf = qemu_opt_get_del(opts, BLOCK_OPT_PREALLOC);
prealloc = qapi_enum_parse(PreallocMode_lookup, buf,
PREALLOC_MODE__MAX, PREALLOC_MODE_OFF,
&local_err);
...
refcount_order = ctz32(refcount_bits);
ret = qcow2_create2(filename, size, backing_file, backing_fmt, flags,
cluster_size, prealloc, opts, version, refcount_order,
&local_err);
...
} ||<
(block/qcow2.c)
|c| static int qcow2_create2(const char *filename, int64_t total_size, const char *backing_file, const char *backing_format, int flags, size_t cluster_size, PreallocMode prealloc, QemuOpts *opts, int version, int refcount_order, Error **errp) { int cluster_bits; QDict *options;
/* Calculate cluster_bits */
cluster_bits = ctz32(cluster_size);
...
BlockBackend *blk;
QCowHeader *header;
uint64_t* refcount_table;
Error *local_err = NULL;
int ret;
if (prealloc == PREALLOC_MODE_FULL || prealloc == PREALLOC_MODE_FALLOC) {
...
nrefblocke = (aligned_total_size + meta_size + cluster_size)
/ (cluster_size - rces - rces * sizeof(uint64_t)
/ cluster_size);
meta_size += DIV_ROUND_UP(nrefblocke, refblock_size) * cluster_size;
/* total size of refcount tables */
nreftablee = nrefblocke / refblock_size;
nreftablee = align_offset(nreftablee, cluster_size / sizeof(uint64_t));
meta_size += nreftablee * sizeof(uint64_t);
qemu_opt_set_number(opts, BLOCK_OPT_SIZE,
aligned_total_size + meta_size, &error_abort); [*]
qemu_opt_set(opts, BLOCK_OPT_PREALLOC, PreallocMode_lookup[prealloc],
&error_abort);
}
ret = bdrv_create_file(filename, opts, &local_err); [*]
blk = blk_new_open(filename, NULL, NULL,
BDRV_O_RDWR | BDRV_O_PROTOCOL, &local_err); [*]
/* Want a backing file? There you go.*/
if (backing_file) {
ret = bdrv_change_backing_file(blk_bs(blk), backing_file, backing_format);
if (ret < 0) {
error_setg_errno(errp, -ret, "Could not assign backing file '%s' "
"with format '%s'", backing_file, backing_format);
goto out;
}
}
...
header = g_malloc0(cluster_size);
*header = (QCowHeader) {
.magic = cpu_to_be32(QCOW_MAGIC),
.version = cpu_to_be32(version),
.cluster_bits = cpu_to_be32(cluster_bits),
.size = cpu_to_be64(0),
.l1_table_offset = cpu_to_be64(0),
.l1_size = cpu_to_be32(0),
.refcount_table_offset = cpu_to_be64(cluster_size),
.refcount_table_clusters = cpu_to_be32(1),
.refcount_order = cpu_to_be32(refcount_order),
.header_length = cpu_to_be32(sizeof(*header)),
};
ret = blk_pwrite(blk, 0, header, cluster_size); [*]
/*
* And now open the image and make it consistent first (i.e. increase the
* refcount of the cluster that is occupied by the header and the refcount
* table)
*/
blk_unref(blk); [*]
blk = NULL;
blk = blk_new_open(filename, NULL, options,
BDRV_O_RDWR | BDRV_O_NO_BACKING, &local_err); [*]
ret = qcow2_alloc_clusters(blk_bs(blk), 3 * cluster_size); [*]
/* Create a full header (including things like feature table) */
ret = qcow2_update_header(blk_bs(blk));
/* Okay, now that we have a valid image, let's give it the right size */
ret = blk_truncate(blk, total_size);
/* And if we're supposed to preallocate metadata, do that now */
if (prealloc != PREALLOC_MODE_OFF) {
BDRVQcow2State *s = blk_bs(blk)->opaque;
qemu_co_mutex_lock(&s->lock);
ret = preallocate(blk_bs(blk)); [*]
qemu_co_mutex_unlock(&s->lock);
}
||<
[]のbdrv_create_fileはraw_createを呼ぶ。これはデバイスはrawフォーマットとするからで、_fileなどのハンガリアンがつくとrawフォーマット(とみなせる)でのオペレーションになる(?) []はpreallocationオプションによって始めから指定されたサイズのqcow2を作成する場合で、これは[]で実際のサイズを計算して上記のraw_createを呼ぶだけである。raw_createを仕様するためそのままのサイズで生のデータが作成され結果的にpreallocateした事になる。 []でblk_new_openによりqcow2ファイルとして新たにopenしBlockDriverStateを返している。また[]で再度blk_new_openしているのはヘッダと参照テーブルが作成された段階で、作成に使われたクラスタの参照カウンタを1増やすためである。以前のBlockDriverStateは[]のblk_unrefで破棄しており新たにopenしている。 [*]のqcow2_alloc_clustersでヘッダと参照テーブル用の3クラスタを確保している。
[*]のpreallocate関数では実際にクラスタをアロケートしていく。
(block/qcow2.c)
|c| static int preallocate(BlockDriverState *bs) {
nb_sectors = bdrv_nb_sectors(bs);
offset = 0;
while (nb_sectors) {
num = MIN(nb_sectors, INT_MAX >> BDRV_SECTOR_BITS);
ret = qcow2_alloc_cluster_offset(bs, offset, &num,
&host_offset, &meta); [*]
while (meta) { [*]
QCowL2Meta *next = meta->next;
ret = qcow2_alloc_cluster_link_l2(bs, meta); [*]
QLIST_REMOVE(meta, next_in_flight);
g_free(meta);
meta = next;
}
/* TODO Preallocate data if requested */
nb_sectors -= num;
offset += num << BDRV_SECTOR_BITS;
}
if (host_offset != 0) {
uint8_t buf[BDRV_SECTOR_SIZE];
memset(buf, 0, BDRV_SECTOR_SIZE);
ret = bdrv_write(bs->file->bs,
(host_offset >> BDRV_SECTOR_BITS) + num - 1,
buf, 1);
if (ret < 0) {
return ret;
}
}
return 0;
} ||< []のqcow2_alloc_cluster_offsetでoffsetで指定されたアドレスに対応するクラスタを確保する。このクラスタの番号はhost_offsetに格納される。 また[]のQCowL2Metaはクラスタは確保されたがまだL2テーブルに登録されていない物のリクエストを指す。
int qcow2_alloc_cluster_offset(BlockDriverState *bs, uint64_t offset, int *num, uint64_t *host_offset, QCowL2Meta **m) {
again: start = offset; remaining = (uint64_t)*num << BDRV_SECTOR_BITS; cluster_offset = 0; *host_offset = 0; cur_bytes = 0; *m = NULL;
while (true) {
if (!*host_offset) {
*host_offset = start_of_cluster(s, cluster_offset);
}
assert(remaining >= cur_bytes);
start += cur_bytes;
remaining -= cur_bytes;
cluster_offset += cur_bytes;
if (remaining == 0) {
break;
}
cur_bytes = remaining;
ret = handle_dependencies(bs, start, &cur_bytes, m);
if (ret == -EAGAIN) {
assert(*m == NULL);
goto again;
} else if (ret < 0) {
return ret;
} else if (cur_bytes == 0) {
break;
} else {
}
/*
* 2. Count contiguous COPIED clusters.
*/
ret = handle_copied(bs, start, &cluster_offset, &cur_bytes, m);
if (ret < 0) {
return ret;
} else if (ret) {
continue;
} else if (cur_bytes == 0) {
break;
}
/*
* 3. If the request still hasn't completed, allocate new clusters,
* considering any cluster_offset of steps 1c or 2.
*/
ret = handle_alloc(bs, start, &cluster_offset, &cur_bytes, m);
if (ret < 0) {
return ret;
} else if (ret) {
continue;
} else {
assert(cur_bytes == 0);
break;
}
}
*num -= remaining >> BDRV_SECTOR_BITS;
assert(*num > 0);
assert(*host_offset != 0);
return 0;
}
では例として"raw"フォーマットが指定された場合これらがどのように動くか見てみる。 rawフォーマットのBlockDriverはblock/raw_bsd.cで定義されている。
(block/raw_bsd.c)
|c| BlockDriver bdrv_raw = { .format_name = "raw", .bdrv_probe = &raw_probe, .bdrv_reopen_prepare = &raw_reopen_prepare, .bdrv_open = &raw_open, .bdrv_close = &raw_close, .bdrv_create = &raw_create, .bdrv_co_readv = &raw_co_readv, .bdrv_co_writev = &raw_co_writev, .bdrv_co_write_zeroes = &raw_co_write_zeroes, .bdrv_co_discard = &raw_co_discard, .bdrv_co_get_block_status = &raw_co_get_block_status, .bdrv_truncate = &raw_truncate, .bdrv_getlength = &raw_getlength, .has_variable_length = true, .bdrv_get_info = &raw_get_info, .bdrv_refresh_limits = &raw_refresh_limits, .bdrv_probe_blocksizes = &raw_probe_blocksizes, .bdrv_probe_geometry = &raw_probe_geometry, .bdrv_media_changed = &raw_media_changed, .bdrv_eject = &raw_eject, .bdrv_lock_medium = &raw_lock_medium, .bdrv_aio_ioctl = &raw_aio_ioctl, .create_opts = &raw_create_opts, .bdrv_has_zero_init = &raw_has_zero_init }; ||<
まずbdrv_openから見てみよう。 (block/raw_bsd.c)
|c| static int raw_open(BlockDriverState *bs, QDict *options, int flags, Error **errp) { bs->sg = bs->file->bs->sg; ... return 0; } ||< 常に成功する
ではbdrv_createはどうだろうか。 (block/raw_bsd.c)
|c| static int raw_create(const char *filename, QemuOpts *opts, Error **errp) { Error *local_err = NULL; int ret;
ret = bdrv_create_file(filename, opts, &local_err); [*]
return ret;
} ||< bdrv_create_fileを読んでいる。 (block/raw_bsd.c)
|c| int bdrv_create_file(const char *filename, QemuOpts opts, Error **errp) { drv = bdrv_find_protocol(filename, true, errp); []
ret = bdrv_create(drv, filename, opts, &local_err); [*]
return ret;
} ||< []でファイル名からフォーマットの推定もしくはプロトコルが指定されている場合、プロトコル用のドライバを取得している。ちなみにrawのprobe値は最小の1に設定されており、他のどのフォーマットも推定できなかった場合のみ採用される。 []のbdrv_createは最終的に
では次にqemu-img convertを見てみる。convertコマンドの場合img_convertが呼び出される。 (qemu-img.c)
|c| static int img_convert(int argc, char **argv) { for(;;) { qemu_progress_init(progress, 1.0); [*] bs_n = argc - optind - 1; out_filename = bs_n >= 1 ? argv[argc - 1] : NULL;
qemu_progress_print(0, 100); blk = g_new0(BlockBackend *, bs_n); bs = g_new0(BlockDriverState *, bs_n); bs_sectors = g_new(int64_t, bs_n);
total_sectors = 0;
for (bs_i = 0; bs_i < bs_n; bs_i++) { [*]
char *id = bs_n > 1 ? g_strdup_printf("source_%d", bs_i)
: g_strdup("source");
blk[bs_i] = img_open(id, image_opts, argv[optind + bs_i],
fmt, src_flags, true, quiet); [*]
g_free(id);
if (!blk[bs_i]) {
ret = -1;
goto out;
}
bs[bs_i] = blk_bs(blk[bs_i]);
bs_sectors[bs_i] = blk_nb_sectors(blk[bs_i]); [*]
...
total_sectors += bs_sectors[bs_i];
}
... /* Find driver and parse its options / drv = bdrv_find_format(out_fmt); [] if (!skip_create) { /* Create the new image / ret = bdrv_create(drv, out_filename, opts, &local_err); [] } out_blk = img_open_file("target", out_filename, out_fmt, flags, true, quiet); [] out_bs = blk_bs(out_blk); [] ... ret = bdrv_get_info(out_bs, &bdi); ... state = (ImgConvertState) { .src = blk, .src_sectors = bs_sectors, .src_num = bs_n, .total_sectors = total_sectors, .target = out_blk, .compressed = compress, .target_has_backing = (bool) out_baseimg, .min_sparse = min_sparse, .cluster_sectors = cluster_sectors, .buf_sectors = bufsectors, }; ret = convert_do_copy(&state); [] } } ||< []のqemu_progress_init(util/qemu-progress.c)でprogress_stateを初期化している。これは"(10.1/100%)"のように、イメージ操作の進捗を表示するただのプログレスバーで、それを初期化しているだけです。 [*]のbs_nはconvert元の数で、これは複数になりえます。例えば
|sh| qemu-img convert guest-s001.vmdk guest-s002.vmdk guest-s003.vmdk guest-s004.vmdk guest-s005.vmdk guest.qcow2 ||< といったように変換元が分割されている場合は、それらをつなげてqcowに変換するなどが可能です。 [*]のimg_openで1つずつイメージをopenしてblkに格納していきます。
(qemu-img.c)
|c| static BlockBackend *img_open_opts(const char *id, const char *optstr, QemuOpts opts, int flags, bool require_io, bool quiet) { options = qemu_opts_to_qdict(opts, NULL); blk = blk_new_open(id, NULL, NULL, options, flags, &local_err); [] if (img_open_password(blk, optstr, require_io, quiet) < 0) { blk_unref(blk); return NULL; } return blk; } static BlockBackend *img_open(const char *id, bool image_opts, const char *filename, const char *fmt, int flags, bool require_io, bool quiet) { BlockBackend blk; if (image_opts) { ... blk = img_open_opts(id, filename, opts, flags, true, quiet); [] } else { blk = img_open_file(id, filename, fmt, flags, true, quiet); } return blk; } ||<
オプションが指定された場合、[]のimg_open_optsでブロックデバイスをopenします。 []のblk_new_openにはfilename(第2引数)にNULLを設定してoptionsを渡して実行しています。blk_new_openはfilenameもしくはoptionsを指定してイメージを開きます。
(block/block-backend.c)
|c| BlockBackend *blk_new_with_bs(const char *name, Error **errp) { BlockBackend *blk; BlockDriverState *bs;
blk = blk_new(name, errp); [*]
bs = bdrv_new_root(); [*]
blk->bs = bs;
bs->blk = blk;
return blk;
}
BlockBackend blk_new_open(const char name, const char filename, const char reference, QDict options, int flags, Error **errp) { blk = blk_new_with_bs(name, errp); [] ... ret = bdrv_open(&blk->bs, filename, reference, options, flags, errp); [] ... return blk; } ||< []のblk_new_with_bsでBlockDriverStateをもったBlockBackend(block/block-backend.c)をnameという名前で初期化している。(今回はnameに渡されたのはqemu-imgのid) BlockDriverState(include/block/block_int.h)はBlockDriver(include/block/block_int.h)のインスタンス(?)で名前の通りブロックデバイス一つを指す。これはIDEやSCSIといったインターフェースに対して、バックエンドに位置する物。つまりqcow2,raw,vmdiといった実際のブロックデバイスに対する操作bdrv_open,bdrv_read|writeやデータを持っている。 さて[]のblk_newでnameという名前のBlockBackendを初期化、[]のbdrv_new_root(block.c)でBlockDriverStateを初期化している。BlockDriverStateの初期化にはbdrv_new(block.c)を用いている。 さてBlockDriverState,BlockBackendの初期化が終われば[*]のbdrv_openでデバイスをopenする。なおopenする際はファイルのフォーマットがわかっている必要があるが、これはimg_openする前に自分でオプションに設定して渡す。(つまりフォーマットの指定は呼び出し側の責任)
(block.c)
|c| int bdrv_open(BlockDriverState **pbs, const char *filename, const char *reference, QDict options, int flags, Error **errp) { return bdrv_open_inherit(pbs, filename, reference, options, flags, NULL, NULL, errp); [] } ||<
[*]ではparentとchild_roleにNULLを設定してbdrv_open_inheritを呼び出している。
(block.c)
|c| static int bdrv_open_inherit(BlockDriverState **pbs, const char *filename, const char *reference, QDict *options, int flags, BlockDriverState *parent, const BdrvChildRole *child_role, Error **errp) { int ret; BdrvChild *file = NULL; BlockDriverState *bs; BlockDriver *drv = NULL; const char *drvname; const char backing; Error local_err = NULL; int snapshot_flags = 0; ... if (pbs) { bs = pbs; } else { bs = bdrv_new(); } bs->explicit_options = qdict_clone_shallow(options); ... / Find the right image format driver / drvname = qdict_get_try_str(options, "driver"); if (drvname) { drv = bdrv_find_format(drvname); [] ... }
/ Open the image / ret = bdrv_open_common(bs, file, options, &local_err); [] }
||< []で-driverオプションで指定された名前のBlockDriverを見つけます。bdrv_find_format(block.c)はBlockDriverのリストであるbdrv_driversから該当するBlockDriverをBlockDriver->format_nameをキーとして検索します。このリストにはbdrv_registr(block.c)を使って各イメージファイルが登録を行っています。(ex. "raw"フォーマットはblock/raw_bsd.c内で登録されている) []でimageを開いています。
(block.c)
|c| static int bdrv_open_common(BlockDriverState *bs, BdrvChild *file, QDict *options, Error **errp) { int ret, open_flags; const char *filename; const char *driver_name = NULL; const char node_name = NULL; QemuOpts opts; BlockDriver drv; Error local_err = NULL; / Open the image, either directly or using a protocol / open_flags = bdrv_open_flags(bs, bs->open_flags); if (drv->bdrv_file_open) { assert(file == NULL); assert(!drv->bdrv_needs_filename || filename != NULL); ret = drv->bdrv_file_open(bs, options, open_flags, &local_err); [] } else { if (file == NULL) { error_setg(errp, "Can't use '%s' as a block driver for the " "protocol level", drv->format_name); ret = -EINVAL; goto free_and_fail; } bs->file = file; ret = drv->bdrv_open(bs, options, open_flags, &local_err); [] } } ||< []のbdrv_openはプロトコルを使用して、リモートのブロックデバイスを開く。なおqcow2やvmdkなどのqemu-nbdを通してnbdとしてマウントするフォーマットは全てbdrv_openを使用する。つまりbdrv_file_openはローカルにあるファイルをopenし、bdrv_openはリモートのnbdサーバーのように扱ってopenする(多分) []でデバイスフォーマット固有のbdrv_file_openによりイメージを開きます。
ではqemu-imgに戻って、変換元のブロックデバイスを開いた後、[]のblk_nb_sectorsでセクタ数を取得しています。これはbdrv_nb_sectorsでBlockDriveStateのtotal_sectorsを返すだけです。 []のskip_createは、変換先のイメージが存在せず新規作成の必要がある場合はfalseになりbdrv_createを実行して作成します。 []のimg_open_fileで変換先イメージを、ファイル名を指定して開き[]でBlockDriverStateを取得しています。そして[*]のconvert_do_copyで、ImgConvertStateに変換方式、フォーマットを指定したあと、実際の変換を行っています。 ちなみにImgConvertStateは (qemu-img.c)
|c| typedef struct ImgConvertState { BlockBackend **src; //変換元ファイルのBlockBackend int64_t *src_sectors; //変換元ファイル(複数)のセクター数のリスト int src_cur, src_num; //変換元ファイルのindexと数 int64_t src_cur_offset; //変換元1ファイル内でのindex(ファイル内offset) int64_t total_sectors; //変換元ファイルの総セクタ数(複数の場合合計) int64_t allocated_sectors; enum ImgConvertBlockStatus status; int64_t sector_next_status; BlockBackend *target; //変換後ファイルのBlockBackend bool has_zero_init; bool compressed; //変換後ファイルを圧縮するかどうかのフラグ bool target_has_backing; //バッキングファイルを持っているかどうかのフラグ int min_sparse; size_t cluster_sectors; size_t buf_sectors; } ImgConvertState; ||<
(qemu-img.c)
|c| static int convert_iteration_sectors(ImgConvertState *s, int64_t sector_num) { int64_t ret; int n;
convert_select_part(s, sector_num); [*]
assert(s->total_sectors > sector_num);
n = MIN(s->total_sectors - sector_num, BDRV_REQUEST_MAX_SECTORS);
if (s->sector_next_status <= sector_num) {
BlockDriverState *file;
ret = bdrv_get_block_status(blk_bs(s->src[s->src_cur]),
sector_num - s->src_cur_offset,
n, &n, &file); [*]
if (ret & BDRV_BLOCK_ZERO) {
s->status = BLK_ZERO;
} else if (ret & BDRV_BLOCK_DATA) {
s->status = BLK_DATA;
} else if (!s->target_has_backing) {
/* Without a target backing file we must copy over the contents of
* the backing file as well. */
/* TODO Check block status of the backing file chain to avoid
* needlessly reading zeroes and limiting the iteration to the
* buffer size */
-o backing_fileが指定されてない場合は元のファイル全てコピーする必要がある。
s->status = BLK_DATA; [*]
} else {
s->status = BLK_BACKING_FILE; [*]
}
s->sector_next_status = sector_num + n;
}
n = MIN(n, s->sector_next_status - sector_num);
if (s->status == BLK_DATA) {
n = MIN(n, s->buf_sectors);
}
if (s->compressed) {
if (n < s->cluster_sectors) {
n = MIN(s->cluster_sectors, s->total_sectors - sector_num);
s->status = BLK_DATA;
} else {
n = QEMU_ALIGN_DOWN(n, s->cluster_sectors);
}
}
return n;
} static int convert_do_copy(ImgConvertState *s) { uint8_t buf = NULL; int64_t sector_num, allocated_done; int ret; int n; ... buf = blk_blockalign(s->target, s->buf_sectors * BDRV_SECTOR_SIZE); []
/* Calculate allocated sectors for progress */
s->allocated_sectors = 0;
sector_num = 0;
while (sector_num < s->total_sectors) { [*]
n = convert_iteration_sectors(s, sector_num); [*]
if (s->status == BLK_DATA) {
s->allocated_sectors += n; [*]
}
sector_num += n;
}
/* Do the copy */
s->src_cur = 0;
s->src_cur_offset = 0;
s->sector_next_status = 0;
sector_num = 0;
allocated_done = 0;
while (sector_num < s->total_sectors) {
n = convert_iteration_sectors(s, sector_num);
if (s->status == BLK_DATA) {
ret = convert_read(s, sector_num, n, buf); [*]
} else if (!s->min_sparse && s->status == BLK_ZERO) {
n = MIN(n, s->buf_sectors);
memset(buf, 0, n * BDRV_SECTOR_SIZE); [*]
s->status = BLK_DATA;
}
ret = convert_write(s, sector_num, n, buf); [*]
sector_num += n;
}
ret = 0;
}
||< []のループでは変換元のセクタ数を計るために走査のみ行います。 []のconvert_iteration_sectorsは変換元srcのsector_num番のセクタから末尾までの区間の状態(データかゼロデータかbacking fileか)を取得して、区間の長さを返します。(ちなみにこの長さはBDRV_REQUEST_MAX_SECTORSを超えた場合は分割して走査を行う) convert_iteration_sectorsは[]のbdrv_get_block_status(block/io.c)でステータスを取得しています。取得したステータスがBLOCK_ZERO(データが0)またはBLOCK_DATA(通常のデータ)のどちらでもない場合、それは該当qcow2にデータが存在しないことを意味します。これは該当データがバッキングファイルにある状態で、仮に-o backing_fileが指定されていない場合はこのバッキングファイルから読み込んでデータをコピーする必要があるため[]で通常のデータBLK_DATAとしています。もしオプションが指定されている場合は[*]でBLK_BACKING_FILEの状態とし、バッキングファイルからはコピーしません。
例えばqcow2だとqcow2_co_get_block_statusになります。
(block/qcow2.c)
|c| static int64_t coroutine_fn qcow2_co_get_block_status(BlockDriverState *bs, int64_t sector_num, int nb_sectors, int pnum, BlockDriverState **file) { ... pnum = nb_sectors; ret = qcow2_get_cluster_offset(bs, sector_num << 9, pnum, &cluster_offset);[] ... if (ret == QCOW2_CLUSTER_ZERO) { status |= BDRV_BLOCK_ZERO; } else if (ret != QCOW2_CLUSTER_UNALLOCATED) { status |= BDRV_BLOCK_DATA; } return status; } ||< この関数は第二引数のsector_numからnb_sectors数のセクタの状態を返します。 []のqcow2_get_cluster_offset(block/qcow2-cluster.c)を使って第二引数のアドレスからクラスターオフセットを取得して、さらにその位置の状態も返す。
(qcow2-cluster.c)
|c| int qcow2_get_cluster_offset(BlockDriverState *bs, uint64_t offset, int *num, uint64_t *cluster_offset) {
index_in_cluster = (offset >> 9) & (s->cluster_sectors - 1);
nb_needed = *num + index_in_cluster;
l1_bits = s->l2_bits + s->cluster_bits;
nb_available = (1ULL << l1_bits) - (offset & ((1ULL << l1_bits) - 1));
/* compute the number of available sectors */
nb_available = (nb_available >> 9) + index_in_cluster;
if (nb_needed > nb_available) {
nb_needed = nb_available;
}
assert(nb_needed <= INT_MAX);
*cluster_offset = 0;
/* seek to the l2 offset in the l1 table */
l1_index = offset >> l1_bits;
if (l1_index >= s->l1_size) {
ret = QCOW2_CLUSTER_UNALLOCATED;
goto out;
}
l2_offset = s->l1_table[l1_index] & L1E_OFFSET_MASK;
if (!l2_offset) {
ret = QCOW2_CLUSTER_UNALLOCATED;
goto out;
}
/* load the l2 table in memory */
ret = l2_load(bs, l2_offset, &l2_table);
if (ret < 0) {
return ret;
}
/* find the cluster offset for the given disk offset */
l2_index = (offset >> s->cluster_bits) & (s->l2_size - 1);
*cluster_offset = be64_to_cpu(l2_table[l2_index]);
/* nb_needed <= INT_MAX, thus nb_clusters <= INT_MAX, too */
nb_clusters = size_to_clusters(s, nb_needed << 9);
ret = qcow2_get_cluster_type(*cluster_offset);
switch (ret) {
case QCOW2_CLUSTER_COMPRESSED:
/* Compressed clusters can only be processed one by one */
c = 1;
*cluster_offset &= L2E_COMPRESSED_OFFSET_SIZE_MASK;
break;
case QCOW2_CLUSTER_ZERO:
c = count_contiguous_clusters_by_type(nb_clusters, &l2_table[l2_index],
QCOW2_CLUSTER_ZERO);
*cluster_offset = 0;
break;
case QCOW2_CLUSTER_UNALLOCATED:
/* how many empty clusters ? */
c = count_contiguous_clusters_by_type(nb_clusters, &l2_table[l2_index],
QCOW2_CLUSTER_UNALLOCATED);
*cluster_offset = 0;
break;
case QCOW2_CLUSTER_NORMAL:
/* how many allocated clusters ? */
c = count_contiguous_clusters(nb_clusters, s->cluster_size,
&l2_table[l2_index], QCOW_OFLAG_ZERO);
*cluster_offset &= L2E_OFFSET_MASK;
break;
default:
abort();
}
qcow2_cache_put(bs, s->l2_table_cache, (void**) &l2_table);
nb_available = (c * s->cluster_sectors);
out: if (nb_available > nb_needed) nb_available = nb_needed;
*num = nb_available - index_in_cluster;
return ret;
fail: qcow2_cache_put(bs, s->l2_table_cache, (void **)&l2_table); return ret; } ||<
qcow2のL1,L2テーブルに、該当するクラスタのエントリがない場合、QCOW2_CLUSTER_UNALLOCATEDが返される。 つまり[]のconvert_iterationではbdrv_get_block_statusによってs->src_urからsector_numまでのセクタの状態を求めている。 [],[]では変換元からnセクタ読み込み、nセクタ書き込んでいます。すなわちこれが変換の本質的な処理でフォーマット間の違いはこのread,writeが隠蔽して中間bufに読み書きをする事で行っている。 また[]ではmin_sparseが指定されておらず、かつデータが0の場合はスパースせずに0データをmemsetを使ってbufに書き込んでいる。つまりわざわざconvert_readを呼ばず、0とわかっているデータは高速化のためにmemsetで読み込むようにしている。またこれ以外にBLK_BACKING_FILEのだとconvert_write内で無視されるようになっている。
(qemu-img.c)
|c| static int convert_write(ImgConvertState *s, int64_t sector_num, int nb_sectors, const uint8_t *buf) { int ret;
while (nb_sectors > 0) {
int n = nb_sectors;
switch (s->status) {
case BLK_BACKING_FILE: [*]
assert(s->target_has_backing);
break;
case BLK_DATA:
if (s->compressed) {
if (s->has_zero_init && s->min_sparse &&
buffer_is_zero(buf, n * BDRV_SECTOR_SIZE))
{
assert(!s->target_has_backing);
break;
}
ret = blk_write_compressed(s->target, sector_num, buf, n);
if (ret < 0) {
return ret;
}
break;
}
if (!s->min_sparse ||
is_allocated_sectors_min(buf, n, &n, s->min_sparse))
{
ret = blk_write(s->target, sector_num, buf, n);
if (ret < 0) {
return ret;
}
break;
}
/* fall-through */
case BLK_ZERO:
if (s->has_zero_init) {
break;
}
ret = blk_write_zeroes(s->target, sector_num, n, 0);
if (ret < 0) {
return ret;
}
break;
}
sector_num += n;
nb_sectors -= n;
buf += n * BDRV_SECTOR_SIZE;
}
return 0;
} ||< [*]のBLK_BACKING_FILEの場合は書き込みを行わない。これはつまりqemu-img convertに-o backing_fileを指定している場合であり、このセクタはバッキングファイルに実体があるため、書き込む必要は無い。
さて今度はconvert_readを見てみる。 (qemu-img.c)
|c| static int convert_read(ImgConvertState *s, int64_t sector_num, int nb_sectors, uint8_t *buf) { int n; int ret;
assert(nb_sectors <= s->buf_sectors);
while (nb_sectors > 0) {
BlockBackend *blk;
int64_t bs_sectors;
convert_select_part(s, sector_num);
blk = s->src[s->src_cur];
bs_sectors = s->src_sectors[s->src_cur];
n = MIN(nb_sectors, bs_sectors - (sector_num - s->src_cur_offset));
ret = blk_read(blk, sector_num - s->src_cur_offset, buf, n); [*]
sector_num += n;
nb_sectors -= n;
buf += n * BDRV_SECTOR_SIZE;
}
return 0;
} ||< blk_readを呼び出している。blk_readはpart2でも触れたがIO処理用のコルーチンを作成して該当するフォーマットのblk_co_readを呼び出す。
(block-backend.c)
|c| BlockDriverState *blk_bs(BlockBackend *blk) { return blk->root ? blk->root->bs : NULL; } ... static int coroutine_fn blk_co_preadv(BlockBackend *blk, int64_t offset, unsigned int bytes, QEMUIOVector *qiov, BdrvRequestFlags flags) { int ret = blk_check_byte_request(blk, offset, bytes); if (ret < 0) { return ret; }
return bdrv_co_do_preadv(blk_bs(blk), offset, bytes, qiov, flags); [*]
||<
blk_readの場合は最終的にbdrv_co_do_preadvにBlockBackend->rootのBlockDriverStateを渡して呼び出す。これはbdrv_aligned_preadv(block/io.c)でこれはbdrv_co_io_emを呼ぶ。このbdrv_co_io_emは非同期I/Oをサポートしていないフォーマット用のエミュレート関数で、raw_aio_readv(block/raw-posix.c)を呼び出す。前にも書いたが、qcow2も最終的な読み書きはrawフォーマットとして行う。
続いてqemu-img commitを見る。これは
|sh| qemu-img commit test.qcow2 ||< とすると、test.qcow2のバッキングファイルにtest.qcow2のdiffを適用(commit)する。
(qemu-img.c)
|c| static int img_commit(int argc, char **argv) { ... blk = img_open(image_opts, filename, fmt, flags, writethrough, quiet); [*] ... bs = blk_bs(blk);
if (base) {
base_bs = bdrv_find_backing_image(bs, base); [*]
} else {
base_bs = backing_bs(bs); [*]
}
cbi = (CommonBlockJobCBInfo){
.errp = &local_err,
.bs = bs,
};
commit_active_start(bs, base_bs, 0, BLOCKDEV_ON_ERROR_REPORT,
common_block_job_cb, &cbi, &local_err); [*]
run_block_job(bs->job, &local_err); [*]
} ||< 参考資料 Coroutines in QEMU: The basics [http://blog.vmsplice.net/2014/01/coroutines-in-qemu-basics.html]