この記事は ドワンゴ Advent Calendar 2025 の 23 日目の記事です。
はじめに
この記事は、端的にまとめると「QEMU すごかった」という内容です。 ドワンゴ Advent Calendar ですが、何かドワンゴのサービス等に関連する内容でもありません。
また、すでに QEMU がすごいということを知っている方からすると、何か新しい事柄はないかもしれません。 それに、私が無邪気にすごかったと思っているだけで、もう皆さん当たり前に知っている内容かもしれません。
でも、せっかくなので最後まで目を通してもらえるとうれしいです。
今回は QEMU の機能を使って NVMe の処理の流れを見ていきたいと思います。
NVMe とは
NVMe (Non-Volatile Memory Express) は、SSD などの不揮発性メモリを PCI Express バス上で効率的に扱うことを念頭に設計されたストレージ向けのプロトコルです。現在では多くの PC でも採用されており、みなさんも耳にしたことがあると思います。従来 SATA で利用されていた AHCI と比較してより効率的に処理が可能となっており、SSD の性能を活かすことが可能となりました。
NVMe の基本構造
NVMe において中心的な概念としてキューがあります。
- Submission Queue (SQ) : 実行してほしいコマンドを投入するキュー
- Completion Queue (CQ) : NVMe コントローラーが完了エントリを書き込み、ホストが消費するキュー
これらのキューは、デバイス上ではなくホストのメモリ上に配置されます。 プロセスは NVMe ドライバーを通じて、これらのキューを操作するとともに、 NVMe コントローラーはこれらのキューを通じて IO を処理します。
NVMe においては、これらのキューを複数 (最大 64k 個) 持つことが可能であるとともに、最大 64k 個のコマンドをそれぞれのキューに保持することが可能です。 そのため、 CPU コアごとに専用のキューを割り当てるなどのことが可能となり、並列な IO 処理が可能となっています。
また、SQ にコマンドを書き込んだ際に NVMe コントローラーにそれらのコマンドの存在を知らせるための仕組みとして Doorbell が存在します。 コマンドを追加したホストは、新しいコマンドが追加されたことを Doorbell を用いて NVMe コントローラーに通知を行い、IO の処理を行います。
これらの仕様は 公開されており 、詳細を確認することも可能です。
3行まとめ
- NVMe はメモリ上のキューを使ってやりとりをする
- コマンドを投入する SQ と、完了エントリが書き込まれる CQ がある
- SQ の更新は Doorbell でコントローラーに通知し、CQ はホストが消費位置を Doorbell で通知する
NVMe の挙動を観察
NVMe の挙動は QEMU を利用することで観察することができます。 QEMU では NVMe コントローラーが実装されています。そのため、 OS と NVMe コントローラーのやりとりを確認することが可能です。
今回 macOS 上の QEMU で Alpine Linux を起動し、挙動を確認しました。
- MacBook Air 2020
- M1 / 8GB / 256GB / macOS Tahoe 26.2
- Alpine Linux 3.20.2 x86_64
- QEMU emulator version 8.1.1
まず NVMe ドライブとして利用するディスクイメージを作成します。
$ qemu-img create -f raw nvme.img 2G
次に QEMU で trace するイベントを選定します。
QEMU で trace できるイベントは qemu-system-x86_64 -trace help で表示できますが、そのうち今回は NVMe に関連しそうなものだけを抽出します。
$ qemu-system-x86_64 -trace help|grep nvme > trace-nvme.txt
次に Alpine Linux の ISO イメージをダウンロードします。
$ curl -LO https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-standard-3.20.2-x86_64.iso
ここまで準備ができたら実際に QEMU を起動します。
$ qemu-system-x86_64 \
-machine q35,accel=tcg \
-m 1024 -smp 2 \
-drive file=nvme.img,if=none,format=raw,id=drv0 \
-device nvme,serial=deadbeef,id=nvme0 \
-device nvme-ns,drive=drv0,nsid=1,uuid=auto \
-cdrom alpine-standard-3.20.2-x86_64.iso \
-boot d \
-nographic \
-trace events=trace-nvme.txt,file=qemu-nvme.log \
-D /dev/null
上記の引数のうち、 -trace オプションの file において指定したファイルに trace したイベントが記録されます。
ちなみに -D /dev/null は、これをつけないと -trace の file で指定したファイルにログが吐かれませんでした。(そのような仕様なのか、意図せぬ挙動なのかは調べていませんが、解消したいところですね)
起動すると qemu-nvme.log にたくさんイベントが出力されることが確認できると思います。
一例として起動した Linux 上で dd if=/dev/nvme0n1 of=/dev/null bs=4096 count=1 を実行すると、手元では下記のような出力がされました。
pci_nvme_mmio_write addr 0x1010 data 0xe size 4
pci_nvme_mmio_doorbell_sq sqid 2 new_tail 14
pci_nvme_io_cmd cid 54144 nsid 0x1 sqid 2 opc 0x2 opname 'NVME_NVM_CMD_READ'
pci_nvme_read cid 54144 nsid 1 nlb 32 count 16384 lba 0x0
pci_nvme_mmio_write addr 0x1014 data 0xe size 4
pci_nvme_mmio_doorbell_cq cqid 2 new_head 14
上記は
- SQ の Doorbell を鳴らしてコマンドが投入されていることを通知する (L.1 ~ L.2)
- SQ から READ コマンドを読み込む
- READ を実施する
- 完了を受け取ったホストが CQ の Doorbell を鳴らし、消費位置を通知する
という流れを観測できています。
上記の出力は NVMe デバイス(QEMU)側で定義された trace イベントであり、 ホストが行った MMIO アクセスと、 それを受けて NVMe コントローラーが処理した内容のみが観測されている。
一方で、SQ に Doorbell を鳴らす前にコマンドを投入する部分については確認することができていません。 また、 CQ に NVMe コントローラーが完了エントリを書き込んだことも確認することができていません。 これらの NVMe ドライバーがメモリ上の SQ にコマンドを投入する処理や、NVMe コントローラーが CQ に完了エントリを書き込む処理はメモリ操作であるため、ここから確認することはできません。
では、今度は書き込みのために dd if=/dev/zero of=/dev/nvme0n1 bs=4096 count=1 を実行してみます。
pci_nvme_mmio_write addr 0x1008 data 0xdd size 4
pci_nvme_mmio_doorbell_sq sqid 1 new_tail 221
pci_nvme_io_cmd cid 58112 nsid 0x1 sqid 1 opc 0x1 opname 'NVME_NVM_CMD_WRITE'
pci_nvme_write cid 58112 opname 'NVME_NVM_CMD_WRITE' nsid 1 nlb 8 count 4096 lba 0x0
pci_nvme_mmio_write addr 0x100c data 0xdd size 4
pci_nvme_mmio_doorbell_cq cqid 1 new_head 221
SQ や CQ の Doorbell については同じですが、コマンドの処理が NVME_NVM_CMD_WRITE となっており、 WRITE コマンドが実行されたことがわかります。
実際の挙動を見ることで、仕様書に書いてある内容がよりイメージがつきやすい状況になったと思います。
3行まとめ
- QEMU はイベントの trace ができる
- trace されるのは NVMe コントローラーで発生したイベント
- NVMe ドライバーの挙動は trace できない
dm_delay を利用した IO の遅延
では、もう少し違ったケースも見ていきたいと思います。 ここでは、Device Mapper の dm_delay を利用して IO を遅延させたときの NVMe コントローラーの挙動についてみていきたいと思います。
Device Mapper と dm_delay とは
Device Mapper とは Linux で利用するブロックデバイスの手前に仮想的なブロックデバイスを作成し、IO に様々な機能を提供する仕組みです。 そのうち、今回利用する dm_delay は IO の遅延を発生させることができます。
実施手順
まず Alpine Linux のネットワークを有効化させ、インターネット上のリポジトリを利用可能な状態にしたうえで device-mapper と util-linux パッケージを導入します。
ip link set eth0 up || true
udhcpc -i eth0 -q -t 5 -n || true
cat > /etc/apk/repositories <<'EOR'
https://dl-cdn.alpinelinux.org/alpine/v3.20/main
https://dl-cdn.alpinelinux.org/alpine/v3.20/community
EOR
apk update
apk add device-mapper util-linux
modprobe dm_delay
次に dm_delay を利用したブロックデバイスを作成します。 下記のコマンドでは /dev/nvme0n1 をバックエンドとして 10 秒の遅延を行う delay0 という名前のブロックデバイスを作成しています。 dmsetup コマンドで作成すると /dev/mapper に作成したデバイス (delay0) が現れていることを確認できます。
# dmsetup create delay0 --table \
"0 $(blockdev --getsz /dev/nvme0n1) delay /dev/nvme0n1 0 10000 /dev/nvme0n1 0 10000"
# localhost:~# ls /dev/mapper/
control delay0
この状態で dd if=/dev/zero of=/dev/mapper/delay0 bs=4096 count=1 を実行すると、実行に 10 秒ほどかかっていることが確認できると思います。
これは /dev/mapper/delay0 が IO を遅延させているということがわかっている状況ですが、 QEMU の trace の結果を見るとよりそれがわかりやすくなります。
下記は実際に実行した際の様子ですが、 dd コマンドの実行に時間を要しているものの、 NVMe コントローラーの処理は一瞬で終わっている状況が確認できると思います。

