[Qemu] NVMeドライブエミュレーション機能の改造（SMART情報）

# はじめに
　[以前の記事](https://qiita.com/ken-yossy/items/5bfb0c873b35f6a1ee14)で、Windows上でのNVMeドライブへのアクセス方法等を調べるためにQemu<sup>[1]</sup>のNVMeドライブエミュレーション機能を使う方法をご紹介しました。その記事作成中にQemuのNVMeドライブエミュレーション機能（というかソース）を見ていると、この機能があまり使われていないのか、対応する仕様（バージョン）が古いだけでなく機能の不足もそれなりにあることがわかりました。

　そこで今回は、NVMeの仕様上必須(mandatory)とされているSMART情報に関する機能に注目しました。

# 3行でまとめると：
* Qemu 4.1.0でエミュレーションされたNVMeドライブをCrystal Disk Infoで見たら「不明(unknown)」だった
* 調べたところ、Qemu 4.1.0のNVMeエミュレーションではSMART情報の取得（`Get Log Page`コマンド）に未対応だった
* エミュレーションコードを改造して`Get Log Page`コマンドのうちSMART情報の取得に対応させたところ、Crystal Disk Infoでドライブ情報が表示されるようになった

# 事の発端と顛末
　[以前の記事](https://qiita.com/ken-yossy/items/5bfb0c873b35f6a1ee14)に記載したように、QemuのWebサイトからバージョン4.1.0のソースファイルを取得してビルドし、WindowsをゲストOSとして、エミュレートされたNVMeドライブを接続すると、そのNVMeドライブに対してフォーマットしたりデータの読み書きをすることはできます。

　その時ふとCrystal Disk Info (CDI)<sup>[2]</sup>でそのエミュレートされたNVMeドライブの状態を見てみたのですが、図1のようになりました。

![Qemu 4.1.0でエミュレートされたNVMeドライブの情報をCrystal Disk Infoで見た結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439683/0c5a3b1d-31d4-903d-a6ba-d1b970f52a33.png "Qemu 4.1.0でエミュレートされたNVMeドライブの情報をCrystal Disk Infoで見た結果")
図1：Qemu 4.1.0でエミュレートされたNVMeドライブの情報をCrystal Disk Infoで見た結果

　ちょっと残念な表示です。

　そこで、QemuのNVMeドライブエミュレーションコードを改造したところ、図2のようにできました。

![図2：NVMeドライブのエミュレーションコードを弄ってCrystal Disk Infoでまともに表示されるようにした結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439683/f03a98d4-0bdc-5cb1-0b2e-8278748ddd25.png "NVMeドライブのエミュレーションコードを弄ってCrystal Disk Infoでまともに表示されるようにした結果")
図2：NVMeドライブのエミュレーションコードを改造した結果

　だいぶまともになりました。といってもこれが普通なのですが。

　結論から言うと、図1の状況を図2の状況にするために改造したのは、SMART情報を取得するコマンドの処理だけです。Qemu v4.1.0では、SMART情報を取得するためのNVMeコマンドである`Get Log Page`コマンドがまともに実装されていなかったのです。

　Qemu 4.1.0のソースファイル内のコメントには、このコードはNVMeのバージョン1.2.0<sup>[3]</sup>に準拠していると記載されていますが、その仕様上実装が必須である`Get Log Page`コマンドが実装されていないので、CDIはSMART情報が取得できず、ドライブの情報・状態を「不明」としていたと推測されます。

# 具体的な改造内容
　今回の改造では、`Get Log Page`コマンドの処理、およびSMART情報に関する処理を追加しました。

　以下は、NVMeドライブエミュレーションコード内のAdminコマンド処理部分に関する改造内容です。各コマンドの処理に分岐する部分に、`Get Log Page`コマンドに対応する記述を追加しています。

```diff
diff --git a/hw/block/nvme.c b/hw/block/nvme.c
index 36d6a8b..ab10773 100644
--- a/hw/block/nvme.c
+++ b/hw/block/nvme.c
 static uint16_t nvme_admin_cmd(NvmeCtrl *n, NvmeCmd *cmd, NvmeRequest *req)
 {
     switch (cmd->opcode) {
@@ -850,6 +887,8 @@ static uint16_t nvme_admin_cmd(NvmeCtrl *n, NvmeCmd *cmd, NvmeRequest *req)
         return nvme_del_sq(n, cmd);
     case NVME_ADM_CMD_CREATE_SQ:
         return nvme_create_sq(n, cmd);
+    case NVME_ADM_CMD_GET_LOG_PAGE:
+        return nvme_get_log_page(n, cmd, req);
     case NVME_ADM_CMD_DELETE_CQ:
         return nvme_del_cq(n, cmd);
     case NVME_ADM_CMD_CREATE_CQ:
@@ -1302,10 +1341,167 @@ static const MemoryRegionOps nvme_cmb_ops = {
     },
 };
```

　また、そもそもSMART情報を管理するコードを追加する必要がありました。

　Qemu 4.1.0にはSMART情報を実装した構造体は存在します。ですので、下記のように、その構造体をエミュレーションコード内で適切に使ってあげれば良いことになります。

```diff
diff --git a/hw/block/nvme.h b/hw/block/nvme.h
index 557194e..1708a1b 100644
--- a/hw/block/nvme.h
+++ b/hw/block/nvme.h
@@ -89,6 +89,7 @@ typedef struct NvmeCtrl {
     NvmeSQueue      admin_sq;
     NvmeCQueue      admin_cq;
     NvmeIdCtrl      id_ctrl;
+    NvmeSmartLog    smart;
 } NvmeCtrl;
 
 #endif /* HW_NVME_H */
```

　ただし、Qemu 4.1.0のコードで定義されているSMART情報の構造体は、NVMe 1.2.0仕様に対していくつかフィールドが足りません。ですので、下記のように追加・変更しました。

```diff
diff --git a/include/block/nvme.h b/include/block/nvme.h
index 3ec8efc..321fa8f 100644
--- a/include/block/nvme.h
+++ b/include/block/nvme.h
@@ -504,7 +504,10 @@ typedef struct NvmeSmartLog {
     uint64_t    unsafe_shutdowns[2];
     uint64_t    media_errors[2];
     uint64_t    number_of_error_log_entries[2];
-    uint8_t     reserved2[320];
+    uint32_t    warning_temperature_time;
+    uint32_t    critical_temperature_time;
+    uint16_t    temperature_sensor[8];
+    uint8_t     reserved2[296];
 } NvmeSmartLog;
 
 enum NvmeSmartWarn {
```

　また、SMART情報は「統計情報」ですので、ドライブの電源断を経ても情報の内容が引き継がれるべきです。今回は、できるだけ楽をするために、電源断時にSMART情報をファイルにダンプして、電源投入時にそのダンプファイルを読み出す、という実装にしました。電源オンオフ回数（`Power Cycles`のこと）のみ処理を追加してみました。

　このエミュレーションにおいて電源オンオフ回数を愚直に記録することに意義があるかは微妙ですが、他の統計情報も同じ手法で不揮発化できるという意味で、ローテクではありますが意義はあるかな、と考えています。

　以上の改造をまとめると以下のような感じです。「★」以降の部分は説明のために追記した内容です。

```diff
diff --git a/hw/block/nvme.c b/hw/block/nvme.c
index 36d6a8b..ab10773 100644
--- a/hw/block/nvme.c
+++ b/hw/block/nvme.c
@@ -39,9 +39,55 @@
 #include "qemu/log.h"
 #include "qemu/module.h"
 #include "qemu/cutils.h"
+#include "qemu/qemu-print.h"
+#include "monitor/monitor.h"
 #include "trace.h"
 #include "nvme.h"

+#include <fcntl.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <stdio.h>
+
+#define NVME_SMART_LOG_FILE "smartlog.bin"
+
★ SMART情報のPower Cyclesを1増やす
+static void nvme_smart_inc_num_power_cycle(NvmeCtrl *_ctrl)
+{
+    (_ctrl->smart.power_cycles[0])++;
+
+    if ( _ctrl->smart.power_cycles[0] == 0 ) {
+        (_ctrl->smart.power_cycles[1])++;
+    }
+}
+
★ SMART情報（512バイト）をファイルにダンプする
+static void nvme_smart_save(NvmeCtrl *_ctrl)
+{
+    int fd = creat( NVME_SMART_LOG_FILE, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH );
+    if ( fd != -1 ) {
+	ssize_t wn = write( fd, (const void*)&(_ctrl->smart), sizeof(NvmeSmartLog) );
+	if ( wn != sizeof(NvmeSmartLog) ) {
+	    qemu_printf( "[NVME] something wrong occurred in writing file: %s\n", strerror(errno) );
+	}
+    } else {
+	qemu_printf( "[NVME] something wrong occurred in creating file: %s\n", strerror(errno) );
+    }
+}
+
★ ファイルからSMART情報を読み出して復元する
+static void nvme_smart_load(NvmeCtrl *_ctrl)
+{
+    memset( &(_ctrl->smart), 0, sizeof(NvmeSmartLog) ); // clear
+    int fd = open( NVME_SMART_LOG_FILE, O_RDONLY );
+    if ( fd != -1 ) {
+	ssize_t rn = read( fd, (void*)&(_ctrl->smart), sizeof(NvmeSmartLog) );
+	if ( rn != sizeof(NvmeSmartLog) ) {
+	    qemu_printf( "[NVME] something wrong occurred in reading file: %s\n", strerror(errno) );
+	}
+    } else {
+	qemu_printf( "[NVME] something wrong occurred in opening file: %s\n", strerror(errno) );
+    }
+}
+
 #define NVME_GUEST_ERR(trace, fmt, ...) \
     do { \
         (trace_##trace)(__VA_ARGS__); \
@@ -843,6 +889,40 @@ static uint16_t nvme_set_feature(NvmeCtrl *n, NvmeCmd *cmd, NvmeRequest *req)
     return NVME_SUCCESS;
 }

★ SMART情報をホストに返す処理 
+static uint16_t nvme_get_smart(NvmeCtrl *_ctrl, NvmeCmd *_cmd, NvmeRequest *_req)
+{
+    uint64_t prp1 = le64_to_cpu(_cmd->prp1);
+    uint64_t prp2 = le64_to_cpu(_cmd->prp2);
+    uint16_t numd = ( le32_to_cpu(_cmd->cdw10) >> 16 ) & 0xFF; // Number of Dwords (NUMD)
+
+    // REVISIT: currently, allows only transfering whole data (512byte) at one time
+    if ( ( numd + 1 ) < sizeof(NvmeSmartLog) / 4 ) {
+        return NVME_INVALID_FIELD;
+    }
+
+    return nvme_dma_read_prp(_ctrl, (uint8_t *)&_ctrl->smart, sizeof(NvmeSmartLog), prp1, prp2);
+}
+
★ Get Log Pageコマンドの処理
+static uint16_t nvme_get_log_page(NvmeCtrl *_ctrl, NvmeCmd *_cmd, NvmeRequest *_req)
+{
+    uint32_t cdw10 = le32_to_cpu(_cmd->cdw10);
+    uint8_t  lid   = cdw10 & 0xFF; // Log Page Identifier (LID)
+
+    switch ( lid ) {
+    case NVME_LOG_ERROR_INFO:
+        break;
+    case NVME_LOG_SMART_INFO:
+        return nvme_get_smart(_ctrl, _cmd, _req);
+    case NVME_LOG_FW_SLOT_INFO:
+        break;
+    default:
+        // REVISIT: need to implement trace event like "trace_nvme_err_invalid_logid(cdw10)"
+        return NVME_INVALID_LOG_ID | NVME_DNR;
+    }
+
+    return NVME_SUCCESS;
+}
+
 static uint16_t nvme_admin_cmd(NvmeCtrl *n, NvmeCmd *cmd, NvmeRequest *req)
 {
     switch (cmd->opcode) {
@@ -850,6 +930,8 @@ static uint16_t nvme_admin_cmd(NvmeCtrl *n, NvmeCmd *cmd, NvmeRequest *req)
         return nvme_del_sq(n, cmd);
     case NVME_ADM_CMD_CREATE_SQ:
         return nvme_create_sq(n, cmd);
+    case NVME_ADM_CMD_GET_LOG_PAGE:
+        return nvme_get_log_page(n, cmd, req);
     case NVME_ADM_CMD_DELETE_CQ:
         return nvme_del_cq(n, cmd);
     case NVME_ADM_CMD_CREATE_CQ:
@@ -1086,6 +1168,9 @@ static void nvme_write_bar(NvmeCtrl *n, hwaddr offset, uint64_t data,
             nvme_clear_ctrl(n);
             n->bar.cc = data;
             n->bar.csts |= NVME_CSTS_SHST_COMPLETE;
+
★ ホストからシャットダウン要求を受信した時に、SMART情報のPower Cyclesを1増やして
★ SMART情報自体をファイルにダンプする
+            nvme_smart_inc_num_power_cycle( n ); // record as "Power Cycle"
+            nvme_smart_save(n); // save SMART data at shutdown event
         } else if (!NVME_CC_SHN(data) && NVME_CC_SHN(n->bar.cc)) {
             trace_nvme_mmio_shutdown_cleared();
             n->bar.csts &= ~NVME_CSTS_SHST_COMPLETE;
@@ -1302,6 +1387,20 @@ static const MemoryRegionOps nvme_cmb_ops = {
     },
 };

★ SMART情報をファイルから読み込んで初期化する
+static void nvme_realize_smart_log(NvmeCtrl *_ctrl)
+{
+    NvmeSmartLog *log = &(_ctrl->smart);
+
+    nvme_smart_load(_ctrl);
+
+    // followings are just temporary...?
+    log->temperature[0] = ( cpu_to_le16( 273 + 30 ) & 0xFF );
+    log->temperature[1] = ( ( cpu_to_le16( 273 + 30 ) >> 8 ) & 0xFF );
+    log->available_spare = 100;
+    log->available_spare_threshold = 10;
+    log->temperature_sensor[0] = cpu_to_le16( 273 + 30 );
+}
+
 static void nvme_realize(PCIDevice *pci_dev, Error **errp)
 {
     NvmeCtrl *n = NVME(pci_dev);
@@ -1381,6 +1480,8 @@ static void nvme_realize(PCIDevice *pci_dev, Error **errp)
         id->vwc = 1;
     }

★ コントローラ初期化内でSMART情報の初期化（ファイルからの読み込み）を行う
+    nvme_realize_smart_log(n);
+
     n->bar.cap = 0;
     NVME_CAP_SET_MQES(n->bar.cap, 0x7ff);
     NVME_CAP_SET_CQR(n->bar.cap, 1);
```

　その他、SMART情報には温度の情報がありますが、エミュレーションでは「実際の温度」はありませんので、摂氏30度（絶対温度で305ケルビン）をハードコーディングしています。総書き込み量・総読み出し量や、電源オン時間などの値も0を入れているだけです。

　なおQemuには、システムエミュレータならではである、スナップショットやマイグレーションといった機能がありますが、このNVMeドライブエミュレーション機能はそれらに対応していませんので、今回の改造で追加したこのSMARTに関する機能もそれらの機能には対応させていません。

# まとめ
　QemuのNVMeドライブエミュレーション機能について、今回はNVMeの仕様上必須(mandatory)とされているSMART情報に関する機能に注目し、Crystal Disk Infoでまともにドライブ情報が表示される程度まで改造しました。

　QemuのNVMeドライブエミュレーション機能には、SMART以外にも、仕様（バージョン）が古いことなどの課題もありますので、今後も少しずつ改造していこうかなと考えています。

　なお、改造したコードは[Github上のリポジトリ](https://github.com/ken-yossy/nvme-model-for-qemu)に置いてあります。

# References
[1] _QEMU_, https://www.qemu.org/
[2] Crystal Disk Info, https://crystalmark.info/ja/software/crystaldiskinfo/
[3] NVM Express, _"NVM Express<sup>TM</sup> Base Specification"_, Revision 1.2, Nov., 2014

# ライセンス表記
<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="クリエイティブ・コモンズ・ライセンス" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" /></a><br />この記事は<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンス</a>の下に提供されています。