ハイパーバイザの作り方~ちゃんと理解する仮想化技術~ 第20回 bhyveにおける仮想ディスクの実装
前回の記事では、bhyveにおける仮想NICの実装についてTAPデバイスを用いたホストドライバの実現方法を例に挙げ解説しました。
今回の記事では、bhyveにおける仮想ディスクの実装について解説していきます。
bhyveがゲストマシンに提供する仮想IOデバイスは、全てユーザプロセスである/usr/sbin/bhyve上に実装されています(図1)。
bhyveは実機上のディスクコントローラと異なり、ホストOSのファイルシステム上のディスクイメージファイルに対してディスクIOを行います。
これを実現するために、/usr/sbin/bhyve上の仮想ディスクコントローラは、ゲストOSからのIOリクエストをディスクイメージファイルへのファイルIOへ変換します。
以下に、ディスク読み込み手順と全体図(図1)を示します。
書き込み処理では、リクエストと共にデータをリングバッファを用いて送りますが、それ以外は読み込みと同様です。
これまでに、準仮想化I/Oの仕組みとして、virtioとVirtqueue、virtio-netについて解説してきました。ここでは、ブロックデバイスを準仮想化する、virtio-blkについて解説を行います。
virtio-netは受信キュー、送信キュー、コントロールキューの3つのVirtqueueからなっていましたが、virtio-blkでは単一のVirtqueueを用います。これは、ディスクコントローラの挙動がNICとは異なり、必ずOSからコマンド送信を行った後にデバイスからレスポンスが返るという順序になるためです1。
ブロックIOのリクエストは連載第12回の「ゲスト→ホスト方向のデータ転送方法」で解説した手順で送信されます。
virtio-blkでは1つのブロックIOリクエストに対して、以下のようにDescriptor2群を使用します。1個目のDescriptorはstruct virtio_blk_outhdr(表1)を指します。この構造体にはリクエストの種類、リクエスト優先度、アクセス先オフセットを指定します。2〜(n−1)個目以降のDescriptorはリクエストに使用するバッファを指します。リクエストがreadな場合は読み込み結果を入れる空きバッファを、writeな場合は書き込むデータを含むバッファを指定します。
バッファのアドレスは物理アドレス指定になるため、仮想アドレスで連続した領域でも物理的配置がバラバラな状態な場合があります。これをサポートするためにバッファ用Descriptorを複数に別けて確保出来るようになっています。
struct virtio_blk_outhdrにはバッファ長のフィールドがありませんが、これはDescriptorのlenフィールドを用いてホストへ通知されます。n個目のDescriptorは1byteのステータスコード(表2)のアドレスを指します。このフィールドはホスト側がリクエストの実行結果を返すために使われます。
type | member | description |
---|---|---|
u32 | type | リクエストの種類(read=0x0, write=0x1, ident=0x8) |
u32 | ioprio | リクエスト優先度 |
u64 | sector | セクタ番号(オフセット値) |
type | member | description |
---|---|---|
0 | OK | 正常終了 |
1 | IOERR | IOエラー |
2 | UNSUPP | サポートされないリクエスト |
/usr/sbin/bhyveはvirtio-blkを通じてゲストOSからディスクIOリクエストを受け取り、ディスクイメージへ読み書きを行います。bhyveが対応するディスクイメージはRAW形式のみなので、ディスクイメージへの読み書きはとても単純です。ゲストOSから指定されたオフセット値とバッファ長をそのまま用いてディスクイメージへ読み書きを行えばよいだけです3。
それでは、このディスクイメージへのIOの部分についてbhyveのコードを実際に確認してみましょう。/usr/sbin/bhyveの仮想ディスクIO処理のコードをコードリスト1に示します。
/* ゲストOSからIO要求があった時に呼ばれる */
static void
pci_vtblk_proc(struct pci_vtblk_softc *sc, struct vqueue_info *vq)
{
struct virtio_blk_hdr *vbh;
uint8_t *status;
int i, n;
int err;
int iolen;
int writeop, type;
off_t offset;
struct iovec iov[VTBLK_MAXSEGS + 2];
uint16_t flags[VTBLK_MAXSEGS + 2];
/* iovに1リクエスト分のDescriptorを取り出し */
n = vq_getchain(vq, iov, VTBLK_MAXSEGS + 2, flags);
〜 略 〜
/* 一つ目のDescriptorはstruct virtio_blk_outhdr */
vbh = iov[0].iov_base;
/* 最後のDescriptorはステータスコード */
status = iov[--n].iov_base;
〜 略 〜
/* リクエストの種類 */
type = vbh->vbh_type;
writeop = (type == VBH_OP_WRITE);
/* オフセットをsectorからbyteに変換 */
offset = vbh->vbh_sector * DEV_BSIZE;
/* バッファの合計長 */
iolen = 0;
for (i = 1; i < n; i++) {
〜 略 〜
iolen += iov[i].iov_len;
}
〜 略 〜
switch (type) {
/* WRITEならpwritev()でiovの配列で表されるバッファリストからディスクイメージへ書き込み */
case VBH_OP_WRITE:
err = pwritev(sc->vbsc_fd, iov + 1, i - 1, offset);
break;
/* READならpreadv()でディスクイメージからiovの配列で表されるバッファリストへ読み込み */
case VBH_OP_READ:
err = preadv(sc->vbsc_fd, iov + 1, i - 1, offset);
break;
/* IDENTなら仮想ディスクのidentifyを返す */
case VBH_OP_IDENT:
/* Assume a single buffer */
strlcpy(iov[1].iov_base, sc->vbsc_ident,
min(iov[1].iov_len, sizeof(sc->vbsc_ident)));
err = 0;
break;
default:
err = -ENOSYS;
break;
}
〜 略 〜
/* ステータスコードのアドレスにIOの結果を書き込む */
if (err < 0) {
if (err == -ENOSYS)
*status = VTBLK_S_UNSUPP;
else
*status = VTBLK_S_IOERR;
} else
*status = VTBLK_S_OK;
〜 略 〜
/* ステータスコードを書き込んだ事を通知 */
vq_relchain(vq, 1);
}
今回は仮想マシンのストレージデバイスについて解説しました。 次回は、仮想マシンのコンソールデバイスについて解説します。