今回の場合、原因がはっきりとわかっていたので特に面白みや意外性はないのですが、IO 性能が想定より出ないときには今回のようなアプローチで調査をすることも状況によっては有用かもしれません。 (本当はもう少し良い例があればよかったのですが…すみません)
3行まとめ
- Device Mapper の dm_delay を使うと IO を遅延させることができる
- dm_delay で遅延させる場合は、仮想的に作ったブロックデバイス上で遅延させるので NVMe の処理が遅延しているわけではない
- QEMU の trace を使うと目視できてイメージしやすい
次のステップ
ここまで、 NVMe の挙動に関して見てきました。
QEMU で trace しているのは NVMe コントローラーでの処理の内容です。
では、実際にコマンドを発行しているのは誰かというと、 NVMe ドライバーです。今回の場合、 Linux の NVMe ドライバーの挙動を観察してきましたが、 NVMe ドライバーが違えば挙動が異なります。
下記の記事では別のアプローチではありますが、 QEMU で Windows を起動し、その NVMe ドライバーの挙動を確認しています。
また、 Linux や Windows に含まれる NVMe ドライバーの他には AWS NVMe ドライバーも存在します。
次のステップとしては、これらの NVMe ドライバーによって異なるコマンドを観察することで、それぞれのドライバーの特徴もわかるかもしれません。
3行まとめ
- NVMe コントローラーにコマンドを投げるのは NVMe ドライバー
- NVMe ドライバーが違えば挙動が違う
- 異なる NVMe ドライバーの挙動を観察するのも興味深いかもしれません
さいごに
ここまで読んでいただきありがとうございました。
実のところ、私は普段から QEMU をとてもよく使って開発をしているわけでも、 NVMe ドライバーを開発したりしているわけではありません。 なので、あまり深い内容でもなければ、なんらかの実際の開発で役立つような内容ではなかったと思います。
しかしながら、この記事を読んで何かひとつでも「知っていることが増えた!」と思っていただくことができたのであれば、非常にうれしく思います。